prisma-flow 0.1.1__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.
- prisma_flow-0.1.1.dist-info/METADATA +236 -0
- prisma_flow-0.1.1.dist-info/RECORD +28 -0
- prisma_flow-0.1.1.dist-info/WHEEL +4 -0
- prisma_flow-0.1.1.dist-info/entry_points.txt +3 -0
- prisma_flow-0.1.1.dist-info/licenses/LICENSE +28 -0
- prismaflow/__init__.py +40 -0
- prismaflow/cli.py +208 -0
- prismaflow/enums.py +20 -0
- prismaflow/exceptions.py +27 -0
- prismaflow/io/__init__.py +8 -0
- prismaflow/io/json.py +64 -0
- prismaflow/io/yaml.py +127 -0
- prismaflow/layout/__init__.py +23 -0
- prismaflow/layout/engine.py +162 -0
- prismaflow/layout/geometry.py +152 -0
- prismaflow/layout/overlap.py +51 -0
- prismaflow/layout/text.py +36 -0
- prismaflow/models.py +499 -0
- prismaflow/py.typed +0 -0
- prismaflow/renderers/__init__.py +10 -0
- prismaflow/renderers/html.py +61 -0
- prismaflow/renderers/mermaid.py +69 -0
- prismaflow/renderers/png.py +38 -0
- prismaflow/renderers/svg.py +256 -0
- prismaflow/templates/__init__.py +8 -0
- prismaflow/templates/base.py +28 -0
- prismaflow/templates/prisma_2020_new.py +207 -0
- prismaflow/validation.py +297 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prisma-flow
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Lightweight Python tools for generating PRISMA-style flow diagrams without system dependencies.
|
|
5
|
+
License-Expression: BSD-3-Clause
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: prisma,systematic-review,scoping-review,evidence-synthesis,literature-review,svg,python,open-science
|
|
8
|
+
Author: Open Science Labs Incubator contributors
|
|
9
|
+
Requires-Python: >=3.10,<4
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Provides-Extra: docs
|
|
22
|
+
Provides-Extra: png
|
|
23
|
+
Provides-Extra: yaml
|
|
24
|
+
Requires-Dist: douki (>=0.12.1) ; extra == "dev"
|
|
25
|
+
Requires-Dist: makim (==1.29.0) ; extra == "dev"
|
|
26
|
+
Requires-Dist: mypy (>=1.10) ; extra == "dev"
|
|
27
|
+
Requires-Dist: pre-commit (>=3) ; extra == "dev"
|
|
28
|
+
Requires-Dist: pydantic (>=2)
|
|
29
|
+
Requires-Dist: pytest (>=8) ; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov (>=5) ; extra == "dev"
|
|
31
|
+
Requires-Dist: pyyaml (>=6) ; extra == "yaml"
|
|
32
|
+
Requires-Dist: resvg (>=0.1.2) ; extra == "png"
|
|
33
|
+
Requires-Dist: ruff (>=0.6) ; extra == "dev"
|
|
34
|
+
Project-URL: Documentation, https://osl-incubator.github.io/prisma-flow/
|
|
35
|
+
Project-URL: Homepage, https://github.com/osl-incubator/prisma-flow
|
|
36
|
+
Project-URL: Issues, https://github.com/osl-incubator/prisma-flow/issues
|
|
37
|
+
Project-URL: Repository, https://github.com/osl-incubator/prisma-flow
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# prisma-flow
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+
[](https://pypi.org/project/prisma-flow/)
|
|
44
|
+
[](https://pypi.org/project/prisma-flow/)
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
`prisma-flow` is a lightweight Python package for generating PRISMA-style flow
|
|
48
|
+
diagrams for evidence synthesis workflows.
|
|
49
|
+
|
|
50
|
+
Unlike Graphviz-based tools, `prisma-flow` does not require system-level graph
|
|
51
|
+
layout binaries. Unlike Mermaid-based tools, it does not require Node or Mermaid
|
|
52
|
+
CLI. The default renderer is a pure-Python, template-based SVG generator.
|
|
53
|
+
|
|
54
|
+
The project is designed for systematic reviews, scoping reviews, evidence
|
|
55
|
+
syntheses, and literature review workflows.
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- Pure-Python SVG rendering by default
|
|
60
|
+
- Standalone HTML export
|
|
61
|
+
- Mermaid text export without Mermaid CLI
|
|
62
|
+
- JSON input/output in the base install
|
|
63
|
+
- Optional YAML input/output via `prisma-flow[yaml]`
|
|
64
|
+
- Optional PNG method that clearly reports the missing optional dependency
|
|
65
|
+
- Python API and `prisma-flow` command-line interface
|
|
66
|
+
- PRISMA count validation with errors and warnings
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install prisma-flow
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
or:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uv add prisma-flow
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Optional YAML support:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
uv add "prisma-flow[yaml]"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Optional PNG support, when a supported backend is added:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv add "prisma-flow[png]"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Python API
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from prismaflow import PrismaFlow
|
|
96
|
+
|
|
97
|
+
flow = PrismaFlow.new_review(
|
|
98
|
+
records_identified_databases=1240,
|
|
99
|
+
records_identified_registers=50,
|
|
100
|
+
records_removed_duplicates=210,
|
|
101
|
+
records_removed_automation=0,
|
|
102
|
+
records_removed_other=0,
|
|
103
|
+
records_screened=1080,
|
|
104
|
+
records_excluded=950,
|
|
105
|
+
reports_sought=130,
|
|
106
|
+
reports_not_retrieved=10,
|
|
107
|
+
reports_assessed=120,
|
|
108
|
+
reports_excluded={
|
|
109
|
+
"Wrong population": 30,
|
|
110
|
+
"Wrong intervention": 20,
|
|
111
|
+
"Wrong outcome": 15,
|
|
112
|
+
"Not primary research": 15,
|
|
113
|
+
},
|
|
114
|
+
studies_included=40,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
report = flow.validate()
|
|
118
|
+
print(report.format_text())
|
|
119
|
+
|
|
120
|
+
flow.to_svg("prisma.svg")
|
|
121
|
+
flow.to_html("prisma.html")
|
|
122
|
+
flow.to_mermaid("prisma.mmd")
|
|
123
|
+
flow.to_json("review.json")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## CLI usage
|
|
127
|
+
|
|
128
|
+
Validate input data:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
prisma-flow validate examples/basic_new_review.json
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Render SVG:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
prisma-flow render examples/basic_new_review.json -o prisma.svg
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Render other base-install formats:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
prisma-flow render examples/basic_new_review.json --format html -o prisma.html
|
|
144
|
+
prisma-flow render examples/basic_new_review.json --format mermaid -o prisma.mmd
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
If validation fails, the CLI prints a report and exits with a non-zero status:
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
Validation failed:
|
|
151
|
+
- records_screened should equal identified records minus removed records. Expected: 1080 Found: 1090
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Data model
|
|
155
|
+
|
|
156
|
+
The v0.1 implementation supports the PRISMA 2020 new-review databases/registers
|
|
157
|
+
template:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from prismaflow import (
|
|
161
|
+
EligibilityStage,
|
|
162
|
+
IdentificationStage,
|
|
163
|
+
IncludedStage,
|
|
164
|
+
PrismaFlow,
|
|
165
|
+
PrismaTemplate,
|
|
166
|
+
ScreeningStage,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
flow = PrismaFlow(
|
|
170
|
+
template=PrismaTemplate.PRISMA_2020_NEW_DATABASES_REGISTERS,
|
|
171
|
+
identification=IdentificationStage(
|
|
172
|
+
records_identified_databases=1240,
|
|
173
|
+
records_identified_registers=50,
|
|
174
|
+
),
|
|
175
|
+
screening=ScreeningStage(
|
|
176
|
+
records_removed_duplicates=210,
|
|
177
|
+
records_removed_automation=0,
|
|
178
|
+
records_removed_other=0,
|
|
179
|
+
records_screened=1080,
|
|
180
|
+
records_excluded=950,
|
|
181
|
+
),
|
|
182
|
+
eligibility=EligibilityStage(
|
|
183
|
+
reports_sought=130,
|
|
184
|
+
reports_not_retrieved=10,
|
|
185
|
+
reports_assessed=120,
|
|
186
|
+
reports_excluded={"Wrong population": 30},
|
|
187
|
+
),
|
|
188
|
+
included=IncludedStage(studies_included=40),
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Dependency policy
|
|
193
|
+
|
|
194
|
+
SVG, HTML, Mermaid, and JSON work with the base install. YAML is optional. PNG
|
|
195
|
+
is intentionally optional and not implemented as a required renderer in v0.1.
|
|
196
|
+
|
|
197
|
+
The package does **not** require Graphviz, Cairo, CairoSVG, Node, Mermaid CLI,
|
|
198
|
+
Inkscape, Playwright, browser engines, Matplotlib, or Plotly.
|
|
199
|
+
|
|
200
|
+
## Development
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
conda env create -f conda/dev.yaml
|
|
204
|
+
conda activate prismaflow
|
|
205
|
+
poetry config virtualenvs.create false
|
|
206
|
+
poetry install --extras "dev yaml"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Run the same workflow through Makim:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
makim tests.linter
|
|
213
|
+
makim tests.unit
|
|
214
|
+
makim package.build
|
|
215
|
+
makim docs.build
|
|
216
|
+
makim all.ci
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Documentation
|
|
220
|
+
|
|
221
|
+
The documentation site is built with Quarto:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
quarto render docs
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Preview locally:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
quarto preview docs
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
BSD-3-Clause.
|
|
236
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
prismaflow/__init__.py,sha256=899hbwyttDc5loJRvFGwzphxCKlmQMcOPN-TZdO4C2E,912
|
|
2
|
+
prismaflow/cli.py,sha256=gQqb42DbN7nr-SAKVvAzW36wxgfB_2fVvUILmFgHv84,5778
|
|
3
|
+
prismaflow/enums.py,sha256=OVrVv0viAyCe0yTw6c9HULmR1LeNASrsKD3Qzzyj27E,575
|
|
4
|
+
prismaflow/exceptions.py,sha256=ErfXBgK7UXuzkz1Hfy0ckHhjLRH0RZNDsOMKiAQqYVs,541
|
|
5
|
+
prismaflow/io/__init__.py,sha256=dEy96FtJNwnAp5sUw-NMBVXEKD84PaRRwxSDXMqBwRE,206
|
|
6
|
+
prismaflow/io/json.py,sha256=A5yNY7FEXtCgRjehL7M7oOp7KJD3-dDcujWxwKW4lsc,1632
|
|
7
|
+
prismaflow/io/yaml.py,sha256=QpK-uYr-AkjClW44EmfDBJzNrBlnOk1lShhc0waPn_s,3244
|
|
8
|
+
prismaflow/layout/__init__.py,sha256=bpd8kq8lK3-4412XWb0R0Cr7BatyAY-lXpEpeg_xB1A,448
|
|
9
|
+
prismaflow/layout/engine.py,sha256=T933eQCp4ugiIjFJNZtOpaYwwM_El0XR-ZlJqNmNB7Y,4026
|
|
10
|
+
prismaflow/layout/geometry.py,sha256=igOLToIzG3aIj8Y48mn93tP5s4UcL_wueDwGYnPaJnM,3529
|
|
11
|
+
prismaflow/layout/overlap.py,sha256=DFX6F2uUmEAp-VCjMffQI3s7Ynhda1jDiaFBb_tEGo4,1455
|
|
12
|
+
prismaflow/layout/text.py,sha256=oIJ1WrWyBMKIrv0E2vQ1vA4gyrlVJ3EaUqklQLBOMPo,849
|
|
13
|
+
prismaflow/models.py,sha256=rOggTlHZuvJo58JBQrZd-5wruLl-c7f0Y8-PXVivKf4,14839
|
|
14
|
+
prismaflow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
prismaflow/renderers/__init__.py,sha256=ssDxOwN30HbAwGo0iq6TnNMUQDQPFZCzrrmIVYUOqYo,317
|
|
16
|
+
prismaflow/renderers/html.py,sha256=CUjYycp_5KdZsosQJSgFYTR6x9kvidhjZko3PK2nWlc,1391
|
|
17
|
+
prismaflow/renderers/mermaid.py,sha256=s8A3fO_W4DMAsTvIFnvZzZYHACuxGTE7Zbj1qAIiKkQ,1949
|
|
18
|
+
prismaflow/renderers/png.py,sha256=kLLU4P_AuLIIRT-Ehgbx8SS5whytQn1NnML6L6HhYA8,1052
|
|
19
|
+
prismaflow/renderers/svg.py,sha256=msM8Mqlu1s3L_EpyMb6TraJpQxeRfMfCKQJBPI59mJk,7927
|
|
20
|
+
prismaflow/templates/__init__.py,sha256=RLHizwx71HVJWPDrY0m-LOVdBFO9KBJhjnHzarfhhhE,223
|
|
21
|
+
prismaflow/templates/base.py,sha256=L5INco9lgdbEd6STwbifM-tpZrl_2ITeXisOXvHI4ZE,598
|
|
22
|
+
prismaflow/templates/prisma_2020_new.py,sha256=kkdeL2mJg7fsnwTzPUJUijfcDE65oPRM0vJwN6sHfUg,7218
|
|
23
|
+
prismaflow/validation.py,sha256=f8qKNUqUQg3hvHNLWRDH8gBhOlombseE2fLMAw45mRQ,8225
|
|
24
|
+
prisma_flow-0.1.1.dist-info/METADATA,sha256=bUTJn4d0oppiMzkfOkEQ8ADd_AKlxWAYCKfupdKYWGo,6397
|
|
25
|
+
prisma_flow-0.1.1.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
|
|
26
|
+
prisma_flow-0.1.1.dist-info/entry_points.txt,sha256=TeiZqSlnbxszvNXeJ2BWhMAgTNSu0OvpUKdIwzrvcqw,51
|
|
27
|
+
prisma_flow-0.1.1.dist-info/licenses/LICENSE,sha256=vY8jl7ZbJTe7b45QSxc7JAy_FC7eZnnOFI1xz70Ab68,1514
|
|
28
|
+
prisma_flow-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Open Science Labs Incubator
|
|
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.
|
prismaflow/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
title: Lightweight PRISMA-style flow diagram generation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from prismaflow.enums import PrismaTemplate
|
|
6
|
+
from prismaflow.exceptions import (
|
|
7
|
+
OptionalDependencyError,
|
|
8
|
+
PrismaFlowError,
|
|
9
|
+
PrismaValidationError,
|
|
10
|
+
TemplateNotSupportedError,
|
|
11
|
+
)
|
|
12
|
+
from prismaflow.models import (
|
|
13
|
+
EligibilityStage,
|
|
14
|
+
FlowMetadata,
|
|
15
|
+
IdentificationStage,
|
|
16
|
+
IncludedStage,
|
|
17
|
+
PrismaFlow,
|
|
18
|
+
ScreeningStage,
|
|
19
|
+
)
|
|
20
|
+
from prismaflow.validation import ValidationMessage, ValidationReport, validate_flow
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.1" # semantic-release
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"EligibilityStage",
|
|
26
|
+
"FlowMetadata",
|
|
27
|
+
"IdentificationStage",
|
|
28
|
+
"IncludedStage",
|
|
29
|
+
"OptionalDependencyError",
|
|
30
|
+
"PrismaFlow",
|
|
31
|
+
"PrismaFlowError",
|
|
32
|
+
"PrismaTemplate",
|
|
33
|
+
"PrismaValidationError",
|
|
34
|
+
"ScreeningStage",
|
|
35
|
+
"TemplateNotSupportedError",
|
|
36
|
+
"ValidationMessage",
|
|
37
|
+
"ValidationReport",
|
|
38
|
+
"__version__",
|
|
39
|
+
"validate_flow",
|
|
40
|
+
]
|
prismaflow/cli.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
title: Command-line interface for prisma-flow.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
13
|
+
|
|
14
|
+
from prismaflow.exceptions import OptionalDependencyError, PrismaFlowError
|
|
15
|
+
from prismaflow.models import PrismaFlow
|
|
16
|
+
|
|
17
|
+
RenderFormat = str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
21
|
+
"""
|
|
22
|
+
title: Run the prisma-flow command-line interface.
|
|
23
|
+
parameters:
|
|
24
|
+
argv:
|
|
25
|
+
type: Sequence[str] | None
|
|
26
|
+
description: Value for argv.
|
|
27
|
+
returns:
|
|
28
|
+
type: int
|
|
29
|
+
description: Return value.
|
|
30
|
+
"""
|
|
31
|
+
parser = build_parser()
|
|
32
|
+
args = parser.parse_args(argv)
|
|
33
|
+
try:
|
|
34
|
+
if args.command == "validate":
|
|
35
|
+
return _validate_command(args)
|
|
36
|
+
if args.command == "render":
|
|
37
|
+
return _render_command(args)
|
|
38
|
+
except PydanticValidationError as exc:
|
|
39
|
+
print("Input model validation failed:", file=sys.stderr)
|
|
40
|
+
print(str(exc), file=sys.stderr)
|
|
41
|
+
return 2
|
|
42
|
+
except OptionalDependencyError as exc:
|
|
43
|
+
print(str(exc), file=sys.stderr)
|
|
44
|
+
return 2
|
|
45
|
+
except PrismaFlowError as exc:
|
|
46
|
+
print(str(exc), file=sys.stderr)
|
|
47
|
+
return 2
|
|
48
|
+
parser.print_help(sys.stderr)
|
|
49
|
+
return 2
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
53
|
+
"""
|
|
54
|
+
title: Build the CLI argument parser.
|
|
55
|
+
returns:
|
|
56
|
+
type: argparse.ArgumentParser
|
|
57
|
+
description: Return value.
|
|
58
|
+
"""
|
|
59
|
+
parser = argparse.ArgumentParser(
|
|
60
|
+
prog="prisma-flow",
|
|
61
|
+
description="Generate PRISMA-style flow diagrams without system dependencies.",
|
|
62
|
+
)
|
|
63
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
64
|
+
|
|
65
|
+
validate = subparsers.add_parser("validate", help="validate a PRISMA flow file")
|
|
66
|
+
validate.add_argument("input", help="input JSON/YAML file")
|
|
67
|
+
validate.add_argument(
|
|
68
|
+
"--strict-included",
|
|
69
|
+
action="store_true",
|
|
70
|
+
help="treat included-study reconciliation warnings as errors",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
render = subparsers.add_parser("render", help="render a PRISMA flow file")
|
|
74
|
+
render.add_argument("input", help="input JSON/YAML file")
|
|
75
|
+
render.add_argument(
|
|
76
|
+
"-f",
|
|
77
|
+
"--format",
|
|
78
|
+
choices=["svg", "html", "mermaid", "png", "json", "yaml"],
|
|
79
|
+
help="output format; inferred from --output when omitted",
|
|
80
|
+
)
|
|
81
|
+
render.add_argument("-o", "--output", help="output path")
|
|
82
|
+
render.add_argument(
|
|
83
|
+
"--allow-invalid",
|
|
84
|
+
action="store_true",
|
|
85
|
+
help="render even when PRISMA count validation has errors",
|
|
86
|
+
)
|
|
87
|
+
return parser
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _validate_command(args: argparse.Namespace) -> int:
|
|
91
|
+
"""
|
|
92
|
+
title: _validate_command.
|
|
93
|
+
parameters:
|
|
94
|
+
args:
|
|
95
|
+
type: argparse.Namespace
|
|
96
|
+
description: Value for args.
|
|
97
|
+
returns:
|
|
98
|
+
type: int
|
|
99
|
+
description: Return value.
|
|
100
|
+
"""
|
|
101
|
+
flow = _load_flow(args.input)
|
|
102
|
+
report = flow.validate(strict_included=args.strict_included)
|
|
103
|
+
print(report.format_text())
|
|
104
|
+
return 0 if report.ok else 1
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _render_command(args: argparse.Namespace) -> int:
|
|
108
|
+
"""
|
|
109
|
+
title: _render_command.
|
|
110
|
+
parameters:
|
|
111
|
+
args:
|
|
112
|
+
type: argparse.Namespace
|
|
113
|
+
description: Value for args.
|
|
114
|
+
returns:
|
|
115
|
+
type: int
|
|
116
|
+
description: Return value.
|
|
117
|
+
"""
|
|
118
|
+
flow = _load_flow(args.input)
|
|
119
|
+
report = flow.validate()
|
|
120
|
+
if report.has_errors and not args.allow_invalid:
|
|
121
|
+
print(report.format_text(), file=sys.stderr)
|
|
122
|
+
return 1
|
|
123
|
+
if report.has_warnings:
|
|
124
|
+
print(report.format_text(), file=sys.stderr)
|
|
125
|
+
|
|
126
|
+
output_format = args.format or _infer_format(args.output)
|
|
127
|
+
rendered = _render(flow, output_format, args.output)
|
|
128
|
+
if args.output is None and isinstance(rendered, str):
|
|
129
|
+
print(rendered, end="")
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _load_flow(path: str | Path) -> PrismaFlow:
|
|
134
|
+
"""
|
|
135
|
+
title: _load_flow.
|
|
136
|
+
parameters:
|
|
137
|
+
path:
|
|
138
|
+
type: str | Path
|
|
139
|
+
description: Value for path.
|
|
140
|
+
returns:
|
|
141
|
+
type: PrismaFlow
|
|
142
|
+
description: Return value.
|
|
143
|
+
"""
|
|
144
|
+
suffix = Path(path).suffix.lower()
|
|
145
|
+
if suffix in {".yaml", ".yml"}:
|
|
146
|
+
return PrismaFlow.from_yaml(path)
|
|
147
|
+
return PrismaFlow.from_json(path)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _infer_format(output: str | None) -> RenderFormat:
|
|
151
|
+
"""
|
|
152
|
+
title: _infer_format.
|
|
153
|
+
parameters:
|
|
154
|
+
output:
|
|
155
|
+
type: str | None
|
|
156
|
+
description: Value for output.
|
|
157
|
+
returns:
|
|
158
|
+
type: RenderFormat
|
|
159
|
+
description: Return value.
|
|
160
|
+
"""
|
|
161
|
+
if not output:
|
|
162
|
+
return "svg"
|
|
163
|
+
suffix = Path(output).suffix.lower().lstrip(".")
|
|
164
|
+
if suffix == "mmd":
|
|
165
|
+
return "mermaid"
|
|
166
|
+
if suffix in {"svg", "html", "png", "json", "yaml", "yml"}:
|
|
167
|
+
return "yaml" if suffix == "yml" else suffix
|
|
168
|
+
return "svg"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _render(
|
|
172
|
+
flow: PrismaFlow,
|
|
173
|
+
output_format: RenderFormat,
|
|
174
|
+
output: str | None,
|
|
175
|
+
) -> str | bytes:
|
|
176
|
+
"""
|
|
177
|
+
title: _render.
|
|
178
|
+
parameters:
|
|
179
|
+
flow:
|
|
180
|
+
type: PrismaFlow
|
|
181
|
+
description: Value for flow.
|
|
182
|
+
output_format:
|
|
183
|
+
type: RenderFormat
|
|
184
|
+
description: Value for output_format.
|
|
185
|
+
output:
|
|
186
|
+
type: str | None
|
|
187
|
+
description: Value for output.
|
|
188
|
+
returns:
|
|
189
|
+
type: str | bytes
|
|
190
|
+
description: Return value.
|
|
191
|
+
"""
|
|
192
|
+
if output_format == "svg":
|
|
193
|
+
return flow.to_svg(output)
|
|
194
|
+
if output_format == "html":
|
|
195
|
+
return flow.to_html(output)
|
|
196
|
+
if output_format == "mermaid":
|
|
197
|
+
return flow.to_mermaid(output)
|
|
198
|
+
if output_format == "png":
|
|
199
|
+
return flow.to_png(output)
|
|
200
|
+
if output_format == "json":
|
|
201
|
+
return flow.to_json(output)
|
|
202
|
+
if output_format == "yaml":
|
|
203
|
+
return flow.to_yaml(output)
|
|
204
|
+
raise PrismaFlowError(f"Unsupported output format: {output_format}")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == "__main__": # pragma: no cover
|
|
208
|
+
raise SystemExit(main())
|
prismaflow/enums.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
title: Enumerations used by prisma-flow.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PrismaTemplate(str, Enum):
|
|
9
|
+
"""
|
|
10
|
+
title: Supported and planned PRISMA diagram templates.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
PRISMA_2020_NEW_DATABASES_REGISTERS = "prisma_2020_new_databases_registers"
|
|
14
|
+
PRISMA_2020_NEW_DATABASES_REGISTERS_OTHER = (
|
|
15
|
+
"prisma_2020_new_databases_registers_other"
|
|
16
|
+
)
|
|
17
|
+
PRISMA_2020_UPDATED_DATABASES_REGISTERS = "prisma_2020_updated_databases_registers"
|
|
18
|
+
PRISMA_2020_UPDATED_DATABASES_REGISTERS_OTHER = (
|
|
19
|
+
"prisma_2020_updated_databases_registers_other"
|
|
20
|
+
)
|
prismaflow/exceptions.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
title: Exceptions raised by prisma-flow.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PrismaFlowError(Exception):
|
|
7
|
+
"""
|
|
8
|
+
title: Base exception for prisma-flow errors.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TemplateNotSupportedError(PrismaFlowError):
|
|
13
|
+
"""
|
|
14
|
+
title: Raised when a requested PRISMA template is not implemented.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OptionalDependencyError(PrismaFlowError):
|
|
19
|
+
"""
|
|
20
|
+
title: Raised when an optional export backend is not installed.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PrismaValidationError(PrismaFlowError):
|
|
25
|
+
"""
|
|
26
|
+
title: Raised when a flow has validation errors.
|
|
27
|
+
"""
|
prismaflow/io/json.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
title: JSON input/output helpers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from prismaflow.models import PrismaFlow
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_json(source: str | Path) -> PrismaFlow:
|
|
13
|
+
"""
|
|
14
|
+
title: Load a PrismaFlow model from a JSON file path or JSON string.
|
|
15
|
+
parameters:
|
|
16
|
+
source:
|
|
17
|
+
type: str | Path
|
|
18
|
+
description: Value for source.
|
|
19
|
+
returns:
|
|
20
|
+
type: PrismaFlow
|
|
21
|
+
description: Return value.
|
|
22
|
+
"""
|
|
23
|
+
if isinstance(source, Path) or _looks_like_path(source):
|
|
24
|
+
path = Path(source)
|
|
25
|
+
if path.exists():
|
|
26
|
+
return PrismaFlow.model_validate_json(path.read_text(encoding="utf-8"))
|
|
27
|
+
return PrismaFlow.model_validate_json(str(source))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def dump_json(flow: PrismaFlow, path: str | Path | None = None) -> str:
|
|
31
|
+
"""
|
|
32
|
+
title: Serialize a flow to JSON and optionally write it.
|
|
33
|
+
parameters:
|
|
34
|
+
flow:
|
|
35
|
+
type: PrismaFlow
|
|
36
|
+
description: Value for flow.
|
|
37
|
+
path:
|
|
38
|
+
type: str | Path | None
|
|
39
|
+
description: Value for path.
|
|
40
|
+
returns:
|
|
41
|
+
type: str
|
|
42
|
+
description: Return value.
|
|
43
|
+
"""
|
|
44
|
+
output = flow.model_dump_json(indent=2)
|
|
45
|
+
if path is not None:
|
|
46
|
+
Path(path).write_text(output + "\n", encoding="utf-8")
|
|
47
|
+
return output
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _looks_like_path(source: str | Path) -> bool:
|
|
51
|
+
"""
|
|
52
|
+
title: _looks_like_path.
|
|
53
|
+
parameters:
|
|
54
|
+
source:
|
|
55
|
+
type: str | Path
|
|
56
|
+
description: Value for source.
|
|
57
|
+
returns:
|
|
58
|
+
type: bool
|
|
59
|
+
description: Return value.
|
|
60
|
+
"""
|
|
61
|
+
if isinstance(source, Path):
|
|
62
|
+
return True
|
|
63
|
+
text = str(source)
|
|
64
|
+
return text.endswith(".json") or "/" in text or "\\" in text
|