appflows 1.0.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.
appflows/__init__.py ADDED
@@ -0,0 +1,94 @@
1
+ """Génération de schémas d'architecture SVG à partir de définitions YAML/JSON.
2
+
3
+ Exemple minimal :
4
+
5
+ >>> from appflows import Generator
6
+ >>> g = Generator()
7
+ >>> g.load_data("examples/yaml/bus.yaml")
8
+ True
9
+ >>> g.calculate_positions()
10
+ >>> svg = g.graph()
11
+ """
12
+
13
+ from importlib.metadata import PackageNotFoundError, version
14
+
15
+ from loguru import logger
16
+
17
+ from appflows.config import (
18
+ MAIN_APP_CODE,
19
+ AppConfig,
20
+ BusConfig,
21
+ Colors,
22
+ ConnectorConfig,
23
+ FlowConfig,
24
+ GraphConfig,
25
+ MainConfig,
26
+ PageConfig,
27
+ Size,
28
+ TagConfig,
29
+ )
30
+ from appflows.formats import (
31
+ BACKWARD_FLOW,
32
+ FORWARD_FLOW,
33
+ load_file,
34
+ parse_chain,
35
+ parse_flow,
36
+ parse_flows_def,
37
+ split_endpoints,
38
+ )
39
+ from appflows.generator import Generator
40
+ from appflows.models import (
41
+ Application,
42
+ Bus,
43
+ Chain,
44
+ Connector,
45
+ DiagramType,
46
+ Flow,
47
+ LayoutMode,
48
+ Link,
49
+ Position,
50
+ Segment,
51
+ )
52
+
53
+ try:
54
+ __version__ = version("appflows")
55
+ except PackageNotFoundError: # paquet non installé (exécution depuis les sources)
56
+ __version__ = "0.0.0.dev0"
57
+
58
+ # Convention loguru pour une bibliothèque : ne rien écrire sur la sortie d'erreur
59
+ # de l'application appelante tant qu'elle ne l'a pas demandé. La CLI et l'UI
60
+ # rappellent `logger.enable("appflows")` ; un intégrateur fait de même.
61
+ logger.disable(__name__)
62
+
63
+ __all__ = [
64
+ "BACKWARD_FLOW",
65
+ "FORWARD_FLOW",
66
+ "MAIN_APP_CODE",
67
+ "AppConfig",
68
+ "Application",
69
+ "Bus",
70
+ "BusConfig",
71
+ "Chain",
72
+ "Colors",
73
+ "Connector",
74
+ "ConnectorConfig",
75
+ "DiagramType",
76
+ "Flow",
77
+ "FlowConfig",
78
+ "Generator",
79
+ "GraphConfig",
80
+ "LayoutMode",
81
+ "Link",
82
+ "MainConfig",
83
+ "PageConfig",
84
+ "Position",
85
+ "Segment",
86
+ "Size",
87
+ "TagConfig",
88
+ "__version__",
89
+ "load_file",
90
+ "parse_chain",
91
+ "parse_flow",
92
+ "parse_flows_def",
93
+ "split_endpoints",
94
+ ]
appflows/cli.py ADDED
@@ -0,0 +1,237 @@
1
+ """Interface en ligne de commande : traitement par lot d'un répertoire.
2
+
3
+ Chaque fichier de définition trouvé dans le répertoire d'entrée est converti en
4
+ SVG, et en PNG si l'extra ``png`` est installé.
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+ from collections.abc import Sequence
10
+ from enum import Enum
11
+ from pathlib import Path
12
+
13
+ from loguru import logger
14
+
15
+ from appflows.formats import SUPPORTED_SUFFIXES
16
+ from appflows.generator import Generator
17
+
18
+ LOG_FORMAT = (
19
+ "<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
20
+ "<level>{level: <8}</level> | <level>{message}</level>"
21
+ )
22
+
23
+
24
+ class DiagramStatus(Enum):
25
+ """Issue du traitement d'une définition.
26
+
27
+ Distinguer « ignoré » de « en échec » est ce qui permet au code de retour de
28
+ la commande d'être exploitable en intégration continue : un fichier déjà
29
+ présent n'est pas une erreur, une définition illisible en est une.
30
+ """
31
+
32
+ PRODUCED = "produit"
33
+ SKIPPED = "ignoré"
34
+ FAILED = "en échec"
35
+
36
+
37
+ def setup_logging(verbose: bool = False, log_file: Path | None = None) -> None:
38
+ """Configure loguru pour la CLI.
39
+
40
+ Args:
41
+ verbose: active le niveau DEBUG sur la sortie d'erreur.
42
+ log_file: si fourni, les erreurs sont également écrites dans ce fichier.
43
+ Sans lui, rien n'est écrit sur disque.
44
+ """
45
+ logger.enable("appflows") # la bibliothèque est muette par défaut
46
+ logger.remove()
47
+ logger.add(sys.stderr, format=LOG_FORMAT, level="DEBUG" if verbose else "INFO")
48
+
49
+ if log_file is not None:
50
+ logger.add(
51
+ log_file,
52
+ format=LOG_FORMAT,
53
+ level="ERROR",
54
+ rotation="1 week",
55
+ retention="1 month",
56
+ )
57
+
58
+
59
+ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
60
+ """Analyse et valide les arguments de la ligne de commande."""
61
+ parser = argparse.ArgumentParser(
62
+ prog="appflows",
63
+ description=(
64
+ "Convertit par lot des définitions de flux applicatifs en schémas SVG (et PNG)."
65
+ ),
66
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
67
+ )
68
+
69
+ parser.add_argument(
70
+ "-i",
71
+ "--input-dir",
72
+ type=Path,
73
+ required=True,
74
+ help=f"Répertoire contenant les définitions ({', '.join(SUPPORTED_SUFFIXES)})",
75
+ )
76
+ parser.add_argument(
77
+ "-o",
78
+ "--output-dir",
79
+ type=Path,
80
+ required=True,
81
+ help="Répertoire de destination des fichiers générés",
82
+ )
83
+ parser.add_argument(
84
+ "-c",
85
+ "--config",
86
+ type=str,
87
+ help="Nom du fichier de configuration situé dans le répertoire d'entrée",
88
+ )
89
+ parser.add_argument(
90
+ "--overwrite",
91
+ action="store_true",
92
+ help="Écraser les fichiers de sortie existants",
93
+ )
94
+ parser.add_argument(
95
+ "--no-png",
96
+ action="store_true",
97
+ help="Ne produire que le SVG, sans conversion PNG",
98
+ )
99
+ parser.add_argument(
100
+ "--log-file",
101
+ type=Path,
102
+ help="Fichier où consigner les erreurs (aucun par défaut)",
103
+ )
104
+ parser.add_argument("-v", "--verbose", action="store_true", help="Sortie détaillée")
105
+
106
+ args = parser.parse_args(argv)
107
+
108
+ if not args.input_dir.is_dir():
109
+ parser.error(f"Le répertoire d'entrée n'existe pas : {args.input_dir}")
110
+
111
+ if args.config:
112
+ config_path = args.input_dir / args.config
113
+ if not config_path.is_file():
114
+ parser.error(f"Le fichier de configuration n'existe pas : {config_path}")
115
+
116
+ return args
117
+
118
+
119
+ def collect_input_files(input_dir: Path, config_file: Path | None = None) -> list[Path]:
120
+ """Liste les définitions à traiter, en excluant le fichier de configuration."""
121
+ excluded = config_file.resolve() if config_file else None
122
+ return sorted(
123
+ f
124
+ for suffix in SUPPORTED_SUFFIXES
125
+ for f in input_dir.glob(f"*{suffix}")
126
+ if excluded is None or f.resolve() != excluded
127
+ )
128
+
129
+
130
+ def _write_png(svg_content: str, output_file: Path) -> bool:
131
+ """Convertit le SVG en PNG. Retourne ``False`` si cairosvg est absent."""
132
+ try:
133
+ import cairosvg
134
+ except ImportError:
135
+ logger.warning(
136
+ "PNG non généré : cairosvg n'est pas installé. "
137
+ "Installez l'extra correspondant avec « pip install appflows[png] » "
138
+ "ou passez --no-png pour masquer cet avertissement."
139
+ )
140
+ return False
141
+
142
+ cairosvg.svg2png(bytestring=svg_content.encode("utf-8"), write_to=str(output_file))
143
+ return True
144
+
145
+
146
+ def process_diagram(
147
+ input_file: Path,
148
+ output_dir: Path,
149
+ config_file: Path | None = None,
150
+ overwrite: bool = False,
151
+ png: bool = True,
152
+ ) -> DiagramStatus:
153
+ """Traite une définition et écrit le SVG (et le PNG) correspondant.
154
+
155
+ Returns:
156
+ L'issue du traitement : produit, ignoré ou en échec.
157
+ """
158
+ output_svg_file = output_dir / input_file.with_suffix(".svg").name
159
+ output_png_file = output_dir / input_file.with_suffix(".png").name
160
+
161
+ if output_svg_file.exists() and not overwrite:
162
+ logger.debug(f"{input_file.name} ignoré (le SVG existe déjà)")
163
+ return DiagramStatus.SKIPPED
164
+
165
+ logger.debug(f"Traitement de {input_file.name}")
166
+
167
+ generator = Generator()
168
+ if not generator.load_data(input_file):
169
+ logger.warning(f"{input_file.name} écarté : définition illisible")
170
+ return DiagramStatus.FAILED
171
+
172
+ if config_file:
173
+ generator.load_config(config_file)
174
+
175
+ try:
176
+ generator.calculate_positions()
177
+ svg_content = generator.graph()
178
+ output_svg_file.write_text(svg_content, encoding="utf-8")
179
+ except Exception as e:
180
+ # Un fichier fautif ne doit pas interrompre le lot en cours.
181
+ logger.error(f"{input_file.name} écarté : {e}")
182
+ return DiagramStatus.FAILED
183
+
184
+ produced = output_svg_file.name
185
+ if png and _write_png(svg_content, output_png_file):
186
+ produced += f" et {output_png_file.name}"
187
+
188
+ logger.info(f"{input_file.name} → {produced}")
189
+ return DiagramStatus.PRODUCED
190
+
191
+
192
+ def main(argv: Sequence[str] | None = None) -> int:
193
+ args = parse_args(argv)
194
+ setup_logging(args.verbose, args.log_file)
195
+
196
+ config_file = args.input_dir / args.config if args.config else None
197
+
198
+ logger.debug(f"Répertoire d'entrée : {args.input_dir}")
199
+ logger.debug(f"Répertoire de sortie : {args.output_dir}")
200
+
201
+ input_files = collect_input_files(args.input_dir, config_file)
202
+ if not input_files:
203
+ logger.error(
204
+ f"Aucune définition ({', '.join(SUPPORTED_SUFFIXES)}) trouvée dans {args.input_dir}"
205
+ )
206
+ return 1
207
+
208
+ args.output_dir.mkdir(parents=True, exist_ok=True)
209
+ logger.info(f"{len(input_files)} fichier(s) à traiter")
210
+
211
+ statuses = [
212
+ process_diagram(
213
+ input_file,
214
+ args.output_dir,
215
+ config_file=config_file,
216
+ overwrite=args.overwrite,
217
+ png=not args.no_png,
218
+ )
219
+ for input_file in input_files
220
+ ]
221
+
222
+ produced = statuses.count(DiagramStatus.PRODUCED)
223
+ skipped = statuses.count(DiagramStatus.SKIPPED)
224
+ failed = statuses.count(DiagramStatus.FAILED)
225
+
226
+ bilan = f"Terminé : {produced}/{len(input_files)} fichier(s) produit(s)"
227
+ if skipped:
228
+ bilan += f", {skipped} ignoré(s) (déjà présent(s))"
229
+ if failed:
230
+ bilan += f", {failed} en échec"
231
+ logger.info(bilan)
232
+
233
+ return 1 if failed else 0
234
+
235
+
236
+ if __name__ == "__main__":
237
+ sys.exit(main())