11import re
22import sys
3- from enum import Enum
43from pathlib import Path
5- from typing import Any , Dict , List , Optional
4+ from textwrap import dedent
5+ from typing import List , Optional
66
77import typer
88from typing_extensions import Annotated
9-
9+ from dataclasses import asdict
10+ from paracelsus .config import (
11+ Formats ,
12+ ColumnSorts ,
13+ Layouts ,
14+ ParacelsusSettingsForGraph ,
15+ ParacelsusSettingsForInject ,
16+ MAX_ENUM_MEMBERS_DEFAULT ,
17+ SORT_DEFAULT ,
18+ )
1019from .graph import get_graph_string , transformers
1120from .pyproject import get_pyproject_settings
1221
1322app = typer .Typer ()
1423
15- PYPROJECT_SETTINGS = get_pyproject_settings ()
16-
17-
18- class Formats (str , Enum ):
19- mermaid = "mermaid"
20- mmd = "mmd"
21- dot = "dot"
22- gv = "gv"
23-
24-
25- class ColumnSorts (str , Enum ):
26- key_based = "key-based"
27- preserve = "preserve-order"
28-
29-
30- class Layouts (str , Enum ):
31- dagre = "dagre"
32- elk = "elk"
33-
34-
35- if "column_sort" in PYPROJECT_SETTINGS :
36- SORT_DEFAULT = ColumnSorts (PYPROJECT_SETTINGS ["column_sort" ]).value
37- else :
38- SORT_DEFAULT = ColumnSorts .key_based .value
39-
40- if "omit_comments" in PYPROJECT_SETTINGS :
41- OMIT_COMMENTS_DEFAULT = PYPROJECT_SETTINGS ["omit_comments" ]
42- else :
43- OMIT_COMMENTS_DEFAULT = False
44-
45- if "max_enum_members" in PYPROJECT_SETTINGS :
46- MAX_ENUM_MEMBERS_DEFAULT = PYPROJECT_SETTINGS ["max_enum_members" ]
47- else :
48- MAX_ENUM_MEMBERS_DEFAULT = 3
49-
5024
51- def get_base_class (base_class_path : str | None , settings : Dict [ str , Any ] | None ) -> str :
25+ def get_base_class (base_class_path : str | None , base_from_config : str ) -> str :
5226 if base_class_path :
5327 return base_class_path
54- if not settings :
55- raise ValueError ("`base_class_path` argument must be passed if no pyproject.toml file is present." )
56- if "base" not in settings :
57- raise ValueError ("`base_class_path` argument must be passed if not defined in pyproject.toml." )
58- return settings ["base" ]
28+ if base_from_config :
29+ return base_from_config
30+
31+ raise ValueError (
32+ dedent (
33+ """\
34+ Either provide `--base-class-path` argument or define `base` in the pyproject.toml file:
35+ [tool.paracelsus]
36+ base = "example.base:Base"
37+ """
38+ )
39+ )
5940
6041
6142@app .command (help = "Create the graph structure and print it to stdout." )
6243def graph (
44+ config : Annotated [
45+ Path ,
46+ typer .Option (
47+ help = "Path to a pyproject.toml file to load configuration from." ,
48+ file_okay = True ,
49+ dir_okay = False ,
50+ resolve_path = True ,
51+ exists = True ,
52+ default_factory = lambda : Path .cwd () / "pyproject.toml" ,
53+ show_default = str (Path .cwd () / "pyproject.toml" ),
54+ ),
55+ ],
6356 base_class_path : Annotated [
6457 Optional [str ],
6558 typer .Argument (help = "The SQLAlchemy base class used by the database to graph." ),
@@ -92,58 +85,69 @@ def graph(
9285 Formats , typer .Option (help = "The file format to output the generated graph to." )
9386 ] = Formats .mermaid .value , # type: ignore # Typer will fail to render the help message, but this code works.
9487 column_sort : Annotated [
95- ColumnSorts ,
88+ Optional [ ColumnSorts ] ,
9689 typer .Option (
9790 help = "Specifies the method of sorting columns in diagrams." ,
91+ show_default = str (SORT_DEFAULT .value ),
9892 ),
99- ] = SORT_DEFAULT , # type: ignore # Typer will fail to render the help message, but this code works.
93+ ] = None ,
10094 omit_comments : Annotated [
101- bool ,
95+ Optional [ bool ] ,
10296 typer .Option (
10397 "--omit-comments" ,
10498 help = "Omit SQLAlchemy column comments from the diagram." ,
10599 ),
106- ] = OMIT_COMMENTS_DEFAULT ,
100+ ] = None ,
107101 max_enum_members : Annotated [
108- int ,
102+ Optional [ int ] ,
109103 typer .Option (
110104 "--max-enum-members" ,
111105 help = "Maximum number of enum members to display in diagrams. 0 means no enum values are shown, any positive number limits the display." ,
106+ show_default = str (MAX_ENUM_MEMBERS_DEFAULT ),
112107 ),
113- ] = MAX_ENUM_MEMBERS_DEFAULT ,
108+ ] = None ,
114109 layout : Annotated [
115110 Optional [Layouts ],
116111 typer .Option (
117112 help = "Specifies the layout of the diagram. Only applicable for mermaid format." ,
118113 ),
119114 ] = None ,
120115):
121- settings = get_pyproject_settings ()
122- base_class = get_base_class (base_class_path , settings )
123-
124- if "imports" in settings :
125- import_module .extend (settings ["imports" ])
116+ settings = get_pyproject_settings (config_file = config )
126117
127- if layout and format != Formats .mermaid :
128- raise ValueError ("The `layout` parameter can only be used with the `mermaid` format." )
118+ graph_settings = ParacelsusSettingsForGraph (
119+ base_class_path = get_base_class (base_class_path , settings .base ),
120+ import_module = import_module + settings .imports ,
121+ include_tables = set (include_tables + settings .include_tables ),
122+ exclude_tables = set (exclude_tables + settings .exclude_tables ),
123+ python_dir = python_dir ,
124+ format = format ,
125+ column_sort = column_sort if column_sort is not None else settings .column_sort ,
126+ omit_comments = omit_comments if omit_comments is not None else settings .omit_comments ,
127+ max_enum_members = max_enum_members if max_enum_members is not None else settings .max_enum_members ,
128+ layout = layout ,
129+ )
129130
130131 graph_string = get_graph_string (
131- base_class_path = base_class ,
132- import_module = import_module ,
133- include_tables = set (include_tables + settings .get ("include_tables" , [])),
134- exclude_tables = set (exclude_tables + settings .get ("exclude_tables" , [])),
135- python_dir = python_dir ,
136- format = format .value ,
137- column_sort = column_sort ,
138- omit_comments = omit_comments ,
139- max_enum_members = max_enum_members ,
140- layout = layout .value if layout else None ,
132+ ** asdict (graph_settings ),
141133 )
142134 typer .echo (graph_string , nl = not graph_string .endswith ("\n " ))
143135
144136
145137@app .command (help = "Create a graph and inject it as a code field into a markdown file." )
146138def inject (
139+ config : Annotated [
140+ Path ,
141+ typer .Option (
142+ help = "Path to a pyproject.toml file to load configuration from." ,
143+ file_okay = True ,
144+ dir_okay = False ,
145+ resolve_path = True ,
146+ exists = True ,
147+ default_factory = lambda : Path .cwd () / "pyproject.toml" ,
148+ show_default = str (Path .cwd () / "pyproject.toml" ),
149+ ),
150+ ],
147151 file : Annotated [
148152 Path ,
149153 typer .Argument (
@@ -201,74 +205,78 @@ def inject(
201205 ),
202206 ] = False ,
203207 column_sort : Annotated [
204- ColumnSorts ,
208+ Optional [ ColumnSorts ] ,
205209 typer .Option (
206210 help = "Specifies the method of sorting columns in diagrams." ,
211+ show_default = str (SORT_DEFAULT .value ),
207212 ),
208- ] = SORT_DEFAULT , # type: ignore # Typer will fail to render the help message, but this code works.
213+ ] = None ,
209214 omit_comments : Annotated [
210- bool ,
215+ Optional [ bool ] ,
211216 typer .Option (
212217 "--omit-comments" ,
213218 help = "Omit SQLAlchemy column comments from the diagram." ,
214219 ),
215- ] = OMIT_COMMENTS_DEFAULT ,
220+ ] = None ,
216221 max_enum_members : Annotated [
217- int ,
222+ Optional [ int ] ,
218223 typer .Option (
219224 "--max-enum-members" ,
220225 help = "Maximum number of enum members to display in diagrams. 0 means no enum values are shown, any positive number limits the display." ,
226+ show_default = str (MAX_ENUM_MEMBERS_DEFAULT ),
221227 ),
222- ] = MAX_ENUM_MEMBERS_DEFAULT ,
228+ ] = None ,
223229 layout : Annotated [
224230 Optional [Layouts ],
225231 typer .Option (
226232 help = "Specifies the layout of the diagram. Only applicable for mermaid format." ,
227233 ),
228234 ] = None ,
229235):
230- settings = get_pyproject_settings ()
231- base_class = get_base_class (base_class_path , settings )
232-
233- if "imports" in settings :
234- import_module .extend (settings ["imports" ])
235-
236- if layout and format != Formats .mermaid :
237- raise ValueError ("The `layout` parameter can only be used with the `mermaid` format." )
236+ settings = get_pyproject_settings (config_file = config )
237+
238+ inject_settings = ParacelsusSettingsForInject (
239+ graph_settings = ParacelsusSettingsForGraph (
240+ base_class_path = get_base_class (base_class_path , settings .base ),
241+ import_module = import_module + settings .imports ,
242+ include_tables = set (include_tables + settings .include_tables ),
243+ exclude_tables = set (exclude_tables + settings .exclude_tables ),
244+ python_dir = python_dir ,
245+ format = format ,
246+ column_sort = column_sort if column_sort is not None else settings .column_sort ,
247+ omit_comments = omit_comments if omit_comments is not None else settings .omit_comments ,
248+ max_enum_members = max_enum_members if max_enum_members is not None else settings .max_enum_members ,
249+ layout = layout ,
250+ ),
251+ file = file ,
252+ replace_begin_tag = replace_begin_tag ,
253+ replace_end_tag = replace_end_tag ,
254+ check = check ,
255+ )
238256
239257 # Generate Graph
240258 graph = get_graph_string (
241- base_class_path = base_class ,
242- import_module = import_module ,
243- include_tables = set (include_tables + settings .get ("include_tables" , [])),
244- exclude_tables = set (exclude_tables + settings .get ("exclude_tables" , [])),
245- python_dir = python_dir ,
246- format = format .value ,
247- column_sort = column_sort ,
248- omit_comments = omit_comments ,
249- max_enum_members = max_enum_members ,
250- layout = layout .value if layout else None ,
259+ ** asdict (inject_settings .graph_settings ),
251260 )
252261
253- comment_format = transformers [format ].comment_format # type: ignore
262+ comment_format = transformers [inject_settings . graph_settings . format ].comment_format # type: ignore
254263
255264 # Convert Graph to Injection String
256- graph_piece = f"""{ replace_begin_tag }
265+ graph_piece = f"""{ inject_settings . replace_begin_tag }
257266```{ comment_format }
258267{ graph }
259268```
260- { replace_end_tag } """
269+ { inject_settings . replace_end_tag } """
261270
262271 # Get content from current file.
263- with open (file , "r" ) as fp :
264- old_content = fp .read ()
272+ old_content = inject_settings .file .read_text ()
265273
266274 # Replace old content with newly generated content.
267- pattern = re .escape (replace_begin_tag ) + "(.*)" + re .escape (replace_end_tag )
275+ pattern = re .escape (inject_settings . replace_begin_tag ) + "(.*)" + re .escape (inject_settings . replace_end_tag )
268276 new_content = re .sub (pattern , graph_piece , old_content , flags = re .MULTILINE | re .DOTALL )
269277
270278 # Return result depends on whether we're in check mode.
271- if check :
279+ if inject_settings . check :
272280 if new_content == old_content :
273281 # If content is the same then we passed the test.
274282 typer .echo ("No changes detected." )
@@ -279,8 +287,7 @@ def inject(
279287 sys .exit (1 )
280288 else :
281289 # Dump newly generated contents back to file.
282- with open (file , "w" ) as fp :
283- fp .write (new_content )
290+ inject_settings .file .write_text (new_content )
284291
285292
286293@app .command (help = "Display the current installed version of paracelsus." )
0 commit comments