semql-erd 0.1.0__py3-none-any.whl
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.
- semql_erd/__init__.py +14 -0
- semql_erd/__main__.py +60 -0
- semql_erd/dot.py +152 -0
- semql_erd/image.py +57 -0
- semql_erd/py.typed +0 -0
- semql_erd-0.1.0.dist-info/METADATA +115 -0
- semql_erd-0.1.0.dist-info/RECORD +9 -0
- semql_erd-0.1.0.dist-info/WHEEL +4 -0
- semql_erd-0.1.0.dist-info/licenses/LICENSE +28 -0
semql_erd/__init__.py
ADDED
|
@@ -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"]
|
semql_erd/__main__.py
ADDED
|
@@ -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())
|
semql_erd/dot.py
ADDED
|
@@ -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"]
|
semql_erd/image.py
ADDED
|
@@ -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"]
|
semql_erd/py.typed
ADDED
|
File without changes
|
|
@@ -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,9 @@
|
|
|
1
|
+
semql_erd/__init__.py,sha256=TQl2IeyRqJzuRbGFRwANgmjw2E3gQHfzP9BbF3ap2qc,479
|
|
2
|
+
semql_erd/__main__.py,sha256=UiZSo6ZlMfnOTRpkbEivkQNr8dxcwKrojxIuHk5cjUg,1740
|
|
3
|
+
semql_erd/dot.py,sha256=UGCFbbN7UAAq6yeWvvdUBoGS2h4KDwWBQcDOZ_2Imo4,5256
|
|
4
|
+
semql_erd/image.py,sha256=3zvxsh8w0Nce0CxeYMWvfFlaLzMgnzH020f7Jhtw8E8,2050
|
|
5
|
+
semql_erd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
semql_erd-0.1.0.dist-info/licenses/LICENSE,sha256=AdcAzanKVr3cVSrhBpG6gytjG0Ss1SBTQDAavLe0CRc,1505
|
|
7
|
+
semql_erd-0.1.0.dist-info/WHEEL,sha256=wXwAVsgVaOZ_pwDFqQm5Rd6PID-Fc74nkLc8X8gHiDo,81
|
|
8
|
+
semql_erd-0.1.0.dist-info/METADATA,sha256=j96jOictCPt0KR21ixhIdNA8KpUyZeVGSd_v8CJmrpk,4032
|
|
9
|
+
semql_erd-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|