scitex 2.16.0__py3-none-any.whl → 2.16.2__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 (101) hide show
  1. scitex/_mcp_tools/audio.py +11 -65
  2. scitex/audio/README.md +40 -12
  3. scitex/audio/__init__.py +27 -235
  4. scitex/audio/_audio_check.py +93 -0
  5. scitex/audio/_mcp/speak_handlers.py +56 -8
  6. scitex/audio/_speak.py +295 -0
  7. scitex/audio/mcp_server.py +98 -73
  8. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
  9. scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
  10. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
  11. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
  12. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
  13. scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
  14. scitex/social/__init__.py +1 -24
  15. scitex/writer/README.md +25 -409
  16. scitex/writer/__init__.py +98 -13
  17. {scitex-2.16.0.dist-info → scitex-2.16.2.dist-info}/METADATA +6 -1
  18. {scitex-2.16.0.dist-info → scitex-2.16.2.dist-info}/RECORD +21 -93
  19. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
  20. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
  21. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
  22. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
  23. scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
  24. scitex/scholar/data/.gitkeep +0 -0
  25. scitex/scholar/data/README.md +0 -44
  26. scitex/scholar/data/bib_files/bibliography.bib +0 -1952
  27. scitex/scholar/data/bib_files/neurovista.bib +0 -277
  28. scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
  29. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
  30. scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
  31. scitex/scholar/data/bib_files/openaccess.bib +0 -89
  32. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
  33. scitex/scholar/data/bib_files/pac.bib +0 -698
  34. scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
  35. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  36. scitex/scholar/data/bib_files/pac_titles.txt +0 -75
  37. scitex/scholar/data/bib_files/paywalled.bib +0 -98
  38. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
  39. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
  40. scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
  41. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  42. scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
  43. scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
  44. scitex/scholar/data/bib_files/test_seizure.bib +0 -46
  45. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  46. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  47. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  48. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  49. scitex/scholar/data/impact_factor.db +0 -0
  50. scitex/writer/Writer.py +0 -487
  51. scitex/writer/_clone_writer_project.py +0 -160
  52. scitex/writer/_compile/__init__.py +0 -41
  53. scitex/writer/_compile/_compile_async.py +0 -130
  54. scitex/writer/_compile/_compile_unified.py +0 -148
  55. scitex/writer/_compile/_parser.py +0 -63
  56. scitex/writer/_compile/_runner.py +0 -457
  57. scitex/writer/_compile/_validator.py +0 -46
  58. scitex/writer/_compile/manuscript.py +0 -110
  59. scitex/writer/_compile/revision.py +0 -82
  60. scitex/writer/_compile/supplementary.py +0 -100
  61. scitex/writer/_dataclasses/__init__.py +0 -44
  62. scitex/writer/_dataclasses/config/_CONSTANTS.py +0 -46
  63. scitex/writer/_dataclasses/config/_WriterConfig.py +0 -175
  64. scitex/writer/_dataclasses/config/__init__.py +0 -9
  65. scitex/writer/_dataclasses/contents/_ManuscriptContents.py +0 -236
  66. scitex/writer/_dataclasses/contents/_RevisionContents.py +0 -136
  67. scitex/writer/_dataclasses/contents/_SupplementaryContents.py +0 -114
  68. scitex/writer/_dataclasses/contents/__init__.py +0 -9
  69. scitex/writer/_dataclasses/core/_Document.py +0 -146
  70. scitex/writer/_dataclasses/core/_DocumentSection.py +0 -546
  71. scitex/writer/_dataclasses/core/__init__.py +0 -7
  72. scitex/writer/_dataclasses/results/_CompilationResult.py +0 -165
  73. scitex/writer/_dataclasses/results/_LaTeXIssue.py +0 -102
  74. scitex/writer/_dataclasses/results/_SaveSectionsResponse.py +0 -118
  75. scitex/writer/_dataclasses/results/_SectionReadResponse.py +0 -131
  76. scitex/writer/_dataclasses/results/__init__.py +0 -11
  77. scitex/writer/_dataclasses/tree/MINIMUM_FILES.md +0 -121
  78. scitex/writer/_dataclasses/tree/_ConfigTree.py +0 -86
  79. scitex/writer/_dataclasses/tree/_ManuscriptTree.py +0 -84
  80. scitex/writer/_dataclasses/tree/_RevisionTree.py +0 -97
  81. scitex/writer/_dataclasses/tree/_ScriptsTree.py +0 -118
  82. scitex/writer/_dataclasses/tree/_SharedTree.py +0 -100
  83. scitex/writer/_dataclasses/tree/_SupplementaryTree.py +0 -101
  84. scitex/writer/_dataclasses/tree/__init__.py +0 -23
  85. scitex/writer/_mcp/__init__.py +0 -4
  86. scitex/writer/_mcp/handlers.py +0 -32
  87. scitex/writer/_mcp/tool_schemas.py +0 -33
  88. scitex/writer/_project/__init__.py +0 -29
  89. scitex/writer/_project/_create.py +0 -89
  90. scitex/writer/_project/_trees.py +0 -63
  91. scitex/writer/_project/_validate.py +0 -61
  92. scitex/writer/utils/.legacy_git_retry.py +0 -164
  93. scitex/writer/utils/__init__.py +0 -24
  94. scitex/writer/utils/_converters.py +0 -635
  95. scitex/writer/utils/_parse_latex_logs.py +0 -138
  96. scitex/writer/utils/_parse_script_args.py +0 -156
  97. scitex/writer/utils/_verify_tree_structure.py +0 -205
  98. scitex/writer/utils/_watch.py +0 -96
  99. {scitex-2.16.0.dist-info → scitex-2.16.2.dist-info}/WHEEL +0 -0
  100. {scitex-2.16.0.dist-info → scitex-2.16.2.dist-info}/entry_points.txt +0 -0
  101. {scitex-2.16.0.dist-info → scitex-2.16.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,138 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # Timestamp: "2025-10-28 17:11:22 (ywatanabe)"
4
- # File: /home/ywatanabe/proj/scitex-code/src/scitex/writer/parse_latex.py
5
- # ----------------------------------------
6
- from __future__ import annotations
7
- import os
8
-
9
- __FILE__ = "./src/scitex/writer/parse_latex.py"
10
- __DIR__ = os.path.dirname(__FILE__)
11
- # ----------------------------------------
12
-
13
- """
14
- LaTeX error and warning parsing from compilation output.
15
-
16
- Simple parsing of LaTeX errors and warnings from stdout/stderr.
17
- """
18
-
19
- from pathlib import Path
20
- from typing import List
21
- from typing import Tuple
22
-
23
- from .._dataclasses import LaTeXIssue
24
-
25
-
26
- def parse_compilation_output(
27
- output: str, log_file: Path = None
28
- ) -> Tuple[List[LaTeXIssue], List[LaTeXIssue]]:
29
- """
30
- Parse errors and warnings from compilation output.
31
-
32
- Args:
33
- output: Compilation output (stdout + stderr)
34
- log_file: Optional path to .log file (unused, for compatibility)
35
-
36
- Returns:
37
- Tuple of (error_issues, warning_issues)
38
- """
39
- errors = []
40
- warnings = []
41
-
42
- for line in output.split("\n"):
43
- # LaTeX error pattern: "! Error message"
44
- if line.startswith("!"):
45
- error_text = line[1:].strip()
46
- if error_text:
47
- errors.append(LaTeXIssue(type="error", message=error_text))
48
-
49
- # LaTeX warning pattern
50
- elif "warning" in line.lower():
51
- warnings.append(LaTeXIssue(type="warning", message=line.strip()))
52
-
53
- return errors, warnings
54
-
55
-
56
- def run_session() -> None:
57
- """Initialize scitex framework, run main function, and cleanup."""
58
- global CONFIG, CC, sys, plt, rng
59
- import sys
60
- import matplotlib.pyplot as plt
61
- import scitex as stx
62
-
63
- args = parse_args()
64
-
65
- CONFIG, sys.stdout, sys.stderr, plt, CC, rng = stx.session.start(
66
- sys,
67
- plt,
68
- args=args,
69
- file=__FILE__,
70
- sdir_suffix=None,
71
- verbose=False,
72
- agg=True,
73
- )
74
-
75
- exit_status = main(args)
76
-
77
- stx.session.close(
78
- CONFIG,
79
- verbose=False,
80
- notify=False,
81
- message="",
82
- exit_status=exit_status,
83
- )
84
-
85
-
86
- def main(args):
87
- if args.file:
88
- with open(args.file) as f_f:
89
- output = f_f.read()
90
- else:
91
- output = args.text
92
-
93
- errors, warnings = parse_compilation_output(output)
94
-
95
- print(f"Errors: {len(errors)}")
96
- for err in errors:
97
- print(f" - {err}")
98
-
99
- print(f"Warnings: {len(warnings)}")
100
- for warn in warnings:
101
- print(f" - {warn}")
102
-
103
- return 0
104
-
105
-
106
- def parse_args():
107
- import argparse
108
-
109
- parser = argparse.ArgumentParser(
110
- description="Parse LaTeX compilation output for errors and warnings"
111
- )
112
- parser.add_argument(
113
- "--file",
114
- "-f",
115
- type=str,
116
- help="File containing compilation output",
117
- )
118
- parser.add_argument(
119
- "--text",
120
- "-t",
121
- type=str,
122
- help="Compilation output text",
123
- )
124
-
125
- return parser.parse_args()
126
-
127
-
128
- if __name__ == "__main__":
129
- run_session()
130
-
131
-
132
- __all__ = [
133
- "parse_compilation_output",
134
- ]
135
-
136
- # python -m scitex.writer.utils._parse_latex_logs --file compilation.log
137
-
138
- # EOF
@@ -1,156 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # Timestamp: "2025-10-28 17:30:00 (ywatanabe)"
4
- # File: /home/ywatanabe/proj/scitex-code/src/scitex/writer/_parse_script_args.py
5
- # ----------------------------------------
6
- from __future__ import annotations
7
- import os
8
-
9
- __FILE__ = "./src/scitex/writer/_parse_script_args.py"
10
- __DIR__ = os.path.dirname(__FILE__)
11
- # ----------------------------------------
12
-
13
- """
14
- Script argument parser - extract available arguments from shell scripts.
15
-
16
- Parses shell scripts to extract documented arguments from usage() functions.
17
- """
18
-
19
- import re
20
- from pathlib import Path
21
- from dataclasses import dataclass
22
- from typing import Optional
23
-
24
-
25
- @dataclass
26
- class ScriptArgument:
27
- """Represents a single script argument."""
28
-
29
- short_flag: Optional[str] # e.g., "-nf"
30
- long_flag: Optional[str] # e.g., "--no_figs"
31
- description: str # e.g., "Exclude figures for quick compilation"
32
- default: Optional[str] # e.g., "false"
33
-
34
- def __str__(self) -> str:
35
- """Format as help text."""
36
- flags = ", ".join(f for f in [self.short_flag, self.long_flag] if f)
37
- default_str = f" (default: {self.default})" if self.default else ""
38
- return f"{flags:20} {self.description}{default_str}"
39
-
40
-
41
- class ScriptArgumentParser:
42
- """Extract arguments from shell script usage() functions."""
43
-
44
- @staticmethod
45
- def parse(script_path: Path) -> list[ScriptArgument]:
46
- """
47
- Parse shell script to extract available arguments.
48
-
49
- Looks for usage() function pattern:
50
- -flag, --long-flag Description (default: value)
51
-
52
- Args:
53
- script_path: Path to shell script
54
-
55
- Returns:
56
- List of ScriptArgument objects
57
- """
58
- if not script_path.exists():
59
- return []
60
-
61
- content = script_path.read_text()
62
-
63
- # Find usage() function
64
- usage_match = re.search(r"usage\s*\(\)\s*{(.*?)exit\s+0", content, re.DOTALL)
65
-
66
- if not usage_match:
67
- return []
68
-
69
- usage_text = usage_match.group(1)
70
-
71
- # Extract arguments from usage text (contains echo statements)
72
- # Find all lines with option flags (start with "echo" and contain flags)
73
- options_text = usage_text
74
- args = []
75
-
76
- # Parse each line in usage (will contain echo statements)
77
- for line in options_text.split("\n"):
78
- # Extract content between quotes in echo statements
79
- quote_match = re.search(r'"([^"]*)"', line)
80
- if not quote_match:
81
- continue
82
-
83
- line_content = quote_match.group(1).strip()
84
- if not line_content or line_content.startswith("#"):
85
- continue
86
-
87
- # Skip lines without flags
88
- if "-" not in line_content:
89
- continue
90
-
91
- arg = ScriptArgumentParser._parse_argument_line(line_content)
92
- if arg:
93
- args.append(arg)
94
-
95
- return args
96
-
97
- @staticmethod
98
- def _parse_argument_line(line: str) -> Optional[ScriptArgument]:
99
- """
100
- Parse single argument line.
101
-
102
- Format: "-nf, --no_figs Description (default: value)"
103
- """
104
- # Extract flags and description
105
- flags_match = re.match(r"(.*?)\s{2,}(.*)", line)
106
- if not flags_match:
107
- return None
108
-
109
- flags_str = flags_match.group(1).strip()
110
- rest = flags_match.group(2).strip()
111
-
112
- # Parse flags
113
- short_flag = None
114
- long_flag = None
115
-
116
- if "," in flags_str:
117
- parts = flags_str.split(",")
118
- short_flag = parts[0].strip() if parts[0].strip() else None
119
- long_flag = (
120
- parts[1].strip() if len(parts) > 1 and parts[1].strip() else None
121
- )
122
- else:
123
- # Single flag (short or long)
124
- if flags_str.startswith("--"):
125
- long_flag = flags_str
126
- elif flags_str.startswith("-"):
127
- short_flag = flags_str
128
- else:
129
- return None
130
-
131
- # Parse description and default
132
- description = rest
133
- default = None
134
-
135
- default_match = re.search(r"\(default:\s*([^)]+)\)", rest)
136
- if default_match:
137
- default = default_match.group(1).strip()
138
- description = rest[: default_match.start()].strip()
139
-
140
- if not description:
141
- return None
142
-
143
- return ScriptArgument(
144
- short_flag=short_flag,
145
- long_flag=long_flag,
146
- description=description,
147
- default=default,
148
- )
149
-
150
-
151
- __all__ = [
152
- "ScriptArgument",
153
- "ScriptArgumentParser",
154
- ]
155
-
156
- # EOF
@@ -1,205 +0,0 @@
1
- #!/usr/bin/env python3
2
- # Timestamp: "2025-10-29 06:13:05 (ywatanabe)"
3
- # File: /home/ywatanabe/proj/scitex-code/src/scitex/writer/_verify_tree_structure.py
4
- # ----------------------------------------
5
- from __future__ import annotations
6
-
7
- import os
8
-
9
- __FILE__ = "./src/scitex/writer/_verify_tree_structure.py"
10
- __DIR__ = os.path.dirname(__FILE__)
11
- # ----------------------------------------
12
-
13
- import argparse
14
-
15
- """Project structure validation for writer module.
16
-
17
- Leverages dataclass verify_structure() methods for validation."""
18
-
19
- from pathlib import Path
20
-
21
- from scitex.logging import getLogger
22
-
23
- from .._dataclasses import (
24
- ConfigTree,
25
- ManuscriptTree,
26
- RevisionTree,
27
- ScriptsTree,
28
- SharedTree,
29
- SupplementaryTree,
30
- )
31
-
32
- logger = getLogger(__name__)
33
-
34
- # Parameters
35
- TREE_VALIDATORS = {
36
- "config": {"dir_name": "config", "tree_class": ConfigTree},
37
- "00_shared": {"dir_name": "00_shared", "tree_class": SharedTree},
38
- "01_manuscript": {
39
- "dir_name": "01_manuscript",
40
- "tree_class": ManuscriptTree,
41
- },
42
- "02_supplementary": {
43
- "dir_name": "02_supplementary",
44
- "tree_class": SupplementaryTree,
45
- },
46
- "03_revision": {"dir_name": "03_revision", "tree_class": RevisionTree},
47
- "scripts": {"dir_name": "scripts", "tree_class": ScriptsTree},
48
- }
49
-
50
-
51
- # Exception classes
52
- class ProjectValidationError(Exception):
53
- """Raised when project structure is invalid."""
54
-
55
- pass
56
-
57
-
58
- # 2. Public validation functions
59
- def verify_tree_structure(
60
- project_dir: Path, func_name="validate_tree_structures"
61
- ) -> None:
62
- """Validates all tree structures in the project directory."""
63
- logger.info(
64
- f"{func_name}: Validating tree structures: {Path(project_dir).absolute()}..."
65
- )
66
- project_dir = Path(project_dir)
67
- for dir_name, (dir_path, tree_class) in TREE_VALIDATORS.items():
68
- validator_func_name = f"_validate_{dir_name}_structure"
69
- eval(validator_func_name)(project_dir)
70
- logger.success(
71
- f"{func_name}: Validated tree structures: {Path(project_dir).absolute()}"
72
- )
73
- return True
74
-
75
-
76
- # 3. Internal validation functions
77
- def _validate_01_manuscript_structure(project_dir: Path) -> bool:
78
- """Validates manuscript structure."""
79
- return _validate_tree_structure_base(
80
- project_dir, **TREE_VALIDATORS["01_manuscript"]
81
- )
82
-
83
-
84
- def _validate_02_supplementary_structure(project_dir: Path) -> bool:
85
- """Validates supplementary structure."""
86
- return _validate_tree_structure_base(
87
- project_dir, **TREE_VALIDATORS["02_supplementary"]
88
- )
89
-
90
-
91
- def _validate_03_revision_structure(project_dir: Path) -> bool:
92
- """Validates revision structure."""
93
- return _validate_tree_structure_base(project_dir, **TREE_VALIDATORS["03_revision"])
94
-
95
-
96
- def _validate_config_structure(project_dir: Path) -> bool:
97
- """Validates config structure."""
98
- return _validate_tree_structure_base(project_dir, **TREE_VALIDATORS["config"])
99
-
100
-
101
- def _validate_scripts_structure(project_dir: Path) -> bool:
102
- """Validates scripts structure."""
103
- return _validate_tree_structure_base(project_dir, **TREE_VALIDATORS["scripts"])
104
-
105
-
106
- def _validate_00_shared_structure(project_dir: Path) -> bool:
107
- """Validates shared structure."""
108
- return _validate_tree_structure_base(project_dir, **TREE_VALIDATORS["00_shared"])
109
-
110
-
111
- # 4. Helper functions
112
- def _validate_tree_structure_base(
113
- project_dir: Path, dir_name: str, tree_class: type = None
114
- ) -> bool:
115
- """Base validation function that checks directory existence and verifies structure using tree class.
116
-
117
- Args:
118
- project_dir: Root project directory
119
- dir_name: Name of directory to validate
120
- tree_class: Tree class with verify_structure method
121
-
122
- Returns
123
- -------
124
- True if structure is valid
125
-
126
- Raises
127
- ------
128
- ProjectValidationError: If directory missing or structure invalid
129
- """
130
- project_dir = Path(project_dir)
131
- target_dir = project_dir / dir_name
132
- if not target_dir.exists():
133
- raise ProjectValidationError(f"Required directory missing: {target_dir}")
134
- if tree_class is not None:
135
- doc = tree_class(target_dir, git_root=project_dir)
136
- is_valid, issues = doc.verify_structure()
137
- if not is_valid:
138
- raise ProjectValidationError(
139
- f"{dir_name} structure invalid:\n"
140
- + "\n".join(f" - {issue}" for issue in issues)
141
- )
142
- logger.debug(f"{dir_name} structure valid: {project_dir}")
143
- return True
144
-
145
-
146
- # 1. Main entry point
147
- def run_session() -> None:
148
- """Initialize scitex framework, run main function, and cleanup."""
149
- global CONFIG, CC, sys, plt, rng
150
- import sys
151
-
152
- import matplotlib.pyplot as plt
153
-
154
- import scitex as stx
155
-
156
- args = parse_args()
157
-
158
- CONFIG, sys.stdout, sys.stderr, plt, CC, rng = stx.session.start(
159
- sys,
160
- plt,
161
- args=args,
162
- file=__FILE__,
163
- sdir_suffix=None,
164
- verbose=False,
165
- agg=True,
166
- )
167
-
168
- exit_status = main(args)
169
-
170
- stx.session.close(
171
- CONFIG,
172
- verbose=False,
173
- notify=False,
174
- message="",
175
- exit_status=exit_status,
176
- )
177
-
178
-
179
- def main(args):
180
- project_dir = Path(args.dir) if args.dir else Path.cwd()
181
- verify_tree_structure(project_dir)
182
- return 0
183
-
184
-
185
- def parse_args() -> argparse.Namespace:
186
- parser = argparse.ArgumentParser(
187
- description="Validate scitex writer project structure"
188
- )
189
- parser.add_argument(
190
- "--dir",
191
- "-d",
192
- type=str,
193
- default=None,
194
- help="Project directory to validate (default: current directory)",
195
- )
196
- args = parser.parse_args()
197
- return args
198
-
199
-
200
- if __name__ == "__main__":
201
- run_session()
202
-
203
- # python -m scitex.writer._verify_tree_structure --dir ./my_paper
204
-
205
- # EOF
@@ -1,96 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # File: /home/ywatanabe/proj/scitex-code/src/scitex/writer/watch.py
4
-
5
- """
6
- Watch mode for auto-recompilation.
7
-
8
- Monitors file changes and triggers automatic recompilation.
9
- """
10
-
11
- import subprocess
12
- from pathlib import Path
13
- from typing import Optional, Callable
14
-
15
- from scitex.logging import getLogger
16
-
17
- logger = getLogger(__name__)
18
-
19
-
20
- def watch_manuscript(
21
- project_dir: Path,
22
- interval: int = 2,
23
- on_compile: Optional[Callable] = None,
24
- timeout: Optional[int] = None,
25
- ) -> None:
26
- """
27
- Watch and auto-recompile manuscript on file changes.
28
-
29
- Args:
30
- project_dir: Path to writer project directory
31
- interval: Check interval in seconds
32
- on_compile: Callback function called after each compilation
33
- timeout: Optional timeout in seconds (None = infinite)
34
-
35
- Examples:
36
- >>> from pathlib import Path
37
- >>> def on_change():
38
- ... print("Recompiled!")
39
- >>> watch_manuscript(Path("/path/to/project"), on_compile=on_change)
40
- """
41
- # Get compile script from project directory
42
- compile_script = project_dir / "compile"
43
-
44
- if not compile_script.exists():
45
- logger.error(f"compile script not found: {compile_script}")
46
- return
47
-
48
- # Build watch command
49
- cmd = [str(compile_script), "-m", "-w"]
50
-
51
- logger.info(f"Starting watch mode for {project_dir}")
52
- logger.info("Press Ctrl+C to stop")
53
-
54
- try:
55
- # Run watch script
56
- process = subprocess.Popen(
57
- cmd,
58
- cwd=project_dir,
59
- stdout=subprocess.PIPE,
60
- stderr=subprocess.STDOUT,
61
- text=True,
62
- bufsize=1, # Line-buffered
63
- )
64
-
65
- # Stream output
66
- for line in iter(process.stdout.readline, ""):
67
- if line:
68
- print(line.rstrip())
69
-
70
- # Call callback on compilation events
71
- if on_compile and "Compilation" in line:
72
- try:
73
- on_compile()
74
- except Exception as e:
75
- logger.error(f"Callback error: {e}")
76
-
77
- process.wait(timeout=timeout)
78
-
79
- except KeyboardInterrupt:
80
- logger.info("\nWatch mode stopped by user")
81
- if process:
82
- process.terminate()
83
- try:
84
- process.wait(timeout=5)
85
- except subprocess.TimeoutExpired:
86
- process.kill()
87
-
88
- except Exception as e:
89
- logger.error(f"Watch mode error: {e}")
90
- if process:
91
- process.terminate()
92
-
93
-
94
- __all__ = ["watch_manuscript"]
95
-
96
- # EOF