semql-erd 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Nikhil Pallamreddy
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: semql-erd
3
+ Version: 0.1.0
4
+ Summary: Graphviz ER diagrams from a semql Catalog — cubes as nodes, joins as edges with crow's-foot arrowheads.
5
+ Author: Nikhil Pallamreddy
6
+ Author-email: Nikhil Pallamreddy <nikhil.pallamreddy+git@gmail.com>
7
+ License-Expression: BSD-3-Clause
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Database
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Multimedia :: Graphics
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: semql>=0.1.0,<0.2
20
+ Requires-Dist: graphviz>=0.20 ; extra == 'image'
21
+ Requires-Python: >=3.12
22
+ Project-URL: Homepage, https://github.com/npalladium/semql
23
+ Project-URL: Repository, https://github.com/npalladium/semql
24
+ Project-URL: Issues, https://github.com/npalladium/semql/issues
25
+ Provides-Extra: image
26
+ Description-Content-Type: text/markdown
27
+
28
+ # semql-erd
29
+
30
+ ER-diagram generator for [`semql`](../semql) catalogues. Walks the
31
+ cubes and joins in a `Catalog` and emits a [Graphviz](https://graphviz.org)
32
+ DOT source (and convenience PNG/SVG when the `graphviz` Python bindings
33
+ + system `dot` binary are available).
34
+
35
+ Useful when:
36
+ - The catalogue is past 10 cubes and reading the YAML/Python isn't
37
+ enough to see the join shape at a glance.
38
+ - A PR touches a `Join` and the reviewer wants a visual diff of the
39
+ before / after graph.
40
+ - Onboarding docs need a stable picture of what's in scope.
41
+
42
+ ## Install
43
+
44
+ ```sh
45
+ pip install semql-erd # DOT source only — no system deps
46
+ pip install "semql-erd[image]" # + graphviz Python bindings
47
+ # (also needs the `dot` binary)
48
+ ```
49
+
50
+ ## Quick start — DOT source
51
+
52
+ `render_dot(catalog)` is dependency-free: it produces a DOT-language
53
+ string you can paste into any Graphviz renderer
54
+ ([Edotor](https://edotor.net) is a quick web one).
55
+
56
+ ```python
57
+ from semql import Backend, Catalog, Cube, Dimension, Join, Measure
58
+ from semql_erd import render_dot
59
+
60
+ orders = Cube(
61
+ name="orders",
62
+ backend=Backend.POSTGRES,
63
+ table="orders",
64
+ alias="o",
65
+ measures=[Measure(name="revenue", sql="{o}.amount", agg="sum", unit="currency")],
66
+ dimensions=[Dimension(name="region", sql="{o}.region", type="string")],
67
+ joins=[Join(to="customers", relationship="many_to_one", on="{o}.cid = {c}.id")],
68
+ )
69
+ customers = Cube(
70
+ name="customers",
71
+ backend=Backend.POSTGRES,
72
+ table="customers",
73
+ alias="c",
74
+ dimensions=[Dimension(name="name", sql="{c}.name", type="string")],
75
+ )
76
+
77
+ print(render_dot(Catalog([orders, customers])))
78
+ ```
79
+
80
+ ## Quick start — PNG/SVG
81
+
82
+ ```python
83
+ from semql_erd import render_image
84
+
85
+ # Requires `pip install "semql-erd[image]"` AND the `dot` binary on PATH.
86
+ render_image(catalog, "catalog.png") # PNG by default
87
+ render_image(catalog, "catalog.svg", format="svg")
88
+ ```
89
+
90
+ ## Conventions
91
+
92
+ - **Nodes** are cubes. The label is a Graphviz record showing the cube
93
+ name (+ `display_name` suffix if set), the backend, and three field
94
+ sections (measures, dimensions, time-dimensions).
95
+ - **Edges** are `Join`s. Arrowhead shape encodes the relationship:
96
+ - `many_to_one` → `crow` on the from-side, `tee` on the to-side
97
+ - `one_to_many` → mirror of the above
98
+ - `one_to_one` → `tee` on both sides
99
+ - **Filtering** mirrors the planner prompt: by default only cubes with
100
+ `expose_in_prompt=True` (and non-META cubes) appear. Pass
101
+ `only_exposed=False` for a full graph.
102
+ - **Layout** defaults to `rankdir="LR"` (left-to-right). Pass
103
+ `rankdir="TB"` for top-to-bottom.
104
+
105
+ ## CLI
106
+
107
+ ```sh
108
+ python -m semql_erd path.to.module:catalog # prints DOT to stdout
109
+ python -m semql_erd path.to.module:catalog out.svg # writes a rendered image
110
+ ```
111
+
112
+ ## Status
113
+
114
+ Early development. The DOT format is stable; record-section ordering
115
+ and node ID naming may evolve.
@@ -0,0 +1,88 @@
1
+ # semql-erd
2
+
3
+ ER-diagram generator for [`semql`](../semql) catalogues. Walks the
4
+ cubes and joins in a `Catalog` and emits a [Graphviz](https://graphviz.org)
5
+ DOT source (and convenience PNG/SVG when the `graphviz` Python bindings
6
+ + system `dot` binary are available).
7
+
8
+ Useful when:
9
+ - The catalogue is past 10 cubes and reading the YAML/Python isn't
10
+ enough to see the join shape at a glance.
11
+ - A PR touches a `Join` and the reviewer wants a visual diff of the
12
+ before / after graph.
13
+ - Onboarding docs need a stable picture of what's in scope.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ pip install semql-erd # DOT source only — no system deps
19
+ pip install "semql-erd[image]" # + graphviz Python bindings
20
+ # (also needs the `dot` binary)
21
+ ```
22
+
23
+ ## Quick start — DOT source
24
+
25
+ `render_dot(catalog)` is dependency-free: it produces a DOT-language
26
+ string you can paste into any Graphviz renderer
27
+ ([Edotor](https://edotor.net) is a quick web one).
28
+
29
+ ```python
30
+ from semql import Backend, Catalog, Cube, Dimension, Join, Measure
31
+ from semql_erd import render_dot
32
+
33
+ orders = Cube(
34
+ name="orders",
35
+ backend=Backend.POSTGRES,
36
+ table="orders",
37
+ alias="o",
38
+ measures=[Measure(name="revenue", sql="{o}.amount", agg="sum", unit="currency")],
39
+ dimensions=[Dimension(name="region", sql="{o}.region", type="string")],
40
+ joins=[Join(to="customers", relationship="many_to_one", on="{o}.cid = {c}.id")],
41
+ )
42
+ customers = Cube(
43
+ name="customers",
44
+ backend=Backend.POSTGRES,
45
+ table="customers",
46
+ alias="c",
47
+ dimensions=[Dimension(name="name", sql="{c}.name", type="string")],
48
+ )
49
+
50
+ print(render_dot(Catalog([orders, customers])))
51
+ ```
52
+
53
+ ## Quick start — PNG/SVG
54
+
55
+ ```python
56
+ from semql_erd import render_image
57
+
58
+ # Requires `pip install "semql-erd[image]"` AND the `dot` binary on PATH.
59
+ render_image(catalog, "catalog.png") # PNG by default
60
+ render_image(catalog, "catalog.svg", format="svg")
61
+ ```
62
+
63
+ ## Conventions
64
+
65
+ - **Nodes** are cubes. The label is a Graphviz record showing the cube
66
+ name (+ `display_name` suffix if set), the backend, and three field
67
+ sections (measures, dimensions, time-dimensions).
68
+ - **Edges** are `Join`s. Arrowhead shape encodes the relationship:
69
+ - `many_to_one` → `crow` on the from-side, `tee` on the to-side
70
+ - `one_to_many` → mirror of the above
71
+ - `one_to_one` → `tee` on both sides
72
+ - **Filtering** mirrors the planner prompt: by default only cubes with
73
+ `expose_in_prompt=True` (and non-META cubes) appear. Pass
74
+ `only_exposed=False` for a full graph.
75
+ - **Layout** defaults to `rankdir="LR"` (left-to-right). Pass
76
+ `rankdir="TB"` for top-to-bottom.
77
+
78
+ ## CLI
79
+
80
+ ```sh
81
+ python -m semql_erd path.to.module:catalog # prints DOT to stdout
82
+ python -m semql_erd path.to.module:catalog out.svg # writes a rendered image
83
+ ```
84
+
85
+ ## Status
86
+
87
+ Early development. The DOT format is stable; record-section ordering
88
+ and node ID naming may evolve.
@@ -0,0 +1,45 @@
1
+ [project]
2
+ name = "semql-erd"
3
+ version = "0.1.0"
4
+ description = "Graphviz ER diagrams from a semql Catalog — cubes as nodes, joins as edges with crow's-foot arrowheads."
5
+ readme = "README.md"
6
+ license = "BSD-3-Clause"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "Nikhil Pallamreddy", email = "nikhil.pallamreddy+git@gmail.com" }
10
+ ]
11
+ requires-python = ">=3.12"
12
+ dependencies = [
13
+ "semql>=0.1.0,<0.2",
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Database",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ "Topic :: Multimedia :: Graphics",
25
+ "Typing :: Typed",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/npalladium/semql"
30
+ Repository = "https://github.com/npalladium/semql"
31
+ Issues = "https://github.com/npalladium/semql/issues"
32
+
33
+ [project.optional-dependencies]
34
+ # Required only for ``render_image`` (PNG/SVG output). ``render_dot``
35
+ # (the DOT-source path) has no third-party dependencies.
36
+ image = [
37
+ "graphviz>=0.20",
38
+ ]
39
+
40
+ [build-system]
41
+ requires = ["uv_build>=0.11.19,<0.12.0"]
42
+ build-backend = "uv_build"
43
+
44
+ [tool.uv.sources]
45
+ semql = { workspace = true, editable = true }
@@ -0,0 +1,14 @@
1
+ """Graphviz ER-diagram generator for semql Catalogs.
2
+
3
+ ``render_dot(catalog)`` returns a DOT-language string and has no
4
+ third-party dependencies. ``render_image(catalog, path)`` shells out
5
+ to Graphviz via the ``graphviz`` Python bindings — install with
6
+ ``pip install "semql-erd[image]"`` and a system ``dot`` binary.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from semql_erd.dot import render_dot
12
+ from semql_erd.image import render_image
13
+
14
+ __all__ = ["render_dot", "render_image"]
@@ -0,0 +1,60 @@
1
+ """CLI: ``python -m semql_erd <module:attr> [out_path]``.
2
+
3
+ ``module:attr`` is a Python import path to a ``Catalog`` instance.
4
+ With no output path, prints DOT to stdout. With an output path,
5
+ renders an image (format inferred from suffix; defaults to PNG).
6
+
7
+ Example:
8
+
9
+ python -m semql_erd my.catalog:CATALOG
10
+ python -m semql_erd my.catalog:CATALOG catalog.svg
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import importlib
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ from semql import Catalog
20
+
21
+ from semql_erd.dot import render_dot
22
+ from semql_erd.image import render_image
23
+
24
+
25
+ def _load_catalog(spec: str) -> Catalog:
26
+ if ":" not in spec:
27
+ raise SystemExit(
28
+ f"expected '<module>:<attr>', got {spec!r}. Example: 'my_project.catalog:CATALOG'"
29
+ )
30
+ module_name, attr = spec.split(":", 1)
31
+ module = importlib.import_module(module_name)
32
+ if not hasattr(module, attr):
33
+ raise SystemExit(f"module {module_name!r} has no attribute {attr!r}.")
34
+ obj = getattr(module, attr)
35
+ if not isinstance(obj, Catalog):
36
+ raise SystemExit(f"{spec} is not a Catalog instance (got {type(obj).__name__}).")
37
+ return obj
38
+
39
+
40
+ def main(argv: list[str] | None = None) -> int:
41
+ args = list(argv if argv is not None else sys.argv[1:])
42
+ if not args or args[0] in ("-h", "--help"):
43
+ print(__doc__)
44
+ return 0
45
+ spec = args[0]
46
+ catalog = _load_catalog(spec)
47
+
48
+ if len(args) == 1:
49
+ sys.stdout.write(render_dot(catalog))
50
+ return 0
51
+
52
+ out = Path(args[1])
53
+ fmt = out.suffix.lstrip(".") or "png"
54
+ rendered = render_image(catalog, out, format=fmt)
55
+ print(f"wrote {rendered}", file=sys.stderr)
56
+ return 0
57
+
58
+
59
+ if __name__ == "__main__": # pragma: no cover
60
+ raise SystemExit(main())
@@ -0,0 +1,152 @@
1
+ """DOT-source generation for catalogue ER diagrams.
2
+
3
+ Pure-Python — no third-party imports. ``render_dot(catalog)`` walks
4
+ the cubes and joins and produces a string consumable by any Graphviz
5
+ renderer.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Literal
11
+
12
+ from semql import Catalog
13
+ from semql.model import Cube
14
+
15
+ RankDir = Literal["LR", "TB", "RL", "BT"]
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Crow's-foot arrowhead conventions per relationship.
20
+ # ``arrowhead`` is the marker at the target end of the edge; ``arrowtail``
21
+ # is at the source end. We enable ``dir=both`` so both markers render.
22
+ # ---------------------------------------------------------------------------
23
+
24
+
25
+ def _relationship_attrs(relationship: str) -> dict[str, str]:
26
+ if relationship == "many_to_one":
27
+ return {"arrowtail": "crow", "arrowhead": "tee", "dir": "both"}
28
+ if relationship == "one_to_many":
29
+ return {"arrowtail": "tee", "arrowhead": "crow", "dir": "both"}
30
+ if relationship == "one_to_one":
31
+ return {"arrowtail": "tee", "arrowhead": "tee", "dir": "both"}
32
+ return {"arrowhead": "normal"}
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Escaping for DOT record-shape labels.
37
+ # Record labels use ``|`` as a section separator and ``{`` / ``}`` to group;
38
+ # ``<`` and ``>`` introduce port names. Escape those plus the obvious
39
+ # quote / backslash so a description with apostrophes or pipes doesn't
40
+ # break the layout.
41
+ # ---------------------------------------------------------------------------
42
+
43
+
44
+ _RECORD_ESCAPES = {
45
+ "\\": "\\\\",
46
+ '"': '\\"',
47
+ "|": "\\|",
48
+ "{": "\\{",
49
+ "}": "\\}",
50
+ "<": "\\<",
51
+ ">": "\\>",
52
+ "\n": "\\n",
53
+ }
54
+
55
+
56
+ def _escape_record(text: str) -> str:
57
+ return "".join(_RECORD_ESCAPES.get(ch, ch) for ch in text)
58
+
59
+
60
+ def _cube_label(cube: Cube) -> str:
61
+ """Build a DOT record-shape label string for ``cube``.
62
+
63
+ Layout: header (cube name + optional display_name + backend) on top,
64
+ measures / dimensions / time-dimensions stacked below, separated by
65
+ horizontal rules (``|`` between sections)."""
66
+ header_parts = [cube.name]
67
+ if cube.display_name:
68
+ header_parts.append(f"({cube.display_name})")
69
+ header_parts.append(f"\n[{cube.backend.value}]")
70
+ header = " ".join(header_parts)
71
+
72
+ sections: list[str] = [header]
73
+ if cube.measures:
74
+ ms = ", ".join(m.name for m in cube.measures)
75
+ sections.append(f"measures: {ms}")
76
+ if cube.dimensions:
77
+ ds = ", ".join(d.name for d in cube.dimensions)
78
+ sections.append(f"dimensions: {ds}")
79
+ if cube.time_dimensions:
80
+ ts = ", ".join(td.name for td in cube.time_dimensions)
81
+ sections.append(f"time: {ts}")
82
+
83
+ return "{" + "|".join(_escape_record(s) for s in sections) + "}"
84
+
85
+
86
+ def _node_id(cube: Cube) -> str:
87
+ """A safe DOT node identifier — cube names are already restricted
88
+ to ``[a-z_][a-z0-9_]*`` by the resolver regex, so they need no
89
+ further escaping."""
90
+ return cube.name
91
+
92
+
93
+ def _cubes_in_scope(catalog: Catalog, *, only_exposed: bool) -> list[Cube]:
94
+ """Filter the catalogue for rendering. META reflection cubes are
95
+ always excluded — they're an introspection mechanism, not part of
96
+ the data model. ``only_exposed=True`` (default) also drops cubes
97
+ flagged ``expose_in_prompt=False`` so the diagram matches what the
98
+ planner sees."""
99
+ from semql import iter_cubes
100
+
101
+ return list(iter_cubes(catalog, only_exposed=only_exposed))
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Entry point
106
+ # ---------------------------------------------------------------------------
107
+
108
+
109
+ def render_dot(
110
+ catalog: Catalog,
111
+ *,
112
+ only_exposed: bool = True,
113
+ rankdir: RankDir = "LR",
114
+ title: str | None = None,
115
+ ) -> str:
116
+ """Render the catalogue as a Graphviz DOT source string.
117
+
118
+ ``only_exposed`` (default ``True``) mirrors the planner-prompt
119
+ filter — only cubes flagged ``expose_in_prompt=True`` appear.
120
+ ``rankdir`` controls layout direction (LR/TB/RL/BT).
121
+ ``title`` is an optional graph label rendered at the top.
122
+ """
123
+ cubes = _cubes_in_scope(catalog, only_exposed=only_exposed)
124
+ in_scope: set[str] = {c.name for c in cubes}
125
+
126
+ lines: list[str] = ["digraph catalog {"]
127
+ lines.append(f' rankdir="{rankdir}";')
128
+ lines.append(' node [shape=record, fontname="Helvetica", fontsize=10];')
129
+ lines.append(' edge [fontname="Helvetica", fontsize=9];')
130
+ if title:
131
+ lines.append(f' label="{_escape_record(title)}";')
132
+ lines.append(" labelloc=t;")
133
+ lines.append("")
134
+
135
+ for cube in cubes:
136
+ lines.append(f' {_node_id(cube)} [label="{_cube_label(cube)}"];')
137
+
138
+ lines.append("")
139
+ for cube in cubes:
140
+ for join in cube.joins:
141
+ if join.to not in in_scope:
142
+ # Skip edges that would dangle into filtered-out cubes.
143
+ continue
144
+ attrs = _relationship_attrs(join.relationship)
145
+ attr_str = ", ".join(f'{k}="{v}"' for k, v in attrs.items())
146
+ lines.append(f" {_node_id(cube)} -> {join.to} [{attr_str}];")
147
+
148
+ lines.append("}")
149
+ return "\n".join(lines) + "\n"
150
+
151
+
152
+ __all__ = ["RankDir", "render_dot"]
@@ -0,0 +1,57 @@
1
+ # pyright: reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false
2
+ # ``graphviz`` ships no type stubs; pyright reports every method
3
+ # return as Unknown. The functions wrap it tightly enough that local
4
+ # inference covers the actual contract.
5
+ """PNG/SVG rendering for catalogue ER diagrams.
6
+
7
+ Shells out to Graphviz via the optional ``graphviz`` Python bindings.
8
+ Install with ``pip install "semql-erd[image]"`` and a system ``dot``
9
+ binary on PATH. The pure-Python ``render_dot`` path stays usable
10
+ without these.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+
17
+ from semql import Catalog
18
+
19
+ from semql_erd.dot import RankDir, render_dot
20
+
21
+
22
+ def render_image(
23
+ catalog: Catalog,
24
+ path: str | Path,
25
+ *,
26
+ format: str = "png",
27
+ only_exposed: bool = True,
28
+ rankdir: RankDir = "LR",
29
+ title: str | None = None,
30
+ ) -> Path:
31
+ """Render the catalogue as a PNG / SVG / PDF image at ``path``.
32
+
33
+ Calls the system ``dot`` binary via the ``graphviz`` Python
34
+ bindings. Raises ``ImportError`` if the optional ``image`` extra
35
+ isn't installed, and ``graphviz.ExecutableNotFound`` (re-raised
36
+ from the bindings) if the ``dot`` binary isn't on PATH.
37
+ """
38
+ try:
39
+ from graphviz import Source # type: ignore[import-not-found]
40
+ except ImportError as exc: # pragma: no cover
41
+ raise ImportError(
42
+ "render_image requires the ``image`` extra. Install with "
43
+ "``pip install 'semql-erd[image]'``."
44
+ ) from exc
45
+
46
+ dot_source = render_dot(catalog, only_exposed=only_exposed, rankdir=rankdir, title=title)
47
+ target = Path(path)
48
+ # ``graphviz.Source`` writes ``<filename>.<format>``; we want the
49
+ # exact path the caller asked for, so strip the suffix and pass it
50
+ # as ``filename`` with ``format=`` matching the requested type.
51
+ filename = target.with_suffix("")
52
+ src = Source(dot_source, format=format)
53
+ rendered = src.render(filename=str(filename), cleanup=True)
54
+ return Path(rendered)
55
+
56
+
57
+ __all__ = ["render_image"]
File without changes