scitex 2.14.0__py3-none-any.whl → 2.15.3__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 (264) hide show
  1. scitex/__init__.py +71 -17
  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 +210 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +27 -0
  16. scitex/_mcp_tools/template.py +24 -0
  17. scitex/_mcp_tools/writer.py +17 -210
  18. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  19. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  20. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  21. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  22. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  23. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  24. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  25. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  26. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  27. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  28. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
  29. scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
  30. scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
  31. scitex/audio/README.md +40 -36
  32. scitex/audio/__init__.py +129 -61
  33. scitex/audio/_branding.py +185 -0
  34. scitex/audio/_mcp/__init__.py +32 -0
  35. scitex/audio/_mcp/handlers.py +59 -6
  36. scitex/audio/_mcp/speak_handlers.py +238 -0
  37. scitex/audio/_relay.py +225 -0
  38. scitex/audio/_tts.py +18 -10
  39. scitex/audio/engines/base.py +17 -10
  40. scitex/audio/engines/elevenlabs_engine.py +7 -2
  41. scitex/audio/mcp_server.py +228 -75
  42. scitex/canvas/README.md +1 -1
  43. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  44. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  45. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  46. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  47. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  48. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  49. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  50. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  51. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  52. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  53. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  54. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  55. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  56. scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
  57. scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
  58. scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
  59. scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
  60. scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
  61. scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
  62. scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
  63. scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
  64. scitex/canvas/editor/flask_editor/_core.py +25 -1684
  65. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  66. scitex/cli/__init__.py +38 -43
  67. scitex/cli/audio.py +160 -41
  68. scitex/cli/capture.py +133 -20
  69. scitex/cli/introspect.py +488 -0
  70. scitex/cli/main.py +200 -109
  71. scitex/cli/mcp.py +60 -34
  72. scitex/cli/plt.py +414 -0
  73. scitex/cli/repro.py +15 -8
  74. scitex/cli/resource.py +15 -8
  75. scitex/cli/scholar/__init__.py +154 -8
  76. scitex/cli/scholar/_crossref_scitex.py +296 -0
  77. scitex/cli/scholar/_fetch.py +25 -3
  78. scitex/cli/social.py +355 -0
  79. scitex/cli/stats.py +136 -11
  80. scitex/cli/template.py +129 -12
  81. scitex/cli/tex.py +15 -8
  82. scitex/cli/writer.py +49 -299
  83. scitex/cloud/__init__.py +41 -2
  84. scitex/config/README.md +1 -1
  85. scitex/config/__init__.py +16 -2
  86. scitex/config/_env_registry.py +256 -0
  87. scitex/context/__init__.py +22 -0
  88. scitex/dev/__init__.py +20 -1
  89. scitex/diagram/__init__.py +42 -19
  90. scitex/diagram/mcp_server.py +13 -125
  91. scitex/gen/__init__.py +50 -14
  92. scitex/gen/_list_packages.py +4 -4
  93. scitex/introspect/__init__.py +82 -0
  94. scitex/introspect/_call_graph.py +303 -0
  95. scitex/introspect/_class_hierarchy.py +163 -0
  96. scitex/introspect/_core.py +41 -0
  97. scitex/introspect/_docstring.py +131 -0
  98. scitex/introspect/_examples.py +113 -0
  99. scitex/introspect/_imports.py +271 -0
  100. scitex/{gen/_inspect_module.py → introspect/_list_api.py} +48 -56
  101. scitex/introspect/_mcp/__init__.py +41 -0
  102. scitex/introspect/_mcp/handlers.py +233 -0
  103. scitex/introspect/_members.py +155 -0
  104. scitex/introspect/_resolve.py +89 -0
  105. scitex/introspect/_signature.py +131 -0
  106. scitex/introspect/_source.py +80 -0
  107. scitex/introspect/_type_hints.py +172 -0
  108. scitex/io/_save.py +1 -2
  109. scitex/io/bundle/README.md +1 -1
  110. scitex/logging/_formatters.py +19 -9
  111. scitex/mcp_server.py +98 -5
  112. scitex/os/__init__.py +4 -0
  113. scitex/{gen → os}/_check_host.py +4 -5
  114. scitex/plt/__init__.py +245 -550
  115. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  116. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  117. scitex/plt/gallery/README.md +1 -1
  118. scitex/plt/utils/_hitmap/__init__.py +82 -0
  119. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  120. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  121. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  122. scitex/plt/utils/_hitmap/_constants.py +40 -0
  123. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  124. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  125. scitex/plt/utils/_hitmap/_query.py +113 -0
  126. scitex/plt/utils/_hitmap.py +46 -1616
  127. scitex/plt/utils/_metadata/__init__.py +80 -0
  128. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  129. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  130. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  131. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  132. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  133. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  134. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  135. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  136. scitex/plt/utils/_metadata/_csv.py +416 -0
  137. scitex/plt/utils/_metadata/_detect.py +225 -0
  138. scitex/plt/utils/_metadata/_legend.py +127 -0
  139. scitex/plt/utils/_metadata/_rounding.py +117 -0
  140. scitex/plt/utils/_metadata/_verification.py +202 -0
  141. scitex/schema/README.md +1 -1
  142. scitex/scholar/__init__.py +8 -0
  143. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  144. scitex/scholar/core/Scholar.py +63 -1700
  145. scitex/scholar/core/_mixins/__init__.py +36 -0
  146. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  147. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  148. scitex/scholar/core/_mixins/_loaders.py +103 -0
  149. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  150. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  151. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  152. scitex/scholar/core/_mixins/_savers.py +69 -0
  153. scitex/scholar/core/_mixins/_search.py +103 -0
  154. scitex/scholar/core/_mixins/_services.py +88 -0
  155. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  156. scitex/scholar/crossref_scitex.py +367 -0
  157. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  158. scitex/scholar/examples/00_run_all.sh +120 -0
  159. scitex/scholar/jobs/_executors.py +27 -3
  160. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  161. scitex/scholar/pdf_download/_cli.py +154 -0
  162. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  163. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  164. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  165. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  166. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  167. scitex/scholar/pipelines/_single_steps.py +71 -36
  168. scitex/scholar/storage/_LibraryManager.py +97 -1695
  169. scitex/scholar/storage/_mixins/__init__.py +30 -0
  170. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  171. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  172. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  173. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  174. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  175. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  176. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  177. scitex/security/README.md +3 -3
  178. scitex/session/README.md +1 -1
  179. scitex/session/__init__.py +26 -7
  180. scitex/session/_decorator.py +1 -1
  181. scitex/sh/README.md +1 -1
  182. scitex/sh/__init__.py +7 -4
  183. scitex/social/__init__.py +155 -0
  184. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  185. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  186. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  187. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  188. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  189. scitex/stats/_mcp/_handlers/_format.py +94 -0
  190. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  191. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  192. scitex/stats/_mcp/_handlers/_power.py +247 -0
  193. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  194. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  195. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  196. scitex/stats/_mcp/handlers.py +19 -1171
  197. scitex/stats/auto/_stat_style.py +175 -0
  198. scitex/stats/auto/_style_definitions.py +411 -0
  199. scitex/stats/auto/_styles.py +22 -620
  200. scitex/stats/descriptive/__init__.py +11 -8
  201. scitex/stats/descriptive/_ci.py +39 -0
  202. scitex/stats/power/_power.py +15 -4
  203. scitex/str/__init__.py +2 -1
  204. scitex/str/_title_case.py +63 -0
  205. scitex/template/README.md +1 -1
  206. scitex/template/__init__.py +25 -10
  207. scitex/template/_code_templates.py +147 -0
  208. scitex/template/_mcp/handlers.py +81 -0
  209. scitex/template/_mcp/tool_schemas.py +55 -0
  210. scitex/template/_templates/__init__.py +51 -0
  211. scitex/template/_templates/audio.py +233 -0
  212. scitex/template/_templates/canvas.py +312 -0
  213. scitex/template/_templates/capture.py +268 -0
  214. scitex/template/_templates/config.py +43 -0
  215. scitex/template/_templates/diagram.py +294 -0
  216. scitex/template/_templates/io.py +107 -0
  217. scitex/template/_templates/module.py +53 -0
  218. scitex/template/_templates/plt.py +202 -0
  219. scitex/template/_templates/scholar.py +267 -0
  220. scitex/template/_templates/session.py +130 -0
  221. scitex/template/_templates/session_minimal.py +43 -0
  222. scitex/template/_templates/session_plot.py +67 -0
  223. scitex/template/_templates/session_stats.py +77 -0
  224. scitex/template/_templates/stats.py +323 -0
  225. scitex/template/_templates/writer.py +296 -0
  226. scitex/template/clone_writer_directory.py +5 -5
  227. scitex/ui/_backends/_email.py +10 -2
  228. scitex/ui/_backends/_webhook.py +5 -1
  229. scitex/web/_search_pubmed.py +10 -6
  230. scitex/writer/README.md +1 -1
  231. scitex/writer/__init__.py +43 -34
  232. scitex/writer/_mcp/handlers.py +11 -744
  233. scitex/writer/_mcp/tool_schemas.py +5 -335
  234. scitex-2.15.3.dist-info/METADATA +667 -0
  235. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/RECORD +241 -120
  236. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  237. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  238. scitex/diagram/_compile.py +0 -312
  239. scitex/diagram/_diagram.py +0 -355
  240. scitex/diagram/_mcp/__init__.py +0 -4
  241. scitex/diagram/_mcp/handlers.py +0 -400
  242. scitex/diagram/_mcp/tool_schemas.py +0 -157
  243. scitex/diagram/_presets.py +0 -173
  244. scitex/diagram/_schema.py +0 -182
  245. scitex/diagram/_split.py +0 -278
  246. scitex/gen/_ci.py +0 -12
  247. scitex/gen/_title_case.py +0 -89
  248. scitex/plt/_mcp/__init__.py +0 -4
  249. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  250. scitex/plt/_mcp/_handlers_figure.py +0 -195
  251. scitex/plt/_mcp/_handlers_plot.py +0 -252
  252. scitex/plt/_mcp/_handlers_style.py +0 -219
  253. scitex/plt/_mcp/handlers.py +0 -74
  254. scitex/plt/_mcp/tool_schemas.py +0 -497
  255. scitex/plt/mcp_server.py +0 -231
  256. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  257. scitex/scholar/examples/dev.py +0 -38
  258. scitex-2.14.0.dist-info/METADATA +0 -1238
  259. /scitex/{gen → context}/_detect_environment.py +0 -0
  260. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  261. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  262. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/WHEEL +0 -0
  263. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/entry_points.txt +0 -0
  264. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/scholar/storage/_mixins/_symlink_handlers.py
4
+
5
+ """
6
+ Symlink handling mixin for LibraryManager.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional
15
+
16
+ from scitex import logging
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class SymlinkHandlersMixin:
22
+ """Mixin providing symlink handling methods."""
23
+
24
+ def _generate_readable_name(
25
+ self,
26
+ comprehensive_metadata: Dict,
27
+ master_storage_path: Path,
28
+ authors: Optional[List[str]] = None,
29
+ year: Optional[int] = None,
30
+ journal: Optional[str] = None,
31
+ ) -> str:
32
+ """Generate readable symlink name from metadata."""
33
+ from scitex.dict import DotDict
34
+
35
+ # Extract author
36
+ first_author = "Unknown"
37
+ if authors and len(authors) > 0:
38
+ author_parts = authors[0].split()
39
+ first_author = (
40
+ author_parts[-1] if len(author_parts) > 1 else author_parts[0]
41
+ )
42
+ first_author = "".join(c for c in first_author if c.isalnum() or c == "-")[
43
+ :20
44
+ ]
45
+
46
+ # Format year
47
+ if isinstance(year, DotDict):
48
+ year = None
49
+
50
+ if isinstance(year, str) and year.isdigit():
51
+ year = int(year)
52
+
53
+ year_str = f"{year:04d}" if isinstance(year, int) else "0000"
54
+
55
+ # Clean journal name
56
+ journal_clean = "Unknown"
57
+ if journal:
58
+ journal_clean = self.config.path_manager._sanitize_filename(journal)[:30]
59
+ if not journal_clean:
60
+ journal_clean = "Unknown"
61
+
62
+ # Get citation count and impact factor
63
+ cc, if_val = self._extract_cc_and_if(comprehensive_metadata)
64
+
65
+ # Count PDFs
66
+ pdf_files = list(master_storage_path.glob("*.pdf"))
67
+ n_pdfs = len(pdf_files)
68
+
69
+ readable_name = f"PDF-{n_pdfs:02d}_CC-{cc:06d}_IF-{int(if_val):03d}_{year_str}_{first_author}_{journal_clean}"
70
+ return readable_name
71
+
72
+ def _extract_cc_and_if(self, comprehensive_metadata: Dict) -> tuple:
73
+ """Extract citation count and impact factor from metadata."""
74
+ if "metadata" in comprehensive_metadata:
75
+ metadata_section = comprehensive_metadata.get("metadata", {})
76
+ cc_val = metadata_section.get("citation_count", {})
77
+ if isinstance(cc_val, dict):
78
+ cc = cc_val.get("total", 0) or 0
79
+ else:
80
+ cc = cc_val or 0
81
+
82
+ publication_section = metadata_section.get("publication", {})
83
+ if_val = publication_section.get("impact_factor", 0.0) or 0.0
84
+ else:
85
+ cc_val = comprehensive_metadata.get("citation_count", 0)
86
+ if isinstance(cc_val, dict):
87
+ cc = cc_val.get("total", 0) or 0
88
+ else:
89
+ cc = cc_val or 0
90
+
91
+ if_val = (
92
+ comprehensive_metadata.get("journal_impact_factor")
93
+ or comprehensive_metadata.get("impact_factor")
94
+ or comprehensive_metadata.get("publication", {}).get("impact_factor")
95
+ )
96
+ if isinstance(if_val, dict):
97
+ if_val = if_val.get("value", 0.0) or 0.0
98
+ else:
99
+ if_val = if_val or 0.0
100
+
101
+ return cc, if_val
102
+
103
+ def update_symlink(
104
+ self,
105
+ master_storage_path: Path,
106
+ project: str,
107
+ metadata: Optional[Dict] = None,
108
+ ) -> Optional[Path]:
109
+ """Update project symlink to reflect current paper status."""
110
+ try:
111
+ if metadata is None:
112
+ metadata_file = master_storage_path / "metadata.json"
113
+ if metadata_file.exists():
114
+ with open(metadata_file) as f:
115
+ metadata = json.load(f)
116
+ else:
117
+ logger.warning(f"No metadata found for {master_storage_path.name}")
118
+ return None
119
+
120
+ # Extract metadata from nested structure if needed
121
+ if "metadata" in metadata:
122
+ meta_section = metadata.get("metadata", {})
123
+ basic_section = meta_section.get("basic", {})
124
+ pub_section = meta_section.get("publication", {})
125
+ authors = basic_section.get("authors")
126
+ year = basic_section.get("year")
127
+ journal = pub_section.get("journal")
128
+ else:
129
+ authors = metadata.get("authors")
130
+ year = metadata.get("year")
131
+ journal = metadata.get("journal")
132
+
133
+ readable_name = self._generate_readable_name(
134
+ comprehensive_metadata=metadata,
135
+ master_storage_path=master_storage_path,
136
+ authors=authors,
137
+ year=year,
138
+ journal=journal,
139
+ )
140
+
141
+ return self._create_project_symlink(
142
+ master_storage_path=master_storage_path,
143
+ project=project,
144
+ readable_name=readable_name,
145
+ )
146
+ except Exception as exc_:
147
+ logger.error(
148
+ f"Failed to update symlink for {master_storage_path.name}: {exc_}"
149
+ )
150
+ return None
151
+
152
+ def _create_project_symlink(
153
+ self, master_storage_path: Path, project: str, readable_name: str
154
+ ) -> Optional[Path]:
155
+ """Create symlink in project directory pointing to master storage."""
156
+ try:
157
+ project_dir = self.config.path_manager.get_library_project_dir(project)
158
+ symlink_path = project_dir / readable_name
159
+ master_id = master_storage_path.name
160
+
161
+ # Remove old symlinks pointing to the same master entry
162
+ for existing_link in project_dir.iterdir():
163
+ if not existing_link.is_symlink():
164
+ continue
165
+
166
+ try:
167
+ target = existing_link.resolve()
168
+ if target.name == master_id and existing_link.name != readable_name:
169
+ logger.debug(f"Removing old symlink: {existing_link.name}")
170
+ existing_link.unlink()
171
+ except Exception as e:
172
+ logger.debug(f"Skipping broken symlink {existing_link.name}: {e}")
173
+ continue
174
+
175
+ # Create new symlink
176
+ if not symlink_path.exists():
177
+ relative_path = os.path.relpath(master_storage_path, project_dir)
178
+ symlink_path.symlink_to(relative_path)
179
+ logger.success(
180
+ f"Created project symlink: {symlink_path} -> {relative_path}"
181
+ )
182
+ else:
183
+ logger.debug(f"Project symlink already exists: {symlink_path}")
184
+
185
+ return symlink_path
186
+
187
+ except Exception as exc_:
188
+ logger.warning(f"Failed to create project symlink: {exc_}")
189
+ return None
190
+
191
+ def _ensure_project_symlink(
192
+ self,
193
+ title: str,
194
+ year: Optional[int] = None,
195
+ authors: Optional[List[str]] = None,
196
+ paper_id: str = None,
197
+ master_storage_path: Path = None,
198
+ ) -> None:
199
+ """Ensure project symlink exists for paper in master library."""
200
+ try:
201
+ if not paper_id or not master_storage_path:
202
+ return
203
+
204
+ project_lib_path = (
205
+ self.config.path_manager.get_scholar_library_path() / self.project
206
+ )
207
+ project_lib_path.mkdir(parents=True, exist_ok=True)
208
+
209
+ paper_info = {"title": title, "year": year, "authors": authors or []}
210
+ readable_paths = self._call_path_manager_get_storage_paths(
211
+ paper_info=paper_info, collection_name=self.project
212
+ )
213
+ readable_name = readable_paths["readable_name"]
214
+ symlink_path = project_lib_path / readable_name
215
+ relative_path = f"../MASTER/{paper_id}"
216
+
217
+ if not symlink_path.exists():
218
+ symlink_path.symlink_to(relative_path)
219
+ logger.info(
220
+ f"Created project symlink: {readable_name} -> {relative_path}"
221
+ )
222
+ except Exception as exc_:
223
+ logger.debug(f"Error creating project symlink: {exc_}")
224
+
225
+
226
+ # EOF
scitex/security/README.md CHANGED
@@ -14,7 +14,7 @@ Reusable security utilities for the SciTeX ecosystem. Works everywhere: local, c
14
14
  The module is part of the `scitex` package (editable install):
15
15
 
16
16
  ```bash
17
- cd ~/proj/scitex-code
17
+ cd ~/proj/scitex-python
18
18
  pip install -e .
19
19
  ```
20
20
 
@@ -162,7 +162,7 @@ Total open alerts: 2
162
162
  ```yaml
163
163
  - name: Check Security
164
164
  run: |
165
- pip install -e ~/proj/scitex-code
165
+ pip install -e ~/proj/scitex-python
166
166
  python -m scitex.security.cli check --save
167
167
  ```
168
168
 
@@ -245,7 +245,7 @@ Get path to latest alerts file.
245
245
  ## Testing
246
246
 
247
247
  ```bash
248
- cd ~/proj/scitex-code
248
+ cd ~/proj/scitex-python
249
249
  pytest tests/test_security.py
250
250
  ```
251
251
 
scitex/session/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <!-- ---
2
2
  !-- Timestamp: 2025-11-18 10:14:48
3
3
  !-- Author: ywatanabe
4
- !-- File: /home/ywatanabe/proj/scitex-code/src/scitex/session/README.md
4
+ !-- File: /home/ywatanabe/proj/scitex-python/src/scitex/session/README.md
5
5
  !-- --- -->
6
6
 
7
7
  # scitex.session
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  # Timestamp: "2025-08-21 20:36:45 (ywatanabe)"
4
3
  # File: /home/ywatanabe/proj/SciTeX-Code/src/scitex/session/__init__.py
5
4
  # ----------------------------------------
6
5
  from __future__ import annotations
6
+
7
7
  import os
8
8
 
9
9
  __FILE__ = __file__
@@ -20,27 +20,46 @@ Usage:
20
20
  import sys
21
21
  import matplotlib.pyplot as plt
22
22
  from scitex import session
23
-
23
+
24
24
  # Start a session
25
25
  CONFIG, sys.stdout, sys.stderr, plt, COLORS, rng = session.start(sys, plt)
26
-
26
+
27
27
  # Your experiment code here
28
-
29
- # Close the session
28
+
29
+ # Close the session
30
30
  session.close(CONFIG)
31
31
 
32
32
  # Session manager for advanced use cases
33
33
  manager = session.SessionManager()
34
34
  active_sessions = manager.get_active_sessions()
35
+
36
+ # Using INJECTED sentinel for decorator parameters
37
+ @stx.session
38
+ def main(CONFIG=stx.session.INJECTED, plt=stx.session.INJECTED):
39
+ ...
35
40
  """
36
41
 
42
+
43
+ # Sentinel object for decorator-injected parameters
44
+ class _InjectedSentinel:
45
+ """Sentinel value indicating a parameter will be injected by a decorator."""
46
+
47
+ def __repr__(self):
48
+ return "<INJECTED>"
49
+
50
+
51
+ INJECTED = _InjectedSentinel()
52
+
53
+
37
54
  # Import session management functionality
55
+ from ._decorator import run, session
56
+ from ._lifecycle import close, running2finished, start
38
57
  from ._manager import SessionManager
39
- from ._lifecycle import start, close, running2finished
40
- from ._decorator import session, run
41
58
 
42
59
  # Export public API
43
60
  __all__ = [
61
+ # Sentinel for injected parameters
62
+ "INJECTED",
44
63
  # Session lifecycle (main functions)
45
64
  "start",
46
65
  "close",
@@ -21,7 +21,7 @@ import sys as sys_module
21
21
 
22
22
  from ._lifecycle import start, close
23
23
  from scitex.logging import getLogger
24
- from scitex import INJECTED
24
+ from . import INJECTED # Use local INJECTED from session module
25
25
 
26
26
  # Internal logger for the decorator itself
27
27
  _decorator_logger = getLogger(__name__)
scitex/sh/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <!-- ---
2
2
  !-- Timestamp: 2025-10-29 07:23:56
3
3
  !-- Author: ywatanabe
4
- !-- File: /home/ywatanabe/proj/scitex-code/src/scitex/sh/README.md
4
+ !-- File: /home/ywatanabe/proj/scitex-python/src/scitex/sh/README.md
5
5
  !-- --- -->
6
6
 
7
7
  # scitex.sh - Shell Command Execution Module
scitex/sh/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  from __future__ import annotations
3
+
4
4
  import os
5
5
 
6
6
  __FILE__ = __file__
@@ -8,9 +8,9 @@ __DIR__ = os.path.dirname(__FILE__)
8
8
 
9
9
  from typing import Union
10
10
 
11
- from ._types import CommandInput, ReturnFormat, ShellResult
12
- from ._security import quote, validate_command
13
11
  from ._execute import execute
12
+ from ._security import quote, validate_command
13
+ from ._types import CommandInput, ReturnFormat, ShellResult
14
14
 
15
15
 
16
16
  def sh(
@@ -88,6 +88,9 @@ def sh_run(command: CommandInput, verbose: bool = True) -> ShellResult:
88
88
  return execute(command, verbose=verbose)
89
89
 
90
90
 
91
- __all__ = ["sh", "sh_run", "quote"]
91
+ # Legacy functions moved from gen module
92
+ from ._shell_legacy import run_shellcommand, run_shellscript
93
+
94
+ __all__ = ["sh", "sh_run", "quote", "run_shellcommand", "run_shellscript"]
92
95
 
93
96
  # EOF
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2026-01-22
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/social/__init__.py
4
+
5
+ """SciTeX Social - Unified social media management.
6
+
7
+ This module provides a thin wrapper around socialia, the core social media
8
+ integration package. It uses scitex branding and environment variable prefixes.
9
+
10
+ Features
11
+ --------
12
+ - Twitter/X posting and analytics
13
+ - LinkedIn posting
14
+ - Reddit posting
15
+ - YouTube analytics
16
+ - Google Analytics integration
17
+
18
+ Environment Variables
19
+ ---------------------
20
+ Credentials use SCITEX_SOCIAL_ prefix (falls back to SOCIALIA_):
21
+ - SCITEX_SOCIAL_X_CONSUMER_KEY, SCITEX_SOCIAL_X_CONSUMER_KEY_SECRET
22
+ - SCITEX_SOCIAL_X_ACCESS_TOKEN, SCITEX_SOCIAL_X_ACCESS_TOKEN_SECRET
23
+ - SCITEX_SOCIAL_X_BEARER_TOKEN
24
+ - SCITEX_SOCIAL_LINKEDIN_CLIENT_ID, SCITEX_SOCIAL_LINKEDIN_CLIENT_SECRET
25
+ - SCITEX_SOCIAL_LINKEDIN_ACCESS_TOKEN
26
+ - SCITEX_SOCIAL_REDDIT_CLIENT_ID, SCITEX_SOCIAL_REDDIT_CLIENT_SECRET
27
+ - SCITEX_SOCIAL_YOUTUBE_API_KEY
28
+ - SCITEX_SOCIAL_GOOGLE_ANALYTICS_PROPERTY_ID
29
+
30
+ Usage
31
+ -----
32
+ import scitex as stx
33
+
34
+ # Twitter/X
35
+ x = stx.social.Twitter()
36
+ x.post("Hello from SciTeX!")
37
+
38
+ # LinkedIn
39
+ linkedin = stx.social.LinkedIn()
40
+ linkedin.post("Research update", visibility="public")
41
+
42
+ # YouTube analytics
43
+ yt = stx.social.YouTube()
44
+ stats = yt.get_channel_stats()
45
+
46
+ # Google Analytics
47
+ ga = stx.social.GoogleAnalytics()
48
+ report = ga.get_report(start_date="7daysAgo")
49
+
50
+ See Also
51
+ --------
52
+ - socialia: https://github.com/ywatanabe1989/socialia
53
+ - scitex: https://scitex.ai
54
+ """
55
+
56
+ import os as _os
57
+
58
+ # Set branding BEFORE importing socialia
59
+ _os.environ.setdefault("SOCIALIA_BRAND", "scitex.social")
60
+ _os.environ.setdefault("SOCIALIA_ENV_PREFIX", "SCITEX_SOCIAL")
61
+
62
+ # Check socialia availability
63
+ try:
64
+ import socialia as _socialia
65
+
66
+ # Re-export platform clients
67
+ from socialia import (
68
+ # Content strategies for MCP
69
+ PLATFORM_STRATEGIES,
70
+ # Base class
71
+ BasePoster,
72
+ GoogleAnalytics,
73
+ LinkedIn,
74
+ LinkedInPoster,
75
+ Reddit,
76
+ RedditPoster,
77
+ # Platform clients (preferred names)
78
+ Twitter,
79
+ # Backward compatibility aliases
80
+ TwitterPoster,
81
+ YouTube,
82
+ YouTubePoster,
83
+ )
84
+ from socialia import __version__ as _socialia_version
85
+
86
+ SOCIALIA_AVAILABLE = True
87
+ __socialia_version__ = _socialia_version
88
+
89
+ except ImportError:
90
+ SOCIALIA_AVAILABLE = False
91
+ __socialia_version__ = None
92
+
93
+ # Provide helpful error on access
94
+ class _SocialiaNotAvailable:
95
+ """Placeholder when socialia is not installed."""
96
+
97
+ def __init__(self, *args, **kwargs):
98
+ raise ImportError(
99
+ "socialia is required for scitex.social. "
100
+ "Install with: pip install socialia"
101
+ )
102
+
103
+ def __getattr__(self, name):
104
+ raise ImportError(
105
+ "socialia is required for scitex.social. "
106
+ "Install with: pip install socialia"
107
+ )
108
+
109
+ BasePoster = _SocialiaNotAvailable
110
+ Twitter = _SocialiaNotAvailable
111
+ LinkedIn = _SocialiaNotAvailable
112
+ Reddit = _SocialiaNotAvailable
113
+ YouTube = _SocialiaNotAvailable
114
+ GoogleAnalytics = _SocialiaNotAvailable
115
+ TwitterPoster = _SocialiaNotAvailable
116
+ LinkedInPoster = _SocialiaNotAvailable
117
+ RedditPoster = _SocialiaNotAvailable
118
+ YouTubePoster = _SocialiaNotAvailable
119
+ PLATFORM_STRATEGIES = ""
120
+
121
+
122
+ def has_socialia() -> bool:
123
+ """Check if socialia is available.
124
+
125
+ Returns
126
+ -------
127
+ bool
128
+ True if socialia is installed and importable.
129
+ """
130
+ return SOCIALIA_AVAILABLE
131
+
132
+
133
+ __all__ = [
134
+ # Availability check
135
+ "SOCIALIA_AVAILABLE",
136
+ "has_socialia",
137
+ "__socialia_version__",
138
+ # Base class
139
+ "BasePoster",
140
+ # Platform clients (preferred names)
141
+ "Twitter",
142
+ "LinkedIn",
143
+ "Reddit",
144
+ "YouTube",
145
+ "GoogleAnalytics",
146
+ # Backward compatibility aliases
147
+ "TwitterPoster",
148
+ "LinkedInPoster",
149
+ "RedditPoster",
150
+ "YouTubePoster",
151
+ # Content strategies
152
+ "PLATFORM_STRATEGIES",
153
+ ]
154
+
155
+ # EOF
@@ -0,0 +1,149 @@
1
+ # External Package Branding Guide
2
+
3
+ When scitex wraps external packages (like `figrecipe` for `scitex.plt` or `crossref-local` for `scitex.scholar`), those packages should support configurable branding so documentation and error messages show the scitex namespace.
4
+
5
+ ## When to Use Branding
6
+
7
+ - **Use branding**: External packages that scitex wraps (figrecipe, crossref-local, etc.)
8
+ - **Don't use branding**: Internal scitex modules (scitex.audio, scitex.stats, etc.) - just hardcode `SCITEX_*` prefix
9
+
10
+ ## Pattern Overview
11
+
12
+ The external package provides a `_branding.py` module that:
13
+ 1. Reads brand name from environment variable
14
+ 2. Derives environment variable prefix from brand name
15
+ 3. Provides helper functions for rebranding text/docstrings
16
+
17
+ ## Implementation Template
18
+
19
+ ```python
20
+ # external_package/_branding.py
21
+
22
+ import os
23
+ import re
24
+ from typing import Optional
25
+
26
+ # Environment variables for branding
27
+ # Parent package sets these before importing
28
+ BRAND_NAME = os.environ.get("{PACKAGE}_BRAND", "{package}")
29
+ BRAND_ALIAS = os.environ.get("{PACKAGE}_ALIAS", "{alias}")
30
+
31
+ # Original values for replacement
32
+ _ORIGINAL_NAME = "{package}"
33
+ _ORIGINAL_ALIAS = "{alias}"
34
+
35
+
36
+ def _brand_to_env_prefix(brand: str) -> str:
37
+ """Convert brand name to environment variable prefix.
38
+
39
+ Examples:
40
+ "figrecipe" -> "FIGRECIPE"
41
+ "scitex.plt" -> "SCITEX_PLT"
42
+ "crossref-local" -> "CROSSREF_LOCAL"
43
+ """
44
+ return brand.upper().replace(".", "_").replace("-", "_")
45
+
46
+
47
+ # Environment variable prefix based on brand
48
+ ENV_PREFIX = _brand_to_env_prefix(BRAND_NAME)
49
+
50
+
51
+ def get_env(key: str, default: Optional[str] = None) -> Optional[str]:
52
+ """Get environment variable with brand-aware prefix.
53
+
54
+ Checks {ENV_PREFIX}_{key} first, then falls back to original prefix.
55
+ """
56
+ value = os.environ.get(f"{ENV_PREFIX}_{key}")
57
+ if value is not None:
58
+ return value
59
+
60
+ # Fall back to original prefix if different
61
+ original_prefix = _brand_to_env_prefix(_ORIGINAL_NAME)
62
+ if ENV_PREFIX != original_prefix:
63
+ value = os.environ.get(f"{original_prefix}_{key}")
64
+ if value is not None:
65
+ return value
66
+
67
+ return default
68
+
69
+
70
+ def rebrand_text(text: Optional[str]) -> Optional[str]:
71
+ """Apply branding to a text string (docstrings, error messages)."""
72
+ if text is None:
73
+ return None
74
+
75
+ if BRAND_NAME == _ORIGINAL_NAME and BRAND_ALIAS == _ORIGINAL_ALIAS:
76
+ return text
77
+
78
+ result = text
79
+
80
+ # Replace import statements
81
+ result = re.sub(
82
+ rf"import\s+{_ORIGINAL_NAME}\s+as\s+{_ORIGINAL_ALIAS}",
83
+ f"import {BRAND_NAME} as {BRAND_ALIAS}",
84
+ result,
85
+ )
86
+
87
+ # Replace "from package" statements
88
+ result = re.sub(
89
+ rf"from\s+{_ORIGINAL_NAME}(\s+import|\s*\.)",
90
+ lambda m: f"from {BRAND_NAME}{m.group(1)}",
91
+ result,
92
+ )
93
+
94
+ return result
95
+
96
+
97
+ def get_mcp_server_name() -> str:
98
+ """Get the MCP server name based on branding."""
99
+ return BRAND_NAME.replace(".", "-")
100
+ ```
101
+
102
+ ## Usage in Parent Package (scitex)
103
+
104
+ ```python
105
+ # scitex/plt/__init__.py
106
+ import os
107
+
108
+ # Set branding BEFORE importing the external package
109
+ os.environ["FIGRECIPE_BRAND"] = "scitex.plt"
110
+ os.environ["FIGRECIPE_ALIAS"] = "plt"
111
+
112
+ # Now import - docstrings will show scitex.plt instead of figrecipe
113
+ from figrecipe import *
114
+ ```
115
+
116
+ ## Port Scheme
117
+
118
+ SciTeX uses port scheme 3129X (TEX → te-ku-su → 2-9-3 in Japanese):
119
+
120
+ | Port | Service |
121
+ |-------|------------------|
122
+ | 31290 | scitex-cloud |
123
+ | 31291 | crossref-local |
124
+ | 31292 | openalex |
125
+ | 31293 | scitex-audio |
126
+
127
+ ## Environment Variable Pattern
128
+
129
+ External packages should use `{ENV_PREFIX}_{SETTING}`:
130
+
131
+ ```
132
+ SCITEX_PLT_MODE=local
133
+ CROSSREF_LOCAL_API_URL=http://localhost:8333
134
+ SCITEX_AUDIO_RELAY_URL=http://localhost:31293
135
+ ```
136
+
137
+ ## Example: crossref-local
138
+
139
+ See GitHub Issue: https://github.com/ywatanabe1989/crossref-local/issues/11
140
+
141
+ The crossref-local package should implement:
142
+ - `CROSSREF_LOCAL_BRAND` / `CROSSREF_LOCAL_ALIAS` env vars
143
+ - Dynamic `ENV_PREFIX` derived from brand name
144
+ - When used via scitex.scholar, shows `scitex.scholar` in docs
145
+
146
+ ## References
147
+
148
+ - figrecipe/_branding.py - Reference implementation
149
+ - scitex/audio/_branding.py - Simple internal module (no rebranding needed)