scitex 2.16.0__py3-none-any.whl → 2.16.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.
- scitex/_mcp_tools/audio.py +11 -65
- scitex/audio/README.md +40 -12
- scitex/audio/__init__.py +27 -235
- scitex/audio/_audio_check.py +93 -0
- scitex/audio/_mcp/speak_handlers.py +56 -8
- scitex/audio/_speak.py +295 -0
- scitex/audio/mcp_server.py +98 -73
- scitex/social/__init__.py +1 -24
- scitex/writer/README.md +25 -409
- scitex/writer/__init__.py +98 -13
- {scitex-2.16.0.dist-info → scitex-2.16.1.dist-info}/METADATA +6 -1
- {scitex-2.16.0.dist-info → scitex-2.16.1.dist-info}/RECORD +15 -62
- scitex/writer/Writer.py +0 -487
- scitex/writer/_clone_writer_project.py +0 -160
- scitex/writer/_compile/__init__.py +0 -41
- scitex/writer/_compile/_compile_async.py +0 -130
- scitex/writer/_compile/_compile_unified.py +0 -148
- scitex/writer/_compile/_parser.py +0 -63
- scitex/writer/_compile/_runner.py +0 -457
- scitex/writer/_compile/_validator.py +0 -46
- scitex/writer/_compile/manuscript.py +0 -110
- scitex/writer/_compile/revision.py +0 -82
- scitex/writer/_compile/supplementary.py +0 -100
- scitex/writer/_dataclasses/__init__.py +0 -44
- scitex/writer/_dataclasses/config/_CONSTANTS.py +0 -46
- scitex/writer/_dataclasses/config/_WriterConfig.py +0 -175
- scitex/writer/_dataclasses/config/__init__.py +0 -9
- scitex/writer/_dataclasses/contents/_ManuscriptContents.py +0 -236
- scitex/writer/_dataclasses/contents/_RevisionContents.py +0 -136
- scitex/writer/_dataclasses/contents/_SupplementaryContents.py +0 -114
- scitex/writer/_dataclasses/contents/__init__.py +0 -9
- scitex/writer/_dataclasses/core/_Document.py +0 -146
- scitex/writer/_dataclasses/core/_DocumentSection.py +0 -546
- scitex/writer/_dataclasses/core/__init__.py +0 -7
- scitex/writer/_dataclasses/results/_CompilationResult.py +0 -165
- scitex/writer/_dataclasses/results/_LaTeXIssue.py +0 -102
- scitex/writer/_dataclasses/results/_SaveSectionsResponse.py +0 -118
- scitex/writer/_dataclasses/results/_SectionReadResponse.py +0 -131
- scitex/writer/_dataclasses/results/__init__.py +0 -11
- scitex/writer/_dataclasses/tree/MINIMUM_FILES.md +0 -121
- scitex/writer/_dataclasses/tree/_ConfigTree.py +0 -86
- scitex/writer/_dataclasses/tree/_ManuscriptTree.py +0 -84
- scitex/writer/_dataclasses/tree/_RevisionTree.py +0 -97
- scitex/writer/_dataclasses/tree/_ScriptsTree.py +0 -118
- scitex/writer/_dataclasses/tree/_SharedTree.py +0 -100
- scitex/writer/_dataclasses/tree/_SupplementaryTree.py +0 -101
- scitex/writer/_dataclasses/tree/__init__.py +0 -23
- scitex/writer/_mcp/__init__.py +0 -4
- scitex/writer/_mcp/handlers.py +0 -32
- scitex/writer/_mcp/tool_schemas.py +0 -33
- scitex/writer/_project/__init__.py +0 -29
- scitex/writer/_project/_create.py +0 -89
- scitex/writer/_project/_trees.py +0 -63
- scitex/writer/_project/_validate.py +0 -61
- scitex/writer/utils/.legacy_git_retry.py +0 -164
- scitex/writer/utils/__init__.py +0 -24
- scitex/writer/utils/_converters.py +0 -635
- scitex/writer/utils/_parse_latex_logs.py +0 -138
- scitex/writer/utils/_parse_script_args.py +0 -156
- scitex/writer/utils/_verify_tree_structure.py +0 -205
- scitex/writer/utils/_watch.py +0 -96
- {scitex-2.16.0.dist-info → scitex-2.16.1.dist-info}/WHEEL +0 -0
- {scitex-2.16.0.dist-info → scitex-2.16.1.dist-info}/entry_points.txt +0 -0
- {scitex-2.16.0.dist-info → scitex-2.16.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,546 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
# Timestamp: "2025-10-29 06:08:40 (ywatanabe)"
|
|
4
|
-
# File: /home/ywatanabe/proj/scitex-code/src/scitex/writer/dataclasses/_DocumentSection.py
|
|
5
|
-
# ----------------------------------------
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
import os
|
|
8
|
-
|
|
9
|
-
__FILE__ = "./src/scitex/writer/dataclasses/_DocumentSection.py"
|
|
10
|
-
__DIR__ = os.path.dirname(__FILE__)
|
|
11
|
-
# ----------------------------------------
|
|
12
|
-
|
|
13
|
-
"""
|
|
14
|
-
DocumentSection - wrapper for document file with git-backed version control.
|
|
15
|
-
|
|
16
|
-
Provides intuitive version control API while leveraging git internally.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
from typing import Optional
|
|
21
|
-
import subprocess
|
|
22
|
-
|
|
23
|
-
from scitex.logging import getLogger
|
|
24
|
-
|
|
25
|
-
logger = getLogger(__name__)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class DocumentSection:
|
|
29
|
-
"""
|
|
30
|
-
Wrapper for document section file with git-backed version control.
|
|
31
|
-
|
|
32
|
-
Provides simple version control API while leveraging git internally:
|
|
33
|
-
- Users get intuitive .read(), .write(), .save(), .history(), .diff()
|
|
34
|
-
- We maintain clean separation from git complexity
|
|
35
|
-
- Enables advanced users to use git directly when needed
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
def __init__(self, path: Path, git_root: Optional[Path] = None):
|
|
39
|
-
"""
|
|
40
|
-
Initialize with file path and optional git root.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
path: Path to the document file
|
|
44
|
-
git_root: Path to git repository root (for efficiency)
|
|
45
|
-
"""
|
|
46
|
-
self.path = path
|
|
47
|
-
self._git_root = git_root
|
|
48
|
-
self._cached_git_root = None
|
|
49
|
-
|
|
50
|
-
@property
|
|
51
|
-
def git_root(self) -> Optional[Path]:
|
|
52
|
-
"""Get cached git root, finding it if needed."""
|
|
53
|
-
if self._git_root is not None:
|
|
54
|
-
return self._git_root
|
|
55
|
-
if self._cached_git_root is None:
|
|
56
|
-
self._cached_git_root = self._find_git_root()
|
|
57
|
-
return self._cached_git_root
|
|
58
|
-
|
|
59
|
-
@staticmethod
|
|
60
|
-
def _find_git_root(start_path: Path = None) -> Optional[Path]:
|
|
61
|
-
"""Find git root by walking up directory tree."""
|
|
62
|
-
if start_path is None:
|
|
63
|
-
start_path = Path.cwd()
|
|
64
|
-
current = start_path.absolute()
|
|
65
|
-
while current != current.parent:
|
|
66
|
-
if (current / ".git").exists():
|
|
67
|
-
return current
|
|
68
|
-
current = current.parent
|
|
69
|
-
return None
|
|
70
|
-
|
|
71
|
-
def read(self):
|
|
72
|
-
"""Read file contents with intelligent fallback strategy."""
|
|
73
|
-
if not self.path.exists():
|
|
74
|
-
logger.warning(f"File does not exist: {self.path}")
|
|
75
|
-
return None
|
|
76
|
-
|
|
77
|
-
try:
|
|
78
|
-
import scitex.io as stx_io
|
|
79
|
-
|
|
80
|
-
return stx_io.load(str(self.path))
|
|
81
|
-
except ImportError:
|
|
82
|
-
logger.debug("scitex.io not available, using plain text reader")
|
|
83
|
-
return self._read_plain_text()
|
|
84
|
-
except ValueError as e:
|
|
85
|
-
logger.warning(
|
|
86
|
-
f"scitex.io could not parse {self.path} ({e}), "
|
|
87
|
-
"falling back to plain text"
|
|
88
|
-
)
|
|
89
|
-
return self._read_plain_text()
|
|
90
|
-
except Exception as e:
|
|
91
|
-
logger.error(f"Unexpected error reading {self.path}: {e}", exc_info=True)
|
|
92
|
-
return None
|
|
93
|
-
|
|
94
|
-
def _read_plain_text(self):
|
|
95
|
-
"""Read file as plain text with proper encoding handling."""
|
|
96
|
-
try:
|
|
97
|
-
return self.path.read_text(encoding="utf-8")
|
|
98
|
-
except UnicodeDecodeError:
|
|
99
|
-
logger.warning(f"UTF-8 decode failed for {self.path}, trying latin-1")
|
|
100
|
-
return self.path.read_text(encoding="latin-1")
|
|
101
|
-
except Exception as e:
|
|
102
|
-
logger.error(f"Failed to read {self.path} as text: {e}")
|
|
103
|
-
return None
|
|
104
|
-
|
|
105
|
-
def write(self, content) -> bool:
|
|
106
|
-
"""Write content to file."""
|
|
107
|
-
try:
|
|
108
|
-
if isinstance(content, (list, tuple)):
|
|
109
|
-
# Join lines if content is a list
|
|
110
|
-
text = "\n".join(str(line) for line in content)
|
|
111
|
-
else:
|
|
112
|
-
text = str(content)
|
|
113
|
-
self.path.write_text(text)
|
|
114
|
-
return True
|
|
115
|
-
except Exception as e:
|
|
116
|
-
logger.error(f"Failed to write {self.path}: {e}")
|
|
117
|
-
return False
|
|
118
|
-
|
|
119
|
-
def history(self) -> list:
|
|
120
|
-
"""Get version history (uses git log internally)."""
|
|
121
|
-
if not self.git_root:
|
|
122
|
-
logger.debug(f"No git repository for {self.path}")
|
|
123
|
-
return []
|
|
124
|
-
|
|
125
|
-
try:
|
|
126
|
-
rel_path = self.path.relative_to(self.git_root)
|
|
127
|
-
|
|
128
|
-
result = subprocess.run(
|
|
129
|
-
["git", "log", "--oneline", str(rel_path)],
|
|
130
|
-
cwd=self.git_root,
|
|
131
|
-
capture_output=True,
|
|
132
|
-
text=True,
|
|
133
|
-
timeout=5,
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
if result.returncode != 0:
|
|
137
|
-
logger.debug(f"Git log failed: {result.stderr}")
|
|
138
|
-
return []
|
|
139
|
-
|
|
140
|
-
return result.stdout.strip().split("\n") if result.stdout.strip() else []
|
|
141
|
-
except subprocess.TimeoutExpired:
|
|
142
|
-
logger.warning(f"Git log timed out for {self.path}")
|
|
143
|
-
return []
|
|
144
|
-
except Exception as e:
|
|
145
|
-
logger.error(f"Error getting history for {self.path}: {e}")
|
|
146
|
-
return []
|
|
147
|
-
|
|
148
|
-
def diff(self, ref: str = "HEAD") -> str:
|
|
149
|
-
"""Get diff against git reference (default: HEAD)."""
|
|
150
|
-
if not self.git_root:
|
|
151
|
-
logger.debug(f"No git repository for {self.path}")
|
|
152
|
-
return ""
|
|
153
|
-
|
|
154
|
-
try:
|
|
155
|
-
rel_path = self.path.relative_to(self.git_root)
|
|
156
|
-
|
|
157
|
-
result = subprocess.run(
|
|
158
|
-
["git", "diff", ref, str(rel_path)],
|
|
159
|
-
cwd=self.git_root,
|
|
160
|
-
capture_output=True,
|
|
161
|
-
text=True,
|
|
162
|
-
timeout=5,
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
return result.stdout if result.returncode == 0 else ""
|
|
166
|
-
except subprocess.TimeoutExpired:
|
|
167
|
-
logger.warning(f"Git diff timed out for {self.path}")
|
|
168
|
-
return ""
|
|
169
|
-
except Exception as e:
|
|
170
|
-
logger.error(f"Error getting diff for {self.path}: {e}")
|
|
171
|
-
return ""
|
|
172
|
-
|
|
173
|
-
def diff_between(self, ref1: str, ref2: str) -> str:
|
|
174
|
-
"""
|
|
175
|
-
Compare two arbitrary git references.
|
|
176
|
-
|
|
177
|
-
Args:
|
|
178
|
-
ref1: First git reference (commit, branch, tag, or human-readable spec)
|
|
179
|
-
ref2: Second git reference (commit, branch, tag, or human-readable spec)
|
|
180
|
-
|
|
181
|
-
Returns:
|
|
182
|
-
Diff output string, or "" if error or no differences.
|
|
183
|
-
|
|
184
|
-
Examples:
|
|
185
|
-
section.diff_between("HEAD~2", "HEAD")
|
|
186
|
-
section.diff_between("v1.0", "v2.0")
|
|
187
|
-
section.diff_between("main", "develop")
|
|
188
|
-
section.diff_between("2 days ago", "now")
|
|
189
|
-
"""
|
|
190
|
-
if not self.git_root:
|
|
191
|
-
logger.debug(f"No git repository for {self.path}")
|
|
192
|
-
return ""
|
|
193
|
-
|
|
194
|
-
try:
|
|
195
|
-
# Resolve human-readable refs to commit hashes
|
|
196
|
-
resolved_ref1 = self._resolve_ref(ref1)
|
|
197
|
-
resolved_ref2 = self._resolve_ref(ref2)
|
|
198
|
-
|
|
199
|
-
if not resolved_ref1 or not resolved_ref2:
|
|
200
|
-
logger.error(f"Failed to resolve references: {ref1} or {ref2}")
|
|
201
|
-
return ""
|
|
202
|
-
|
|
203
|
-
rel_path = self.path.relative_to(self.git_root)
|
|
204
|
-
|
|
205
|
-
# Use git diff ref1..ref2 file (three-dot shows what changed on ref2 since ref1 diverged)
|
|
206
|
-
result = subprocess.run(
|
|
207
|
-
[
|
|
208
|
-
"git",
|
|
209
|
-
"diff",
|
|
210
|
-
f"{resolved_ref1}..{resolved_ref2}",
|
|
211
|
-
str(rel_path),
|
|
212
|
-
],
|
|
213
|
-
cwd=self.git_root,
|
|
214
|
-
capture_output=True,
|
|
215
|
-
text=True,
|
|
216
|
-
timeout=5,
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
return result.stdout if result.returncode == 0 else ""
|
|
220
|
-
except subprocess.TimeoutExpired:
|
|
221
|
-
logger.warning(f"Git diff timed out for {self.path}")
|
|
222
|
-
return ""
|
|
223
|
-
except Exception as e:
|
|
224
|
-
logger.error(f"Error getting diff_between for {self.path}: {e}")
|
|
225
|
-
return ""
|
|
226
|
-
|
|
227
|
-
def _resolve_ref(self, spec: str) -> Optional[str]:
|
|
228
|
-
"""
|
|
229
|
-
Resolve human-readable reference specification to git reference.
|
|
230
|
-
|
|
231
|
-
Handles:
|
|
232
|
-
- Standard git refs: HEAD, HEAD~N, branch, tag, commit hash
|
|
233
|
-
- Relative time: "N days ago", "N weeks ago", "N hours ago", "now"
|
|
234
|
-
- Absolute dates: "2025-10-28", "2025-10-28 14:30"
|
|
235
|
-
|
|
236
|
-
Args:
|
|
237
|
-
spec: Reference specification
|
|
238
|
-
|
|
239
|
-
Returns:
|
|
240
|
-
Git reference (commit hash or ref name), or None if invalid.
|
|
241
|
-
"""
|
|
242
|
-
if not self.git_root:
|
|
243
|
-
return None
|
|
244
|
-
|
|
245
|
-
spec = spec.strip()
|
|
246
|
-
|
|
247
|
-
# Direct git reference (HEAD, branch, tag, hash)
|
|
248
|
-
if self._is_valid_git_ref(spec):
|
|
249
|
-
return spec
|
|
250
|
-
|
|
251
|
-
# Handle "now" as HEAD
|
|
252
|
-
if spec.lower() == "now":
|
|
253
|
-
return "HEAD"
|
|
254
|
-
|
|
255
|
-
# Handle "today" as start of day
|
|
256
|
-
if spec.lower() == "today":
|
|
257
|
-
from datetime import datetime, timedelta
|
|
258
|
-
|
|
259
|
-
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
260
|
-
return self._find_commit_at_timestamp(today)
|
|
261
|
-
|
|
262
|
-
# Handle relative time like "2 days ago", "1 week ago", "24 hours ago"
|
|
263
|
-
time_ref = self._parse_relative_time(spec)
|
|
264
|
-
if time_ref:
|
|
265
|
-
return self._find_commit_at_timestamp(time_ref)
|
|
266
|
-
|
|
267
|
-
# Handle absolute date like "2025-10-28" or "2025-10-28 14:30"
|
|
268
|
-
date_ref = self._parse_absolute_date(spec)
|
|
269
|
-
if date_ref:
|
|
270
|
-
return self._find_commit_at_timestamp(date_ref)
|
|
271
|
-
|
|
272
|
-
logger.warning(f"Could not resolve reference: {spec}")
|
|
273
|
-
return None
|
|
274
|
-
|
|
275
|
-
def _is_valid_git_ref(self, ref: str) -> bool:
|
|
276
|
-
"""Check if reference exists in git repository."""
|
|
277
|
-
if not self.git_root:
|
|
278
|
-
return False
|
|
279
|
-
|
|
280
|
-
try:
|
|
281
|
-
result = subprocess.run(
|
|
282
|
-
["git", "rev-parse", "--verify", ref],
|
|
283
|
-
cwd=self.git_root,
|
|
284
|
-
capture_output=True,
|
|
285
|
-
timeout=2,
|
|
286
|
-
)
|
|
287
|
-
return result.returncode == 0
|
|
288
|
-
except Exception:
|
|
289
|
-
return False
|
|
290
|
-
|
|
291
|
-
def _parse_relative_time(self, spec: str):
|
|
292
|
-
"""
|
|
293
|
-
Parse relative time specification like "2 days ago".
|
|
294
|
-
|
|
295
|
-
Returns:
|
|
296
|
-
datetime object or None if not a valid time spec.
|
|
297
|
-
"""
|
|
298
|
-
import re
|
|
299
|
-
from datetime import datetime, timedelta
|
|
300
|
-
|
|
301
|
-
# Pattern: "N <unit> ago"
|
|
302
|
-
match = re.match(r"(\d+)\s*(day|week|hour|minute)s?\s*ago", spec, re.IGNORECASE)
|
|
303
|
-
if not match:
|
|
304
|
-
return None
|
|
305
|
-
|
|
306
|
-
amount = int(match.group(1))
|
|
307
|
-
unit = match.group(2).lower()
|
|
308
|
-
|
|
309
|
-
now = datetime.now()
|
|
310
|
-
if unit == "day":
|
|
311
|
-
return now - timedelta(days=amount)
|
|
312
|
-
elif unit == "week":
|
|
313
|
-
return now - timedelta(weeks=amount)
|
|
314
|
-
elif unit == "hour":
|
|
315
|
-
return now - timedelta(hours=amount)
|
|
316
|
-
elif unit == "minute":
|
|
317
|
-
return now - timedelta(minutes=amount)
|
|
318
|
-
|
|
319
|
-
return None
|
|
320
|
-
|
|
321
|
-
def _parse_absolute_date(self, spec: str):
|
|
322
|
-
"""
|
|
323
|
-
Parse absolute date specification like "2025-10-28" or "2025-10-28 14:30".
|
|
324
|
-
|
|
325
|
-
Returns:
|
|
326
|
-
datetime object or None if not a valid date spec.
|
|
327
|
-
"""
|
|
328
|
-
from datetime import datetime
|
|
329
|
-
|
|
330
|
-
# Try YYYY-MM-DD HH:MM format
|
|
331
|
-
try:
|
|
332
|
-
return datetime.strptime(spec, "%Y-%m-%d %H:%M")
|
|
333
|
-
except ValueError:
|
|
334
|
-
pass
|
|
335
|
-
|
|
336
|
-
# Try YYYY-MM-DD format
|
|
337
|
-
try:
|
|
338
|
-
return datetime.strptime(spec, "%Y-%m-%d")
|
|
339
|
-
except ValueError:
|
|
340
|
-
pass
|
|
341
|
-
|
|
342
|
-
return None
|
|
343
|
-
|
|
344
|
-
def _find_commit_at_timestamp(self, target_datetime) -> Optional[str]:
|
|
345
|
-
"""
|
|
346
|
-
Find commit closest to (before) given timestamp.
|
|
347
|
-
|
|
348
|
-
Args:
|
|
349
|
-
target_datetime: datetime object
|
|
350
|
-
|
|
351
|
-
Returns:
|
|
352
|
-
Commit hash or None if not found.
|
|
353
|
-
"""
|
|
354
|
-
if not self.git_root:
|
|
355
|
-
return None
|
|
356
|
-
|
|
357
|
-
try:
|
|
358
|
-
# Format timestamp for git
|
|
359
|
-
timestamp_str = target_datetime.strftime("%Y-%m-%d %H:%M:%S")
|
|
360
|
-
|
|
361
|
-
# Find commit at or before this timestamp
|
|
362
|
-
result = subprocess.run(
|
|
363
|
-
[
|
|
364
|
-
"git",
|
|
365
|
-
"log",
|
|
366
|
-
"--format=%H",
|
|
367
|
-
"--before=" + timestamp_str,
|
|
368
|
-
"-1", # Get only the most recent one
|
|
369
|
-
],
|
|
370
|
-
cwd=self.git_root,
|
|
371
|
-
capture_output=True,
|
|
372
|
-
text=True,
|
|
373
|
-
timeout=5,
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
if result.returncode == 0 and result.stdout.strip():
|
|
377
|
-
return result.stdout.strip()
|
|
378
|
-
else:
|
|
379
|
-
logger.warning(f"No commit found before {timestamp_str}")
|
|
380
|
-
return None
|
|
381
|
-
except subprocess.TimeoutExpired:
|
|
382
|
-
logger.warning(f"Git log timed out looking for commit at {target_datetime}")
|
|
383
|
-
return None
|
|
384
|
-
except Exception as e:
|
|
385
|
-
logger.error(f"Error finding commit at timestamp: {e}")
|
|
386
|
-
return None
|
|
387
|
-
|
|
388
|
-
def commit(self, message: str) -> bool:
|
|
389
|
-
"""Commit this file to project's git repo with retry logic."""
|
|
390
|
-
from scitex.git import git_retry
|
|
391
|
-
|
|
392
|
-
if not self.git_root:
|
|
393
|
-
logger.warning(f"No git repository found for {self.path}")
|
|
394
|
-
return False
|
|
395
|
-
|
|
396
|
-
def _do_commit():
|
|
397
|
-
rel_path = self.path.relative_to(self.git_root)
|
|
398
|
-
subprocess.run(
|
|
399
|
-
["git", "add", str(rel_path)],
|
|
400
|
-
cwd=self.git_root,
|
|
401
|
-
check=True,
|
|
402
|
-
timeout=5,
|
|
403
|
-
)
|
|
404
|
-
subprocess.run(
|
|
405
|
-
["git", "commit", "-m", message],
|
|
406
|
-
cwd=self.git_root,
|
|
407
|
-
check=True,
|
|
408
|
-
timeout=5,
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
try:
|
|
412
|
-
git_retry(_do_commit)
|
|
413
|
-
logger.info(f"Committed {self.path}: {message}")
|
|
414
|
-
return True
|
|
415
|
-
except TimeoutError as e:
|
|
416
|
-
logger.error(f"Git lock timeout for {self.path}: {e}")
|
|
417
|
-
return False
|
|
418
|
-
except Exception as e:
|
|
419
|
-
logger.error(f"Failed to commit {self.path}: {e}")
|
|
420
|
-
return False
|
|
421
|
-
|
|
422
|
-
def checkout(self, ref: str = "HEAD") -> bool:
|
|
423
|
-
"""Checkout file from git reference."""
|
|
424
|
-
if not self.git_root:
|
|
425
|
-
logger.warning(f"No git repository found for {self.path}")
|
|
426
|
-
return False
|
|
427
|
-
|
|
428
|
-
try:
|
|
429
|
-
rel_path = self.path.relative_to(self.git_root)
|
|
430
|
-
|
|
431
|
-
result = subprocess.run(
|
|
432
|
-
["git", "checkout", ref, str(rel_path)],
|
|
433
|
-
cwd=self.git_root,
|
|
434
|
-
capture_output=True,
|
|
435
|
-
timeout=5,
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
if result.returncode == 0:
|
|
439
|
-
logger.info(f"Checked out {self.path} from {ref}")
|
|
440
|
-
return True
|
|
441
|
-
else:
|
|
442
|
-
logger.error(f"Git checkout failed: {result.stderr.decode()}")
|
|
443
|
-
return False
|
|
444
|
-
except subprocess.TimeoutExpired:
|
|
445
|
-
logger.error(f"Git checkout timed out for {self.path}")
|
|
446
|
-
return False
|
|
447
|
-
except Exception as e:
|
|
448
|
-
logger.error(f"Error checking out {self.path}: {e}")
|
|
449
|
-
return False
|
|
450
|
-
|
|
451
|
-
def __repr__(self) -> str:
|
|
452
|
-
"""String representation."""
|
|
453
|
-
return f"DocumentSection({self.path.name})"
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
def run_session() -> None:
|
|
457
|
-
"""Initialize scitex framework, run main function, and cleanup."""
|
|
458
|
-
global CONFIG, CC, sys, plt, rng
|
|
459
|
-
import sys
|
|
460
|
-
import matplotlib.pyplot as plt
|
|
461
|
-
import scitex as stx
|
|
462
|
-
|
|
463
|
-
args = parse_args()
|
|
464
|
-
|
|
465
|
-
CONFIG, sys.stdout, sys.stderr, plt, CC, rng = stx.session.start(
|
|
466
|
-
sys,
|
|
467
|
-
plt,
|
|
468
|
-
args=args,
|
|
469
|
-
file=__FILE__,
|
|
470
|
-
sdir_suffix=None,
|
|
471
|
-
verbose=False,
|
|
472
|
-
agg=True,
|
|
473
|
-
)
|
|
474
|
-
|
|
475
|
-
exit_status = main(args)
|
|
476
|
-
|
|
477
|
-
stx.session.close(
|
|
478
|
-
CONFIG,
|
|
479
|
-
verbose=False,
|
|
480
|
-
notify=False,
|
|
481
|
-
message="",
|
|
482
|
-
exit_status=exit_status,
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
def main(args):
|
|
487
|
-
section = DocumentSection(Path(args.file))
|
|
488
|
-
|
|
489
|
-
if args.action == "read":
|
|
490
|
-
content = section.read()
|
|
491
|
-
print(content if content else "File not found or empty")
|
|
492
|
-
|
|
493
|
-
elif args.action == "history":
|
|
494
|
-
history = section.history()
|
|
495
|
-
print(f"History ({len(history)} commits):")
|
|
496
|
-
for entry in history:
|
|
497
|
-
print(f" {entry}")
|
|
498
|
-
|
|
499
|
-
elif args.action == "diff":
|
|
500
|
-
diff = section.diff(args.ref)
|
|
501
|
-
print(diff if diff else "No differences")
|
|
502
|
-
|
|
503
|
-
return 0
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
def parse_args():
|
|
507
|
-
import argparse
|
|
508
|
-
|
|
509
|
-
parser = argparse.ArgumentParser(
|
|
510
|
-
description="Demonstrate DocumentSection version control"
|
|
511
|
-
)
|
|
512
|
-
parser.add_argument(
|
|
513
|
-
"--file",
|
|
514
|
-
"-f",
|
|
515
|
-
type=str,
|
|
516
|
-
required=True,
|
|
517
|
-
help="Path to document section file",
|
|
518
|
-
)
|
|
519
|
-
parser.add_argument(
|
|
520
|
-
"--action",
|
|
521
|
-
"-a",
|
|
522
|
-
type=str,
|
|
523
|
-
choices=["read", "history", "diff"],
|
|
524
|
-
default="read",
|
|
525
|
-
help="Action to perform (default: read)",
|
|
526
|
-
)
|
|
527
|
-
parser.add_argument(
|
|
528
|
-
"--ref",
|
|
529
|
-
"-r",
|
|
530
|
-
type=str,
|
|
531
|
-
default="HEAD",
|
|
532
|
-
help="Git reference for diff (default: HEAD)",
|
|
533
|
-
)
|
|
534
|
-
|
|
535
|
-
return parser.parse_args()
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if __name__ == "__main__":
|
|
539
|
-
run_session()
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
__all__ = ["DocumentSection"]
|
|
543
|
-
|
|
544
|
-
# python -m scitex.writer._dataclasses.core._DocumentSection --file ./01_manuscript/contents/introduction.tex --action history
|
|
545
|
-
|
|
546
|
-
# EOF
|