scitex 2.14.0__py3-none-any.whl → 2.15.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.
Files changed (218) hide show
  1. scitex/__init__.py +47 -0
  2. scitex/_env_loader.py +156 -0
  3. scitex/_mcp_resources/__init__.py +37 -0
  4. scitex/_mcp_resources/_cheatsheet.py +135 -0
  5. scitex/_mcp_resources/_figrecipe.py +138 -0
  6. scitex/_mcp_resources/_formats.py +102 -0
  7. scitex/_mcp_resources/_modules.py +337 -0
  8. scitex/_mcp_resources/_session.py +149 -0
  9. scitex/_mcp_tools/__init__.py +4 -0
  10. scitex/_mcp_tools/audio.py +66 -0
  11. scitex/_mcp_tools/diagram.py +11 -95
  12. scitex/_mcp_tools/introspect.py +191 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +244 -0
  16. scitex/_mcp_tools/writer.py +21 -204
  17. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  18. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  19. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  20. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  21. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  22. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  23. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  24. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  25. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  26. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  27. scitex/audio/README.md +40 -36
  28. scitex/audio/__init__.py +127 -59
  29. scitex/audio/_branding.py +185 -0
  30. scitex/audio/_mcp/__init__.py +32 -0
  31. scitex/audio/_mcp/handlers.py +59 -6
  32. scitex/audio/_mcp/speak_handlers.py +238 -0
  33. scitex/audio/_relay.py +225 -0
  34. scitex/audio/engines/elevenlabs_engine.py +6 -1
  35. scitex/audio/mcp_server.py +228 -75
  36. scitex/canvas/README.md +1 -1
  37. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  38. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  39. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  40. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  41. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  42. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  43. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  44. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  45. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  46. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  47. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  48. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  49. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  50. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  51. scitex/cli/__init__.py +38 -43
  52. scitex/cli/audio.py +76 -27
  53. scitex/cli/capture.py +13 -20
  54. scitex/cli/introspect.py +443 -0
  55. scitex/cli/main.py +198 -109
  56. scitex/cli/mcp.py +60 -34
  57. scitex/cli/scholar/__init__.py +8 -0
  58. scitex/cli/scholar/_crossref_scitex.py +296 -0
  59. scitex/cli/scholar/_fetch.py +25 -3
  60. scitex/cli/social.py +314 -0
  61. scitex/cli/writer.py +117 -0
  62. scitex/config/README.md +1 -1
  63. scitex/config/__init__.py +16 -2
  64. scitex/config/_env_registry.py +191 -0
  65. scitex/diagram/__init__.py +42 -19
  66. scitex/diagram/mcp_server.py +13 -125
  67. scitex/introspect/__init__.py +75 -0
  68. scitex/introspect/_call_graph.py +303 -0
  69. scitex/introspect/_class_hierarchy.py +163 -0
  70. scitex/introspect/_core.py +42 -0
  71. scitex/introspect/_docstring.py +131 -0
  72. scitex/introspect/_examples.py +113 -0
  73. scitex/introspect/_imports.py +271 -0
  74. scitex/introspect/_mcp/__init__.py +37 -0
  75. scitex/introspect/_mcp/handlers.py +208 -0
  76. scitex/introspect/_members.py +151 -0
  77. scitex/introspect/_resolve.py +89 -0
  78. scitex/introspect/_signature.py +131 -0
  79. scitex/introspect/_source.py +80 -0
  80. scitex/introspect/_type_hints.py +172 -0
  81. scitex/io/bundle/README.md +1 -1
  82. scitex/mcp_server.py +98 -5
  83. scitex/plt/__init__.py +248 -550
  84. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  85. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  86. scitex/plt/gallery/README.md +1 -1
  87. scitex/plt/utils/_hitmap/__init__.py +82 -0
  88. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  89. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  90. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  91. scitex/plt/utils/_hitmap/_constants.py +40 -0
  92. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  93. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  94. scitex/plt/utils/_hitmap/_query.py +113 -0
  95. scitex/plt/utils/_hitmap.py +46 -1616
  96. scitex/plt/utils/_metadata/__init__.py +80 -0
  97. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  98. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  99. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  100. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  101. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  102. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  103. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  104. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  105. scitex/plt/utils/_metadata/_csv.py +416 -0
  106. scitex/plt/utils/_metadata/_detect.py +225 -0
  107. scitex/plt/utils/_metadata/_legend.py +127 -0
  108. scitex/plt/utils/_metadata/_rounding.py +117 -0
  109. scitex/plt/utils/_metadata/_verification.py +202 -0
  110. scitex/schema/README.md +1 -1
  111. scitex/scholar/__init__.py +8 -0
  112. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  113. scitex/scholar/core/Scholar.py +63 -1700
  114. scitex/scholar/core/_mixins/__init__.py +36 -0
  115. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  116. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  117. scitex/scholar/core/_mixins/_loaders.py +103 -0
  118. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  119. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  120. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  121. scitex/scholar/core/_mixins/_savers.py +69 -0
  122. scitex/scholar/core/_mixins/_search.py +103 -0
  123. scitex/scholar/core/_mixins/_services.py +88 -0
  124. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  125. scitex/scholar/crossref_scitex.py +367 -0
  126. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  127. scitex/scholar/examples/00_run_all.sh +120 -0
  128. scitex/scholar/jobs/_executors.py +27 -3
  129. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  130. scitex/scholar/pdf_download/_cli.py +154 -0
  131. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  132. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  133. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  134. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  135. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  136. scitex/scholar/pipelines/_single_steps.py +71 -36
  137. scitex/scholar/storage/_LibraryManager.py +97 -1695
  138. scitex/scholar/storage/_mixins/__init__.py +30 -0
  139. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  140. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  141. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  142. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  143. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  144. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  145. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  146. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
  147. scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
  148. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
  149. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
  150. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
  151. scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
  152. scitex/security/README.md +3 -3
  153. scitex/session/README.md +1 -1
  154. scitex/sh/README.md +1 -1
  155. scitex/social/__init__.py +153 -0
  156. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  157. scitex/template/README.md +1 -1
  158. scitex/template/clone_writer_directory.py +5 -5
  159. scitex/writer/README.md +1 -1
  160. scitex/writer/_mcp/handlers.py +11 -744
  161. scitex/writer/_mcp/tool_schemas.py +5 -335
  162. scitex-2.15.1.dist-info/METADATA +648 -0
  163. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
  164. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  165. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  166. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
  167. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
  168. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
  169. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
  170. scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
  171. scitex/diagram/_compile.py +0 -312
  172. scitex/diagram/_diagram.py +0 -355
  173. scitex/diagram/_mcp/__init__.py +0 -4
  174. scitex/diagram/_mcp/handlers.py +0 -400
  175. scitex/diagram/_mcp/tool_schemas.py +0 -157
  176. scitex/diagram/_presets.py +0 -173
  177. scitex/diagram/_schema.py +0 -182
  178. scitex/diagram/_split.py +0 -278
  179. scitex/plt/_mcp/__init__.py +0 -4
  180. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  181. scitex/plt/_mcp/_handlers_figure.py +0 -195
  182. scitex/plt/_mcp/_handlers_plot.py +0 -252
  183. scitex/plt/_mcp/_handlers_style.py +0 -219
  184. scitex/plt/_mcp/handlers.py +0 -74
  185. scitex/plt/_mcp/tool_schemas.py +0 -497
  186. scitex/plt/mcp_server.py +0 -231
  187. scitex/scholar/data/.gitkeep +0 -0
  188. scitex/scholar/data/README.md +0 -44
  189. scitex/scholar/data/bib_files/bibliography.bib +0 -1952
  190. scitex/scholar/data/bib_files/neurovista.bib +0 -277
  191. scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
  192. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
  193. scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
  194. scitex/scholar/data/bib_files/openaccess.bib +0 -89
  195. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
  196. scitex/scholar/data/bib_files/pac.bib +0 -698
  197. scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
  198. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  199. scitex/scholar/data/bib_files/pac_titles.txt +0 -75
  200. scitex/scholar/data/bib_files/paywalled.bib +0 -98
  201. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
  202. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
  203. scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
  204. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  205. scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
  206. scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
  207. scitex/scholar/data/bib_files/test_seizure.bib +0 -46
  208. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  209. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  210. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  211. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  212. scitex/scholar/data/impact_factor.db +0 -0
  213. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  214. scitex/scholar/examples/dev.py +0 -38
  215. scitex-2.14.0.dist-info/METADATA +0 -1238
  216. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
  217. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
  218. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,312 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # Timestamp: 2025-12-15
4
- # Author: ywatanabe / Claude
5
- # File: scitex/diagram/_compile.py
6
-
7
- """
8
- Compilers from DiagramSpec to backend formats (Mermaid, Graphviz).
9
-
10
- The compiler applies paper constraints to generate backend-specific
11
- layout directives. This is where domain knowledge about "good paper figures"
12
- gets encoded.
13
- """
14
-
15
- import json
16
- from typing import Optional
17
- from scitex.diagram._schema import (
18
- DiagramSpec, DiagramType, ColumnLayout, SpacingLevel, PaperMode
19
- )
20
- from scitex.diagram._presets import get_preset, DiagramPreset
21
-
22
-
23
- def compile_to_mermaid(
24
- spec: DiagramSpec,
25
- preset: Optional[DiagramPreset] = None
26
- ) -> str:
27
- """
28
- Compile DiagramSpec to Mermaid format with paper-optimized settings.
29
-
30
- Parameters
31
- ----------
32
- spec : DiagramSpec
33
- The semantic diagram specification.
34
- preset : DiagramPreset, optional
35
- Override preset (default: inferred from spec.type).
36
-
37
- Returns
38
- -------
39
- str
40
- Mermaid diagram source code.
41
- """
42
- if preset is None:
43
- preset = get_preset(spec.type.value)
44
-
45
- lines = []
46
-
47
- # Theme initialization
48
- theme_vars = {**preset.mermaid_theme, **spec.theme}
49
- theme_json = json.dumps({"theme": "base", "themeVariables": theme_vars})
50
- lines.append(f"%%{{init: {theme_json}}}%%")
51
-
52
- # Determine direction based on paper constraints
53
- direction = preset.mermaid_direction
54
- if spec.paper.reading_direction == "top_to_bottom":
55
- direction = "TB"
56
- elif spec.paper.column == ColumnLayout.DOUBLE:
57
- # Double column prefers vertical to save horizontal space
58
- direction = "TB"
59
-
60
- lines.append(f"graph {direction}")
61
-
62
- # Build node ID to spec mapping
63
- node_map = {n.id: n for n in spec.nodes}
64
-
65
- # Generate subgraphs for groups
66
- indent = " "
67
- for group_name, group_nodes in spec.layout.groups.items():
68
- lines.append(f'{indent}subgraph {_sanitize_id(group_name)}["{group_name}"]')
69
- for node_id in group_nodes:
70
- if node_id in node_map:
71
- node = node_map[node_id]
72
- lines.append(f"{indent}{indent}{_mermaid_node(node, preset)}")
73
- lines.append(f"{indent}end")
74
-
75
- # Generate standalone nodes (not in any group)
76
- grouped_nodes = set()
77
- for group_nodes in spec.layout.groups.values():
78
- grouped_nodes.update(group_nodes)
79
-
80
- for node in spec.nodes:
81
- if node.id not in grouped_nodes:
82
- lines.append(f"{indent}{_mermaid_node(node, preset)}")
83
-
84
- # Generate edges
85
- for edge in spec.edges:
86
- edge_str = _mermaid_edge(edge)
87
- lines.append(f"{indent}{edge_str}")
88
-
89
- # Generate styles for emphasized nodes
90
- for node in spec.nodes:
91
- if node.emphasis != "normal" or node.id in spec.paper.emphasize:
92
- emphasis = "primary" if node.id in spec.paper.emphasize else node.emphasis
93
- style = preset.emphasis_styles.get(emphasis, {})
94
- if style:
95
- style_parts = [f"{k}:{v}" for k, v in style.items()]
96
- lines.append(f"{indent}style {_sanitize_id(node.id)} {','.join(style_parts)}")
97
-
98
- return "\n".join(lines)
99
-
100
-
101
- def compile_to_graphviz(
102
- spec: DiagramSpec,
103
- preset: Optional[DiagramPreset] = None
104
- ) -> str:
105
- """
106
- Compile DiagramSpec to Graphviz DOT format.
107
-
108
- Parameters
109
- ----------
110
- spec : DiagramSpec
111
- The semantic diagram specification.
112
- preset : DiagramPreset, optional
113
- Override preset.
114
-
115
- Returns
116
- -------
117
- str
118
- Graphviz DOT source code.
119
- """
120
- if preset is None:
121
- preset = get_preset(spec.type.value)
122
-
123
- is_publication = spec.paper.mode == PaperMode.PUBLICATION
124
- lines = []
125
-
126
- # Determine direction
127
- rankdir = preset.graphviz_rankdir
128
- if spec.paper.reading_direction == "top_to_bottom":
129
- rankdir = "TB"
130
- elif spec.paper.column == ColumnLayout.DOUBLE:
131
- rankdir = "TB"
132
-
133
- # Get spacing - publication mode uses tight spacing
134
- if is_publication:
135
- spacing = preset.spacing_map.get("tight", {})
136
- else:
137
- spacing = preset.spacing_map.get(spec.layout.layer_gap.value, {})
138
- ranksep = spacing.get("ranksep", preset.graphviz_ranksep)
139
- nodesep = spacing.get("nodesep", preset.graphviz_nodesep)
140
-
141
- lines.append("digraph G {")
142
- lines.append(f" rankdir={rankdir};")
143
- lines.append(f" ranksep={ranksep};")
144
- lines.append(f" nodesep={nodesep};")
145
- lines.append(" splines=ortho;") # Orthogonal edges for cleaner look
146
- lines.append(' node [fontname="Helvetica", fontsize=10];')
147
- lines.append(' edge [fontname="Helvetica", fontsize=9];')
148
- lines.append("")
149
-
150
- # Node map
151
- node_map = {n.id: n for n in spec.nodes}
152
-
153
- # Build return edges set for publication mode
154
- return_edge_set = set()
155
- for e in spec.paper.return_edges:
156
- if len(e) >= 2:
157
- return_edge_set.add((e[0], e[1]))
158
-
159
- # Generate subgraphs (without clusters for tighter layout in publication)
160
- if is_publication and spec.layout.layers:
161
- # In publication mode with layers, skip clusters - use rank=same instead
162
- for node in spec.nodes:
163
- lines.append(f" {_graphviz_node(node, preset, spec.paper.emphasize)}")
164
- else:
165
- # Draft mode: use clusters for visual grouping
166
- cluster_idx = 0
167
- for group_name, group_nodes in spec.layout.groups.items():
168
- lines.append(f' subgraph cluster_{cluster_idx} {{')
169
- lines.append(f' label="{group_name}";')
170
- for node_id in group_nodes:
171
- if node_id in node_map:
172
- node = node_map[node_id]
173
- lines.append(f" {_graphviz_node(node, preset, spec.paper.emphasize)}")
174
- lines.append(" }")
175
- cluster_idx += 1
176
-
177
- # Standalone nodes
178
- grouped_nodes = set()
179
- for group_nodes in spec.layout.groups.values():
180
- grouped_nodes.update(group_nodes)
181
-
182
- for node in spec.nodes:
183
- if node.id not in grouped_nodes:
184
- lines.append(f" {_graphviz_node(node, preset, spec.paper.emphasize)}")
185
-
186
- lines.append("")
187
-
188
- # Rank constraints from layers (CRITICAL for minimizing whitespace)
189
- for layer in spec.layout.layers:
190
- if layer:
191
- node_ids = "; ".join(_sanitize_id(n) for n in layer)
192
- lines.append(f" {{ rank=same; {node_ids}; }}")
193
-
194
- lines.append("")
195
-
196
- # Edges - handle return edges in publication mode
197
- for edge in spec.edges:
198
- edge_key = (edge.source, edge.target)
199
- if is_publication and edge_key in return_edge_set:
200
- # Make return edges invisible in publication mode
201
- lines.append(f" {_graphviz_edge_with_style(edge, invisible=True)}")
202
- else:
203
- lines.append(f" {_graphviz_edge(edge)}")
204
-
205
- lines.append("}")
206
-
207
- return "\n".join(lines)
208
-
209
-
210
- def _sanitize_id(s: str) -> str:
211
- """Make string safe for use as node ID."""
212
- import re
213
- # Remove or replace problematic characters for Mermaid/Graphviz
214
- s = re.sub(r'[^\w]', '_', s) # Replace non-word chars with _
215
- s = re.sub(r'_+', '_', s) # Collapse multiple underscores
216
- s = s.strip('_') # Remove leading/trailing underscores
217
- return s or "node"
218
-
219
-
220
- def _mermaid_node(node, preset: DiagramPreset) -> str:
221
- """Generate Mermaid node definition."""
222
- shape_template = preset.mermaid_shapes.get(node.shape, '["__LABEL__"]')
223
- shape_str = shape_template.replace("__LABEL__", node.label)
224
- return f"{_sanitize_id(node.id)}{shape_str}"
225
-
226
-
227
- def _mermaid_edge(edge) -> str:
228
- """Generate Mermaid edge definition."""
229
- arrow = "-->" if edge.arrow == "normal" else "---"
230
- if edge.style == "dashed":
231
- arrow = "-.->" if edge.arrow == "normal" else "-.-"
232
- elif edge.style == "dotted":
233
- arrow = "..>" if edge.arrow == "normal" else "..."
234
-
235
- src = _sanitize_id(edge.source)
236
- tgt = _sanitize_id(edge.target)
237
-
238
- if edge.label:
239
- return f'{src} {arrow}|"{edge.label}"| {tgt}'
240
- return f"{src} {arrow} {tgt}"
241
-
242
-
243
- def _graphviz_node(node, preset: DiagramPreset, emphasize: list) -> str:
244
- """Generate Graphviz node definition."""
245
- shape = preset.graphviz_shapes.get(node.shape, "box")
246
-
247
- # Get emphasis style
248
- emphasis_key = "primary" if node.id in emphasize else node.emphasis
249
- style = preset.emphasis_styles.get(emphasis_key, {})
250
-
251
- attrs = [f'label="{node.label}"', f'shape={shape}']
252
-
253
- # Collect style values (filled, rounded, etc.) - combine with comma
254
- styles = []
255
- if style.get("fill"):
256
- attrs.append(f'fillcolor="{style["fill"]}"')
257
- styles.append("filled")
258
- if style.get("stroke"):
259
- attrs.append(f'color="{style["stroke"]}"')
260
- if node.shape == "rounded":
261
- styles.append("rounded")
262
-
263
- # Output style once with comma-separated values
264
- if styles:
265
- attrs.append(f'style="{",".join(styles)}"')
266
-
267
- return f'{_sanitize_id(node.id)} [{", ".join(attrs)}];'
268
-
269
-
270
- def _graphviz_edge(edge) -> str:
271
- """Generate Graphviz edge definition."""
272
- src = _sanitize_id(edge.source)
273
- tgt = _sanitize_id(edge.target)
274
-
275
- attrs = []
276
- if edge.label:
277
- attrs.append(f'label="{edge.label}"')
278
- if edge.style == "dashed":
279
- attrs.append("style=dashed")
280
- elif edge.style == "dotted":
281
- attrs.append("style=dotted")
282
- if edge.arrow == "none":
283
- attrs.append("arrowhead=none")
284
-
285
- if attrs:
286
- return f'{src} -> {tgt} [{", ".join(attrs)}];'
287
- return f"{src} -> {tgt};"
288
-
289
-
290
- def _graphviz_edge_with_style(edge, invisible: bool = False) -> str:
291
- """Generate Graphviz edge with optional invisible style."""
292
- src = _sanitize_id(edge.source)
293
- tgt = _sanitize_id(edge.target)
294
-
295
- attrs = []
296
- if invisible:
297
- attrs.append("style=invis")
298
- # Invisible edges still constrain layout
299
- attrs.append("constraint=true")
300
- else:
301
- if edge.label:
302
- attrs.append(f'label="{edge.label}"')
303
- if edge.style == "dashed":
304
- attrs.append("style=dashed")
305
- elif edge.style == "dotted":
306
- attrs.append("style=dotted")
307
- if edge.arrow == "none":
308
- attrs.append("arrowhead=none")
309
-
310
- if attrs:
311
- return f'{src} -> {tgt} [{", ".join(attrs)}];'
312
- return f"{src} -> {tgt};"
@@ -1,355 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # Timestamp: 2025-12-15
4
- # Author: ywatanabe / Claude
5
- # File: scitex/diagram/_diagram.py
6
-
7
- """
8
- Main Diagram class - the user-facing API.
9
- """
10
-
11
- from pathlib import Path
12
- from typing import Optional, Union, List
13
- import yaml
14
- import re
15
-
16
- from scitex.diagram._schema import DiagramSpec, DiagramType, NodeSpec, EdgeSpec, PaperMode
17
- from scitex.diagram._compile import compile_to_mermaid, compile_to_graphviz
18
- from scitex.diagram._presets import get_preset
19
- from scitex.diagram._split import split_diagram, SplitConfig, SplitStrategy, SplitResult
20
-
21
-
22
- class Diagram:
23
- """
24
- Paper-optimized diagram with semantic specification.
25
-
26
- This class provides the main interface for creating diagrams
27
- that compile to Mermaid or Graphviz with paper-appropriate
28
- layout constraints.
29
-
30
- Examples
31
- --------
32
- >>> # From YAML spec
33
- >>> d = Diagram.from_yaml("workflow.diagram.yaml")
34
- >>> d.to_mermaid("workflow.mmd")
35
-
36
- >>> # From existing Mermaid (parse and enhance)
37
- >>> d = Diagram.from_mermaid("existing.mmd", diagram_type="workflow")
38
- >>> d.spec.paper.column = "double"
39
- >>> d.to_mermaid("enhanced.mmd")
40
-
41
- >>> # Programmatic creation
42
- >>> d = Diagram(type="pipeline")
43
- >>> d.add_node("input", "Raw Data")
44
- >>> d.add_node("process", "Transform", emphasis="primary")
45
- >>> d.add_node("output", "Results")
46
- >>> d.add_edge("input", "process")
47
- >>> d.add_edge("process", "output")
48
- >>> print(d.to_mermaid())
49
- """
50
-
51
- def __init__(
52
- self,
53
- type: str = "workflow",
54
- title: str = "",
55
- column: str = "single",
56
- ):
57
- """
58
- Initialize a new diagram.
59
-
60
- Parameters
61
- ----------
62
- type : str
63
- Diagram type: workflow, decision, pipeline, hierarchy.
64
- title : str
65
- Diagram title.
66
- column : str
67
- Paper column: single or double.
68
- """
69
- self.spec = DiagramSpec(
70
- type=DiagramType(type),
71
- title=title,
72
- )
73
- self.spec.paper.column = column
74
-
75
- @classmethod
76
- def from_yaml(cls, path: Union[str, Path]) -> "Diagram":
77
- """
78
- Load diagram from YAML specification file.
79
-
80
- Parameters
81
- ----------
82
- path : str or Path
83
- Path to YAML file.
84
-
85
- Returns
86
- -------
87
- Diagram
88
- Loaded diagram.
89
- """
90
- path = Path(path)
91
- with open(path, "r", encoding="utf-8") as f:
92
- data = yaml.safe_load(f)
93
-
94
- diagram = cls.__new__(cls)
95
- diagram.spec = DiagramSpec.from_dict(data)
96
- return diagram
97
-
98
- @classmethod
99
- def from_mermaid(
100
- cls,
101
- path: Union[str, Path],
102
- diagram_type: str = "workflow",
103
- ) -> "Diagram":
104
- """
105
- Parse existing Mermaid file and create enhanced Diagram.
106
-
107
- This allows upgrading existing Mermaid files with SciTeX
108
- paper constraints while preserving the original structure.
109
-
110
- Parameters
111
- ----------
112
- path : str or Path
113
- Path to .mmd file.
114
- diagram_type : str
115
- Inferred diagram type.
116
-
117
- Returns
118
- -------
119
- Diagram
120
- Parsed diagram (can be enhanced and re-exported).
121
- """
122
- path = Path(path)
123
- with open(path, "r", encoding="utf-8") as f:
124
- content = f.read()
125
-
126
- diagram = cls(type=diagram_type)
127
- diagram._parse_mermaid(content)
128
- return diagram
129
-
130
- def _parse_mermaid(self, content: str):
131
- """Parse Mermaid content to extract structure."""
132
- # Extract nodes
133
- # Pattern: ID["Label"] or ID("Label") etc.
134
- node_pattern = r'(\w+)\s*[\[\(\{<][\"\']?([^"\'\]\)\}>]+)[\"\']?[\]\)\}>]'
135
-
136
- for match in re.finditer(node_pattern, content):
137
- node_id = match.group(1)
138
- label = match.group(2).strip()
139
- # Skip if it looks like a subgraph name
140
- if node_id.lower() not in ["subgraph", "end", "style", "graph", "direction"]:
141
- # Check for duplicates
142
- existing = [n for n in self.spec.nodes if n.id == node_id]
143
- if not existing:
144
- self.spec.nodes.append(NodeSpec(id=node_id, label=label))
145
-
146
- # Extract edges
147
- # Pattern: A --> B or A -->|label| B
148
- edge_pattern = r'(\w+)\s*(-->|-.->|-----|---)\s*(?:\|["\']?([^|"\']+)["\']?\|)?\s*(\w+)'
149
-
150
- for match in re.finditer(edge_pattern, content):
151
- source = match.group(1)
152
- arrow = match.group(2)
153
- label = match.group(3)
154
- target = match.group(4)
155
-
156
- style = "solid"
157
- if "-.->" in arrow or "-.." in arrow:
158
- style = "dashed"
159
-
160
- self.spec.edges.append(EdgeSpec(
161
- source=source,
162
- target=target,
163
- label=label.strip() if label else None,
164
- style=style,
165
- ))
166
-
167
- # Extract subgraphs as groups
168
- subgraph_pattern = r'subgraph\s+(\w+)\s*\[?"?([^"\]]*)"?\]?'
169
- for match in re.finditer(subgraph_pattern, content):
170
- group_id = match.group(1)
171
- group_name = match.group(2) or group_id
172
- self.spec.layout.groups[group_name] = []
173
-
174
- def add_node(
175
- self,
176
- id: str,
177
- label: str,
178
- shape: str = "box",
179
- emphasis: str = "normal",
180
- ):
181
- """Add a node to the diagram."""
182
- self.spec.nodes.append(NodeSpec(
183
- id=id,
184
- label=label,
185
- shape=shape,
186
- emphasis=emphasis,
187
- ))
188
-
189
- def add_edge(
190
- self,
191
- source: str,
192
- target: str,
193
- label: Optional[str] = None,
194
- style: str = "solid",
195
- ):
196
- """Add an edge between nodes."""
197
- self.spec.edges.append(EdgeSpec(
198
- source=source,
199
- target=target,
200
- label=label,
201
- style=style,
202
- ))
203
-
204
- def set_group(self, group_name: str, node_ids: list):
205
- """Define a group of nodes (rendered as subgraph)."""
206
- self.spec.layout.groups[group_name] = node_ids
207
-
208
- def emphasize(self, *node_ids: str):
209
- """Mark nodes as emphasized (primary styling)."""
210
- self.spec.paper.emphasize.extend(node_ids)
211
-
212
- def to_mermaid(self, path: Optional[Union[str, Path]] = None) -> str:
213
- """
214
- Compile to Mermaid format.
215
-
216
- Parameters
217
- ----------
218
- path : str or Path, optional
219
- If provided, write to file.
220
-
221
- Returns
222
- -------
223
- str
224
- Mermaid source code.
225
- """
226
- result = compile_to_mermaid(self.spec)
227
-
228
- if path:
229
- path = Path(path)
230
- with open(path, "w", encoding="utf-8") as f:
231
- f.write(result)
232
-
233
- return result
234
-
235
- def to_graphviz(self, path: Optional[Union[str, Path]] = None) -> str:
236
- """
237
- Compile to Graphviz DOT format.
238
-
239
- Parameters
240
- ----------
241
- path : str or Path, optional
242
- If provided, write to file.
243
-
244
- Returns
245
- -------
246
- str
247
- Graphviz DOT source code.
248
- """
249
- result = compile_to_graphviz(self.spec)
250
-
251
- if path:
252
- path = Path(path)
253
- with open(path, "w", encoding="utf-8") as f:
254
- f.write(result)
255
-
256
- return result
257
-
258
- def to_yaml(self, path: Optional[Union[str, Path]] = None) -> str:
259
- """
260
- Export specification as YAML.
261
-
262
- Parameters
263
- ----------
264
- path : str or Path, optional
265
- If provided, write to file.
266
-
267
- Returns
268
- -------
269
- str
270
- YAML specification.
271
- """
272
- data = {
273
- "type": self.spec.type.value,
274
- "title": self.spec.title,
275
- "paper": {
276
- "column": self.spec.paper.column.value if hasattr(self.spec.paper.column, 'value') else self.spec.paper.column,
277
- "max_width_mm": self.spec.paper.max_width_mm,
278
- "reading_direction": self.spec.paper.reading_direction,
279
- "mode": self.spec.paper.mode.value if hasattr(self.spec.paper.mode, 'value') else self.spec.paper.mode,
280
- "emphasize": self.spec.paper.emphasize,
281
- },
282
- "layout": {
283
- "groups": self.spec.layout.groups,
284
- "layers": self.spec.layout.layers,
285
- "layer_gap": self.spec.layout.layer_gap.value,
286
- "node_gap": self.spec.layout.node_gap.value,
287
- },
288
- "nodes": [
289
- {"id": n.id, "label": n.label, "shape": n.shape, "emphasis": n.emphasis}
290
- for n in self.spec.nodes
291
- ],
292
- "edges": [
293
- {"from": e.source, "to": e.target, "label": e.label, "style": e.style}
294
- for e in self.spec.edges
295
- ],
296
- }
297
-
298
- result = yaml.dump(data, default_flow_style=False, allow_unicode=True)
299
-
300
- if path:
301
- path = Path(path)
302
- with open(path, "w", encoding="utf-8") as f:
303
- f.write(result)
304
-
305
- return result
306
-
307
- def split(
308
- self,
309
- max_nodes: int = 12,
310
- strategy: str = "by_groups",
311
- keep_hubs: bool = True,
312
- ) -> List["Diagram"]:
313
- """
314
- Split diagram into multiple figures if too large.
315
-
316
- Parameters
317
- ----------
318
- max_nodes : int
319
- Maximum nodes per figure before splitting.
320
- strategy : str
321
- Split strategy: "by_groups" or "by_articulation".
322
- keep_hubs : bool
323
- Show hub nodes as ghosts in both parts.
324
-
325
- Returns
326
- -------
327
- List[Diagram]
328
- List of split diagrams (or single diagram if no split needed).
329
-
330
- Examples
331
- --------
332
- >>> d = Diagram.from_yaml("large_workflow.yaml")
333
- >>> parts = d.split(max_nodes=8)
334
- >>> for i, part in enumerate(parts):
335
- ... part.to_mermaid(f"workflow_part_{i+1}.mmd")
336
- """
337
- config = SplitConfig(
338
- enabled=True,
339
- max_nodes=max_nodes,
340
- strategy=SplitStrategy(strategy),
341
- keep_hubs=keep_hubs,
342
- )
343
-
344
- result = split_diagram(self.spec, config)
345
-
346
- # Wrap each spec in a Diagram object
347
- diagrams = []
348
- for fig_spec, label in zip(result.figures, result.labels):
349
- d = Diagram.__new__(Diagram)
350
- d.spec = fig_spec
351
- if label:
352
- d.spec.title = f"{self.spec.title} ({label})" if self.spec.title else f"Part {label}"
353
- diagrams.append(d)
354
-
355
- return diagrams
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env python3
2
- # File: __init__.py
3
- """MCP server components."""
4
-