fastapi-voyager 0.13.0__tar.gz → 0.13.2__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.
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/PKG-INFO +64 -27
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/README.md +63 -26
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/docs/changelog.md +7 -1
- fastapi_voyager-0.13.2/src/fastapi_voyager/er_diagram.py +279 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/type.py +2 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/version.py +1 -1
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/service/schema/schema.py +5 -2
- fastapi_voyager-0.13.0/src/fastapi_voyager/er_diagram.py +0 -119
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/.github/workflows/publish.yml +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/.gitignore +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/.python-version +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/CONTRIBUTING.md +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/LICENSE +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/docs/idea.md +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/pyproject.toml +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/release.md +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/__init__.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/cli.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/filter.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/module.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/render.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/server.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/type_helper.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/voyager.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/component/demo.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/component/render-graph.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/graph-ui.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/index.html +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/quasar.min.css +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/quasar.min.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/store.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/vue-main.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/__init__.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/demo.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/demo_anno.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/programatic.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/service/__init__.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/service/schema/__init__.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/service/schema/base_entity.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/service/schema/extra.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/test_analysis.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/test_filter.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/test_generic.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/test_import.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/test_module.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/tests/test_type_helper.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/uv.lock +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/voyager.jpg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.2
|
|
4
4
|
Summary: Visualize FastAPI application's routing tree and dependencies
|
|
5
5
|
Project-URL: Homepage, https://github.com/allmonday/fastapi-voyager
|
|
6
6
|
Project-URL: Source, https://github.com/allmonday/fastapi-voyager
|
|
@@ -35,11 +35,12 @@ Visualize your FastAPI endpoints, and explore them interactively.
|
|
|
35
35
|
|
|
36
36
|
> This repo is still in early stage, it supports pydantic v2 only
|
|
37
37
|
|
|
38
|
-
[live demo](https://www.newsyeah.fun/voyager/)
|
|
38
|
+
visit [live demo](https://www.newsyeah.fun/voyager/)
|
|
39
|
+
source code:[composition oriented development pattern](https://github.com/allmonday/composition-oriented-development-pattern)
|
|
39
40
|
|
|
40
|
-
<img width="
|
|
41
|
+
<img width="1597" height="933" alt="image" src="https://github.com/user-attachments/assets/020bf5b2-6c69-44bf-ba1f-39389d388d27" />
|
|
41
42
|
|
|
42
|
-
with configuration
|
|
43
|
+
with simple configuration it can be embedded into FastAPI.
|
|
43
44
|
|
|
44
45
|
```python
|
|
45
46
|
app.mount('/voyager',
|
|
@@ -67,6 +68,8 @@ pip install fastapi-voyager
|
|
|
67
68
|
uv add fastapi-voyager
|
|
68
69
|
```
|
|
69
70
|
|
|
71
|
+
run with cli:
|
|
72
|
+
|
|
70
73
|
```shell
|
|
71
74
|
voyager -m path.to.your.app.module --server
|
|
72
75
|
```
|
|
@@ -77,47 +80,81 @@ voyager -m path.to.your.app.module --server
|
|
|
77
80
|
voyager -m path.to.your.app.module --server --app api
|
|
78
81
|
```
|
|
79
82
|
|
|
80
|
-
## Mount into project
|
|
81
|
-
|
|
82
|
-
```python
|
|
83
|
-
from fastapi import FastAPI
|
|
84
|
-
from fastapi_voyager import create_voyager
|
|
85
|
-
from tests.demo import app
|
|
86
|
-
|
|
87
|
-
app.mount('/voyager', create_voyager(
|
|
88
|
-
app,
|
|
89
|
-
module_color={"tests.service": "red"},
|
|
90
|
-
module_prefix="tests.service"),
|
|
91
|
-
swagger_url="/docs")
|
|
92
|
-
```
|
|
93
83
|
|
|
94
84
|
## Features
|
|
95
85
|
|
|
96
86
|
For scenarios of using FastAPI as internal API integration endpoints, `fastapi-voyager` helps to visualize the dependencies.
|
|
97
87
|
|
|
98
|
-
It is also an architecture
|
|
88
|
+
It is also an architecture tool that can identify issues inside implementation, finding out wrong relationships, overfetchs, or anything else.
|
|
89
|
+
|
|
90
|
+
**If the process of building the view model follows the ER model**, the full potential of fastapi-voyager can be realized. It allows for quick identification of APIs that use entities, as well as which entities are used by a specific API
|
|
99
91
|
|
|
100
|
-
|
|
92
|
+
Given ErDiagram defined by pydantic-resolve, application level entity relationship diagram can be visualized too.
|
|
101
93
|
|
|
102
94
|
### highlight nodes and links
|
|
103
95
|
click a node to highlight it's upperstream and downstream nodes. figure out the related models of one page, or homw many pages are related with one model.
|
|
104
96
|
|
|
105
|
-
|
|
106
97
|
<img width="1100" height="700" alt="image" src="https://github.com/user-attachments/assets/3e0369ea-5fa4-469a-82c1-ed57d407e53d" />
|
|
107
98
|
|
|
108
|
-
### focus on nodes
|
|
109
|
-
|
|
110
|
-
Double click a node, and then toggle focus to hide irrelevant nodes.
|
|
111
|
-
|
|
112
|
-
<img width="1061" height="937" alt="image" src="https://github.com/user-attachments/assets/79709b02-7571-43fc-abc9-17a287a97515" />
|
|
113
|
-
|
|
114
99
|
### view source code
|
|
115
100
|
|
|
116
101
|
double click a node or route to show source code or open file in vscode.
|
|
117
102
|
|
|
118
103
|
<img width="1297" height="940" alt="image" src="https://github.com/user-attachments/assets/c8bb2e7d-b727-42a6-8c9e-64dce297d2d8" />
|
|
119
104
|
|
|
120
|
-
|
|
105
|
+
### quick search
|
|
106
|
+
|
|
107
|
+
seach schemas by name and dispaly it's upstream and downstreams.
|
|
108
|
+
|
|
109
|
+
shift + click can quickly search current one
|
|
110
|
+
|
|
111
|
+
<img width="1587" height="873" alt="image" src="https://github.com/user-attachments/assets/ee4716f3-233d-418f-bc0e-3b214d1498f7" />
|
|
112
|
+
|
|
113
|
+
### display ER diagram
|
|
114
|
+
|
|
115
|
+
ER diagram is a new feature from pydantic-resolve which provide a solid expression for business descritpions.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
diagram = ErDiagram(
|
|
119
|
+
configs=[
|
|
120
|
+
Entity(
|
|
121
|
+
kls=Team,
|
|
122
|
+
relationships=[
|
|
123
|
+
Relationship( field='id', target_kls=list[Sprint], loader=sprint_loader.team_to_sprint_loader),
|
|
124
|
+
Relationship( field='id', target_kls=list[User], loader=user_loader.team_to_user_loader)
|
|
125
|
+
]
|
|
126
|
+
),
|
|
127
|
+
Entity(
|
|
128
|
+
kls=Sprint,
|
|
129
|
+
relationships=[
|
|
130
|
+
Relationship( field='id', target_kls=list[Story], loader=story_loader.sprint_to_story_loader)
|
|
131
|
+
]
|
|
132
|
+
),
|
|
133
|
+
Entity(
|
|
134
|
+
kls=Story,
|
|
135
|
+
relationships=[
|
|
136
|
+
Relationship( field='id', target_kls=list[Task], loader=task_loader.story_to_task_loader),
|
|
137
|
+
Relationship( field='owner_id', target_kls=User, loader=user_loader.user_batch_loader)
|
|
138
|
+
]
|
|
139
|
+
),
|
|
140
|
+
Entity(
|
|
141
|
+
kls=Task,
|
|
142
|
+
relationships=[
|
|
143
|
+
Relationship( field='owner_id', target_kls=User, loader=user_loader.user_batch_loader)
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# display in voyager
|
|
150
|
+
app.mount('/voyager',
|
|
151
|
+
create_voyager(
|
|
152
|
+
app,
|
|
153
|
+
er_diagram=diagram)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
<img width="1276" height="613" alt="image" src="https://github.com/user-attachments/assets/ea0091bb-ee11-4f71-8be3-7129d956c910" />
|
|
157
|
+
|
|
121
158
|
|
|
122
159
|
|
|
123
160
|
## Command Line Usage
|
|
@@ -7,11 +7,12 @@ Visualize your FastAPI endpoints, and explore them interactively.
|
|
|
7
7
|
|
|
8
8
|
> This repo is still in early stage, it supports pydantic v2 only
|
|
9
9
|
|
|
10
|
-
[live demo](https://www.newsyeah.fun/voyager/)
|
|
10
|
+
visit [live demo](https://www.newsyeah.fun/voyager/)
|
|
11
|
+
source code:[composition oriented development pattern](https://github.com/allmonday/composition-oriented-development-pattern)
|
|
11
12
|
|
|
12
|
-
<img width="
|
|
13
|
+
<img width="1597" height="933" alt="image" src="https://github.com/user-attachments/assets/020bf5b2-6c69-44bf-ba1f-39389d388d27" />
|
|
13
14
|
|
|
14
|
-
with configuration
|
|
15
|
+
with simple configuration it can be embedded into FastAPI.
|
|
15
16
|
|
|
16
17
|
```python
|
|
17
18
|
app.mount('/voyager',
|
|
@@ -39,6 +40,8 @@ pip install fastapi-voyager
|
|
|
39
40
|
uv add fastapi-voyager
|
|
40
41
|
```
|
|
41
42
|
|
|
43
|
+
run with cli:
|
|
44
|
+
|
|
42
45
|
```shell
|
|
43
46
|
voyager -m path.to.your.app.module --server
|
|
44
47
|
```
|
|
@@ -49,47 +52,81 @@ voyager -m path.to.your.app.module --server
|
|
|
49
52
|
voyager -m path.to.your.app.module --server --app api
|
|
50
53
|
```
|
|
51
54
|
|
|
52
|
-
## Mount into project
|
|
53
|
-
|
|
54
|
-
```python
|
|
55
|
-
from fastapi import FastAPI
|
|
56
|
-
from fastapi_voyager import create_voyager
|
|
57
|
-
from tests.demo import app
|
|
58
|
-
|
|
59
|
-
app.mount('/voyager', create_voyager(
|
|
60
|
-
app,
|
|
61
|
-
module_color={"tests.service": "red"},
|
|
62
|
-
module_prefix="tests.service"),
|
|
63
|
-
swagger_url="/docs")
|
|
64
|
-
```
|
|
65
55
|
|
|
66
56
|
## Features
|
|
67
57
|
|
|
68
58
|
For scenarios of using FastAPI as internal API integration endpoints, `fastapi-voyager` helps to visualize the dependencies.
|
|
69
59
|
|
|
70
|
-
It is also an architecture
|
|
60
|
+
It is also an architecture tool that can identify issues inside implementation, finding out wrong relationships, overfetchs, or anything else.
|
|
61
|
+
|
|
62
|
+
**If the process of building the view model follows the ER model**, the full potential of fastapi-voyager can be realized. It allows for quick identification of APIs that use entities, as well as which entities are used by a specific API
|
|
71
63
|
|
|
72
|
-
|
|
64
|
+
Given ErDiagram defined by pydantic-resolve, application level entity relationship diagram can be visualized too.
|
|
73
65
|
|
|
74
66
|
### highlight nodes and links
|
|
75
67
|
click a node to highlight it's upperstream and downstream nodes. figure out the related models of one page, or homw many pages are related with one model.
|
|
76
68
|
|
|
77
|
-
|
|
78
69
|
<img width="1100" height="700" alt="image" src="https://github.com/user-attachments/assets/3e0369ea-5fa4-469a-82c1-ed57d407e53d" />
|
|
79
70
|
|
|
80
|
-
### focus on nodes
|
|
81
|
-
|
|
82
|
-
Double click a node, and then toggle focus to hide irrelevant nodes.
|
|
83
|
-
|
|
84
|
-
<img width="1061" height="937" alt="image" src="https://github.com/user-attachments/assets/79709b02-7571-43fc-abc9-17a287a97515" />
|
|
85
|
-
|
|
86
71
|
### view source code
|
|
87
72
|
|
|
88
73
|
double click a node or route to show source code or open file in vscode.
|
|
89
74
|
|
|
90
75
|
<img width="1297" height="940" alt="image" src="https://github.com/user-attachments/assets/c8bb2e7d-b727-42a6-8c9e-64dce297d2d8" />
|
|
91
76
|
|
|
92
|
-
|
|
77
|
+
### quick search
|
|
78
|
+
|
|
79
|
+
seach schemas by name and dispaly it's upstream and downstreams.
|
|
80
|
+
|
|
81
|
+
shift + click can quickly search current one
|
|
82
|
+
|
|
83
|
+
<img width="1587" height="873" alt="image" src="https://github.com/user-attachments/assets/ee4716f3-233d-418f-bc0e-3b214d1498f7" />
|
|
84
|
+
|
|
85
|
+
### display ER diagram
|
|
86
|
+
|
|
87
|
+
ER diagram is a new feature from pydantic-resolve which provide a solid expression for business descritpions.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
diagram = ErDiagram(
|
|
91
|
+
configs=[
|
|
92
|
+
Entity(
|
|
93
|
+
kls=Team,
|
|
94
|
+
relationships=[
|
|
95
|
+
Relationship( field='id', target_kls=list[Sprint], loader=sprint_loader.team_to_sprint_loader),
|
|
96
|
+
Relationship( field='id', target_kls=list[User], loader=user_loader.team_to_user_loader)
|
|
97
|
+
]
|
|
98
|
+
),
|
|
99
|
+
Entity(
|
|
100
|
+
kls=Sprint,
|
|
101
|
+
relationships=[
|
|
102
|
+
Relationship( field='id', target_kls=list[Story], loader=story_loader.sprint_to_story_loader)
|
|
103
|
+
]
|
|
104
|
+
),
|
|
105
|
+
Entity(
|
|
106
|
+
kls=Story,
|
|
107
|
+
relationships=[
|
|
108
|
+
Relationship( field='id', target_kls=list[Task], loader=task_loader.story_to_task_loader),
|
|
109
|
+
Relationship( field='owner_id', target_kls=User, loader=user_loader.user_batch_loader)
|
|
110
|
+
]
|
|
111
|
+
),
|
|
112
|
+
Entity(
|
|
113
|
+
kls=Task,
|
|
114
|
+
relationships=[
|
|
115
|
+
Relationship( field='owner_id', target_kls=User, loader=user_loader.user_batch_loader)
|
|
116
|
+
]
|
|
117
|
+
)
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# display in voyager
|
|
122
|
+
app.mount('/voyager',
|
|
123
|
+
create_voyager(
|
|
124
|
+
app,
|
|
125
|
+
er_diagram=diagram)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
<img width="1276" height="613" alt="image" src="https://github.com/user-attachments/assets/ea0091bb-ee11-4f71-8be3-7129d956c910" />
|
|
129
|
+
|
|
93
130
|
|
|
94
131
|
|
|
95
132
|
## Command Line Usage
|
|
@@ -139,11 +139,17 @@
|
|
|
139
139
|
- 0.13.0
|
|
140
140
|
- [x] if er diagram is provided, show it first.
|
|
141
141
|
- 0.13.1
|
|
142
|
+
- [x] show more details in er diagram
|
|
143
|
+
- 0.13.2
|
|
144
|
+
- [x] show dashed line for link without dataloader
|
|
145
|
+
- 0.13.3
|
|
146
|
+
- [ ] show field description
|
|
147
|
+
- 0.13.4
|
|
142
148
|
- [ ] integration with pydantic-resolve
|
|
143
149
|
- [ ] show hint for resolve, post fields
|
|
144
150
|
- [ ] display loader as edges
|
|
145
151
|
- [ ] add tests
|
|
146
|
-
- 0.13.
|
|
152
|
+
- 0.13.5
|
|
147
153
|
- [ ] refactor vue-main.js, move methods to store
|
|
148
154
|
- [ ] refactor render.py
|
|
149
155
|
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi_voyager.type_helper import (
|
|
4
|
+
update_forward_refs,
|
|
5
|
+
full_class_name,
|
|
6
|
+
get_core_types,
|
|
7
|
+
get_type_name
|
|
8
|
+
)
|
|
9
|
+
from fastapi_voyager.type import (
|
|
10
|
+
FieldInfo,
|
|
11
|
+
PK,
|
|
12
|
+
FieldType,
|
|
13
|
+
LinkType,
|
|
14
|
+
Link,
|
|
15
|
+
ModuleNode,
|
|
16
|
+
SchemaNode,
|
|
17
|
+
)
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
from pydantic_resolve import ErDiagram, Entity, Relationship, MultipleRelationship
|
|
20
|
+
from logging import getLogger
|
|
21
|
+
from fastapi_voyager.module import build_module_schema_tree
|
|
22
|
+
|
|
23
|
+
logger = getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DiagramRenderer:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
*,
|
|
30
|
+
show_fields: FieldType = 'single',
|
|
31
|
+
show_module: bool = True
|
|
32
|
+
) -> None:
|
|
33
|
+
self.show_fields = show_fields if show_fields in ('single', 'object', 'all') else 'single'
|
|
34
|
+
self.show_module = show_module
|
|
35
|
+
|
|
36
|
+
logger.info(f'show_module: {self.show_module}')
|
|
37
|
+
|
|
38
|
+
def render_schema_label(self, node: SchemaNode, color: str | None=None) -> str:
|
|
39
|
+
has_base_fields = any(f.from_base for f in node.fields)
|
|
40
|
+
fields = [n for n in node.fields if n.from_base is False]
|
|
41
|
+
|
|
42
|
+
if self.show_fields == 'all':
|
|
43
|
+
_fields = fields
|
|
44
|
+
elif self.show_fields == 'object':
|
|
45
|
+
_fields = [f for f in fields if f.is_object is True]
|
|
46
|
+
else: # 'single'
|
|
47
|
+
_fields = []
|
|
48
|
+
|
|
49
|
+
fields_parts: list[str] = []
|
|
50
|
+
if self.show_fields == 'all' and has_base_fields:
|
|
51
|
+
fields_parts.append('<tr><td align="left" cellpadding="8"><font color="#999"> Inherited Fields ... </font></td></tr>')
|
|
52
|
+
|
|
53
|
+
for field in _fields:
|
|
54
|
+
type_name = field.type_name[:25] + '..' if len(field.type_name) > 25 else field.type_name
|
|
55
|
+
display_xml = f'<s align="left">{field.name}: {type_name}</s>' if field.is_exclude else f'{field.name}: {type_name}'
|
|
56
|
+
field_str = f"""<tr><td align="left" port="f{field.name}" cellpadding="8"><font> {display_xml} </font></td></tr>"""
|
|
57
|
+
fields_parts.append(field_str)
|
|
58
|
+
|
|
59
|
+
header_color = '#009485' if color is None else color
|
|
60
|
+
header = f"""<tr><td cellpadding="6" bgcolor="{header_color}" align="center" colspan="1" port="{PK}"> <font color="white"> {node.name} </font></td> </tr>"""
|
|
61
|
+
field_content = ''.join(fields_parts) if fields_parts else ''
|
|
62
|
+
return f"""<<table border="0" cellborder="1" cellpadding="0" cellspacing="0" bgcolor="white"> {header} {field_content} </table>>"""
|
|
63
|
+
|
|
64
|
+
def _handle_schema_anchor(self, source: str) -> str:
|
|
65
|
+
if '::' in source:
|
|
66
|
+
a, b = source.split('::', 1)
|
|
67
|
+
return f'"{a}":{b}'
|
|
68
|
+
return f'"{source}"'
|
|
69
|
+
|
|
70
|
+
def render_link(self, link: Link) -> str:
|
|
71
|
+
h = self._handle_schema_anchor
|
|
72
|
+
if link.type == 'schema':
|
|
73
|
+
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "{link.style}", label = "{link.label}", minlen=3];"""
|
|
74
|
+
else:
|
|
75
|
+
raise ValueError(f'Unknown link type: {link.type}')
|
|
76
|
+
|
|
77
|
+
def render_module_schema_content(self, nodes: list[SchemaNode]) -> str:
|
|
78
|
+
def render_node(node: SchemaNode, color: str | None=None) -> str:
|
|
79
|
+
return f'''
|
|
80
|
+
"{node.id}" [
|
|
81
|
+
label = {self.render_schema_label(node, color)}
|
|
82
|
+
shape = "plain"
|
|
83
|
+
margin="0.5,0.1"
|
|
84
|
+
];'''
|
|
85
|
+
|
|
86
|
+
def render_module_schema(mod: ModuleNode, show_cluster:bool=True) -> str:
|
|
87
|
+
inner_nodes = [ render_node(node) for node in mod.schema_nodes ]
|
|
88
|
+
inner_nodes_str = '\n'.join(inner_nodes)
|
|
89
|
+
child_str = '\n'.join(render_module_schema(mod=m, show_cluster=show_cluster) for m in mod.modules)
|
|
90
|
+
|
|
91
|
+
if show_cluster:
|
|
92
|
+
return f'''
|
|
93
|
+
subgraph cluster_module_{mod.fullname.replace('.', '_')} {{
|
|
94
|
+
tooltip="{mod.fullname}"
|
|
95
|
+
color = "#666"
|
|
96
|
+
style="rounded"
|
|
97
|
+
label = " {mod.name}"
|
|
98
|
+
labeljust = "l"
|
|
99
|
+
pencolor="#ccc"
|
|
100
|
+
penwidth=""
|
|
101
|
+
{inner_nodes_str}
|
|
102
|
+
{child_str}
|
|
103
|
+
}}'''
|
|
104
|
+
else:
|
|
105
|
+
return f'''
|
|
106
|
+
{inner_nodes_str}
|
|
107
|
+
{child_str}
|
|
108
|
+
'''
|
|
109
|
+
|
|
110
|
+
# if self.show_module:
|
|
111
|
+
module_schemas = build_module_schema_tree(nodes)
|
|
112
|
+
return '\n'.join(render_module_schema(mod=m, show_cluster=self.show_module) for m in module_schemas)
|
|
113
|
+
|
|
114
|
+
def render_dot(self, nodes: list[SchemaNode], links: list[Link], spline_line=False) -> str:
|
|
115
|
+
module_schemas_str = self.render_module_schema_content(nodes)
|
|
116
|
+
link_str = '\n'.join(self.render_link(link) for link in links)
|
|
117
|
+
|
|
118
|
+
dot_str = f'''
|
|
119
|
+
digraph world {{
|
|
120
|
+
pad="0.5"
|
|
121
|
+
nodesep=0.8
|
|
122
|
+
{'splines=line' if spline_line else ''}
|
|
123
|
+
fontname="Helvetica,Arial,sans-serif"
|
|
124
|
+
node [fontname="Helvetica,Arial,sans-serif"]
|
|
125
|
+
edge [
|
|
126
|
+
fontname="Helvetica,Arial,sans-serif"
|
|
127
|
+
color="gray"
|
|
128
|
+
]
|
|
129
|
+
graph [
|
|
130
|
+
rankdir = "LR"
|
|
131
|
+
];
|
|
132
|
+
node [
|
|
133
|
+
fontsize = "16"
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
subgraph cluster_schema {{
|
|
137
|
+
color = "#aaa"
|
|
138
|
+
margin=18
|
|
139
|
+
style="dashed"
|
|
140
|
+
label=" ER Diagram"
|
|
141
|
+
labeljust="l"
|
|
142
|
+
fontsize="20"
|
|
143
|
+
{module_schemas_str}
|
|
144
|
+
}}
|
|
145
|
+
|
|
146
|
+
{link_str}
|
|
147
|
+
}}
|
|
148
|
+
'''
|
|
149
|
+
return dot_str
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class VoyagerErDiagram:
|
|
153
|
+
def __init__(self,
|
|
154
|
+
er_diagram: ErDiagram,
|
|
155
|
+
show_fields: FieldType = 'single',
|
|
156
|
+
show_module: bool = False):
|
|
157
|
+
|
|
158
|
+
self.er_diagram = er_diagram
|
|
159
|
+
self.nodes: list[SchemaNode] = []
|
|
160
|
+
self.node_set: dict[str, SchemaNode] = {}
|
|
161
|
+
|
|
162
|
+
self.links: list[Link] = []
|
|
163
|
+
self.link_set: set[tuple[str, str]] = set()
|
|
164
|
+
|
|
165
|
+
self.fk_set: dict[str, set[str]] = {}
|
|
166
|
+
|
|
167
|
+
self.show_field = show_fields
|
|
168
|
+
self.show_module = show_module
|
|
169
|
+
|
|
170
|
+
def generate_node_head(self, link_name: str):
|
|
171
|
+
return f'{link_name}::{PK}'
|
|
172
|
+
|
|
173
|
+
def analysis_entity(self, entity: Entity):
|
|
174
|
+
schema = entity.kls
|
|
175
|
+
update_forward_refs(schema)
|
|
176
|
+
self.add_to_node_set(schema, fk_set=self.fk_set.get(full_class_name(schema)))
|
|
177
|
+
|
|
178
|
+
for relationship in entity.relationships:
|
|
179
|
+
annos = get_core_types(relationship.target_kls)
|
|
180
|
+
for anno in annos:
|
|
181
|
+
self.add_to_node_set(anno, fk_set=self.fk_set.get(full_class_name(anno)))
|
|
182
|
+
source_name = f'{full_class_name(schema)}::f{relationship.field}'
|
|
183
|
+
if isinstance(relationship, Relationship):
|
|
184
|
+
self.add_to_link_set(
|
|
185
|
+
source=source_name,
|
|
186
|
+
source_origin=full_class_name(schema),
|
|
187
|
+
target=self.generate_node_head(full_class_name(anno)),
|
|
188
|
+
target_origin=full_class_name(anno),
|
|
189
|
+
type='schema',
|
|
190
|
+
label=get_type_name(relationship.target_kls),
|
|
191
|
+
style='solid' if relationship.loader else 'solid, dashed'
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
elif isinstance(relationship, MultipleRelationship):
|
|
195
|
+
for link in relationship.links:
|
|
196
|
+
self.add_to_link_set(
|
|
197
|
+
source=source_name,
|
|
198
|
+
source_origin=full_class_name(schema),
|
|
199
|
+
target=self.generate_node_head(full_class_name(anno)),
|
|
200
|
+
target_origin=full_class_name(anno),
|
|
201
|
+
type='schema',
|
|
202
|
+
biz=link.biz,
|
|
203
|
+
label=f'{get_type_name(relationship.target_kls)} / {link.biz} ',
|
|
204
|
+
style='solid' if link.loader else 'solid, dashed'
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def add_to_node_set(self, schema, fk_set: set[str] | None = None) -> str:
|
|
208
|
+
"""
|
|
209
|
+
1. calc full_path, add to node_set
|
|
210
|
+
2. if duplicated, do nothing, else insert
|
|
211
|
+
2. return the full_path
|
|
212
|
+
"""
|
|
213
|
+
full_name = full_class_name(schema)
|
|
214
|
+
|
|
215
|
+
if full_name not in self.node_set:
|
|
216
|
+
# skip meta info for normal queries
|
|
217
|
+
self.node_set[full_name] = SchemaNode(
|
|
218
|
+
id=full_name,
|
|
219
|
+
module=schema.__module__,
|
|
220
|
+
name=schema.__name__,
|
|
221
|
+
fields=get_fields(schema, fk_set)
|
|
222
|
+
)
|
|
223
|
+
return full_name
|
|
224
|
+
|
|
225
|
+
def add_to_link_set(
|
|
226
|
+
self,
|
|
227
|
+
source: str,
|
|
228
|
+
source_origin: str,
|
|
229
|
+
target: str,
|
|
230
|
+
target_origin: str,
|
|
231
|
+
type: LinkType,
|
|
232
|
+
label: str,
|
|
233
|
+
style: str,
|
|
234
|
+
biz: str | None = None
|
|
235
|
+
) -> bool:
|
|
236
|
+
"""
|
|
237
|
+
1. add link to link_set
|
|
238
|
+
2. if duplicated, do nothing, else insert
|
|
239
|
+
"""
|
|
240
|
+
pair = (source, target, biz)
|
|
241
|
+
if result := pair not in self.link_set:
|
|
242
|
+
self.link_set.add(pair)
|
|
243
|
+
self.links.append(Link(
|
|
244
|
+
source=source,
|
|
245
|
+
source_origin=source_origin,
|
|
246
|
+
target=target,
|
|
247
|
+
target_origin=target_origin,
|
|
248
|
+
type=type,
|
|
249
|
+
label=label,
|
|
250
|
+
style=style
|
|
251
|
+
))
|
|
252
|
+
return result
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def render_dot(self):
|
|
256
|
+
self.fk_set = {
|
|
257
|
+
full_class_name(entity.kls): set([rel.field for rel in entity.relationships])
|
|
258
|
+
for entity in self.er_diagram.configs
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for entity in self.er_diagram.configs:
|
|
262
|
+
self.analysis_entity(entity)
|
|
263
|
+
renderer = DiagramRenderer(show_fields=self.show_field, show_module=self.show_module)
|
|
264
|
+
return renderer.render_dot(list(self.node_set.values()), self.links)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def get_fields(schema: type[BaseModel], fk_set: set[str] | None = None) -> list[FieldInfo]:
|
|
268
|
+
|
|
269
|
+
fields: list[FieldInfo] = []
|
|
270
|
+
for k, v in schema.model_fields.items():
|
|
271
|
+
anno = v.annotation
|
|
272
|
+
fields.append(FieldInfo(
|
|
273
|
+
is_object=k in fk_set if fk_set is not None else False,
|
|
274
|
+
name=k,
|
|
275
|
+
from_base=False,
|
|
276
|
+
type_name=get_type_name(anno),
|
|
277
|
+
is_exclude=bool(v.exclude)
|
|
278
|
+
))
|
|
279
|
+
return fields
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.13.
|
|
2
|
+
__version__ = "0.13.2"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from typing import Literal
|
|
3
3
|
from pydantic import BaseModel
|
|
4
|
-
from pydantic_resolve import Relationship
|
|
4
|
+
from pydantic_resolve import Relationship, MultipleRelationship, Link
|
|
5
5
|
from .base_entity import BaseEntity
|
|
6
6
|
|
|
7
7
|
|
|
@@ -34,7 +34,10 @@ class Story(BaseModel, BaseEntity):
|
|
|
34
34
|
|
|
35
35
|
class Sprint(BaseModel, BaseEntity):
|
|
36
36
|
__pydantic_resolve_relationships__ = [
|
|
37
|
-
|
|
37
|
+
MultipleRelationship(field='id', target_kls=list[Story], links=[
|
|
38
|
+
Link(biz='all', loader=lambda x: x),
|
|
39
|
+
Link(biz='done', loader=lambda x: x),
|
|
40
|
+
])
|
|
38
41
|
]
|
|
39
42
|
id: int
|
|
40
43
|
name: str
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from fastapi_voyager.type import PK, FieldType, Link, LinkType, SchemaNode
|
|
4
|
-
from fastapi_voyager.type_helper import (
|
|
5
|
-
update_forward_refs,
|
|
6
|
-
full_class_name,
|
|
7
|
-
get_core_types,
|
|
8
|
-
get_type_name
|
|
9
|
-
)
|
|
10
|
-
from fastapi_voyager.render import Renderer
|
|
11
|
-
from fastapi_voyager.type import FieldInfo
|
|
12
|
-
from pydantic import BaseModel
|
|
13
|
-
from pydantic_resolve import ErDiagram, Entity
|
|
14
|
-
|
|
15
|
-
class VoyagerErDiagram:
|
|
16
|
-
def __init__(self,
|
|
17
|
-
er_diagram: ErDiagram,
|
|
18
|
-
show_fields: FieldType = 'single',
|
|
19
|
-
show_module: bool = False):
|
|
20
|
-
self.er_diagram = er_diagram
|
|
21
|
-
self.nodes: list[SchemaNode] = []
|
|
22
|
-
self.node_set: dict[str, SchemaNode] = {}
|
|
23
|
-
|
|
24
|
-
self.links: list[Link] = []
|
|
25
|
-
self.link_set: set[tuple[str, str]] = set()
|
|
26
|
-
|
|
27
|
-
self.fk_set: dict[str, set[str]] = {}
|
|
28
|
-
|
|
29
|
-
self.show_field = show_fields
|
|
30
|
-
self.show_module = show_module
|
|
31
|
-
|
|
32
|
-
def generate_node_head(self, link_name: str):
|
|
33
|
-
return f'{link_name}::{PK}'
|
|
34
|
-
|
|
35
|
-
def analysis_entity(self, entity: Entity):
|
|
36
|
-
schema = entity.kls
|
|
37
|
-
update_forward_refs(schema)
|
|
38
|
-
self.add_to_node_set(schema, fk_set=self.fk_set.get(full_class_name(schema)))
|
|
39
|
-
|
|
40
|
-
for relationship in entity.relationships:
|
|
41
|
-
annos = get_core_types(relationship.target_kls)
|
|
42
|
-
for anno in annos:
|
|
43
|
-
self.add_to_node_set(anno, fk_set=self.fk_set.get(full_class_name(anno)))
|
|
44
|
-
source_name = f'{full_class_name(schema)}::f{relationship.field}'
|
|
45
|
-
self.add_to_link_set(
|
|
46
|
-
source=source_name,
|
|
47
|
-
source_origin=full_class_name(schema),
|
|
48
|
-
target=self.generate_node_head(full_class_name(anno)),
|
|
49
|
-
target_origin=full_class_name(anno),
|
|
50
|
-
type='schema')
|
|
51
|
-
|
|
52
|
-
def add_to_node_set(self, schema, fk_set: set[str] | None = None) -> str:
|
|
53
|
-
"""
|
|
54
|
-
1. calc full_path, add to node_set
|
|
55
|
-
2. if duplicated, do nothing, else insert
|
|
56
|
-
2. return the full_path
|
|
57
|
-
"""
|
|
58
|
-
full_name = full_class_name(schema)
|
|
59
|
-
|
|
60
|
-
if full_name not in self.node_set:
|
|
61
|
-
# skip meta info for normal queries
|
|
62
|
-
self.node_set[full_name] = SchemaNode(
|
|
63
|
-
id=full_name,
|
|
64
|
-
module=schema.__module__,
|
|
65
|
-
name=schema.__name__,
|
|
66
|
-
fields=get_fields(schema, fk_set)
|
|
67
|
-
)
|
|
68
|
-
return full_name
|
|
69
|
-
|
|
70
|
-
def add_to_link_set(
|
|
71
|
-
self,
|
|
72
|
-
source: str,
|
|
73
|
-
source_origin: str,
|
|
74
|
-
target: str,
|
|
75
|
-
target_origin: str,
|
|
76
|
-
type: LinkType
|
|
77
|
-
) -> bool:
|
|
78
|
-
"""
|
|
79
|
-
1. add link to link_set
|
|
80
|
-
2. if duplicated, do nothing, else insert
|
|
81
|
-
"""
|
|
82
|
-
pair = (source, target)
|
|
83
|
-
if result := pair not in self.link_set:
|
|
84
|
-
self.link_set.add(pair)
|
|
85
|
-
self.links.append(Link(
|
|
86
|
-
source=source,
|
|
87
|
-
source_origin=source_origin,
|
|
88
|
-
target=target,
|
|
89
|
-
target_origin=target_origin,
|
|
90
|
-
type=type
|
|
91
|
-
))
|
|
92
|
-
return result
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def render_dot(self):
|
|
96
|
-
self.fk_set = {
|
|
97
|
-
full_class_name(entity.kls): set([rel.field for rel in entity.relationships])
|
|
98
|
-
for entity in self.er_diagram.configs
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
for entity in self.er_diagram.configs:
|
|
102
|
-
self.analysis_entity(entity)
|
|
103
|
-
renderer = Renderer(show_fields=self.show_field, show_module=self.show_module)
|
|
104
|
-
return renderer.render_dot([], [], list(self.node_set.values()), self.links)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def get_fields(schema: type[BaseModel], fk_set: set[str] | None = None) -> list[FieldInfo]:
|
|
108
|
-
|
|
109
|
-
fields: list[FieldInfo] = []
|
|
110
|
-
for k, v in schema.model_fields.items():
|
|
111
|
-
anno = v.annotation
|
|
112
|
-
fields.append(FieldInfo(
|
|
113
|
-
is_object=k in fk_set if fk_set is not None else False,
|
|
114
|
-
name=k,
|
|
115
|
-
from_base=False,
|
|
116
|
-
type_name=get_type_name(anno),
|
|
117
|
-
is_exclude=bool(v.exclude)
|
|
118
|
-
))
|
|
119
|
-
return fields
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/component/render-graph.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/icon/apple-touch-icon.png
RENAMED
|
File without changes
|
{fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/icon/favicon-16x16.png
RENAMED
|
File without changes
|
{fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/icon/favicon-32x32.png
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.13.0 → fastapi_voyager-0.13.2}/src/fastapi_voyager/web/icon/site.webmanifest
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|