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.
Files changed (64) 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/social/__init__.py +1 -24
  9. scitex/writer/README.md +25 -409
  10. scitex/writer/__init__.py +98 -13
  11. {scitex-2.16.0.dist-info → scitex-2.16.1.dist-info}/METADATA +6 -1
  12. {scitex-2.16.0.dist-info → scitex-2.16.1.dist-info}/RECORD +15 -62
  13. scitex/writer/Writer.py +0 -487
  14. scitex/writer/_clone_writer_project.py +0 -160
  15. scitex/writer/_compile/__init__.py +0 -41
  16. scitex/writer/_compile/_compile_async.py +0 -130
  17. scitex/writer/_compile/_compile_unified.py +0 -148
  18. scitex/writer/_compile/_parser.py +0 -63
  19. scitex/writer/_compile/_runner.py +0 -457
  20. scitex/writer/_compile/_validator.py +0 -46
  21. scitex/writer/_compile/manuscript.py +0 -110
  22. scitex/writer/_compile/revision.py +0 -82
  23. scitex/writer/_compile/supplementary.py +0 -100
  24. scitex/writer/_dataclasses/__init__.py +0 -44
  25. scitex/writer/_dataclasses/config/_CONSTANTS.py +0 -46
  26. scitex/writer/_dataclasses/config/_WriterConfig.py +0 -175
  27. scitex/writer/_dataclasses/config/__init__.py +0 -9
  28. scitex/writer/_dataclasses/contents/_ManuscriptContents.py +0 -236
  29. scitex/writer/_dataclasses/contents/_RevisionContents.py +0 -136
  30. scitex/writer/_dataclasses/contents/_SupplementaryContents.py +0 -114
  31. scitex/writer/_dataclasses/contents/__init__.py +0 -9
  32. scitex/writer/_dataclasses/core/_Document.py +0 -146
  33. scitex/writer/_dataclasses/core/_DocumentSection.py +0 -546
  34. scitex/writer/_dataclasses/core/__init__.py +0 -7
  35. scitex/writer/_dataclasses/results/_CompilationResult.py +0 -165
  36. scitex/writer/_dataclasses/results/_LaTeXIssue.py +0 -102
  37. scitex/writer/_dataclasses/results/_SaveSectionsResponse.py +0 -118
  38. scitex/writer/_dataclasses/results/_SectionReadResponse.py +0 -131
  39. scitex/writer/_dataclasses/results/__init__.py +0 -11
  40. scitex/writer/_dataclasses/tree/MINIMUM_FILES.md +0 -121
  41. scitex/writer/_dataclasses/tree/_ConfigTree.py +0 -86
  42. scitex/writer/_dataclasses/tree/_ManuscriptTree.py +0 -84
  43. scitex/writer/_dataclasses/tree/_RevisionTree.py +0 -97
  44. scitex/writer/_dataclasses/tree/_ScriptsTree.py +0 -118
  45. scitex/writer/_dataclasses/tree/_SharedTree.py +0 -100
  46. scitex/writer/_dataclasses/tree/_SupplementaryTree.py +0 -101
  47. scitex/writer/_dataclasses/tree/__init__.py +0 -23
  48. scitex/writer/_mcp/__init__.py +0 -4
  49. scitex/writer/_mcp/handlers.py +0 -32
  50. scitex/writer/_mcp/tool_schemas.py +0 -33
  51. scitex/writer/_project/__init__.py +0 -29
  52. scitex/writer/_project/_create.py +0 -89
  53. scitex/writer/_project/_trees.py +0 -63
  54. scitex/writer/_project/_validate.py +0 -61
  55. scitex/writer/utils/.legacy_git_retry.py +0 -164
  56. scitex/writer/utils/__init__.py +0 -24
  57. scitex/writer/utils/_converters.py +0 -635
  58. scitex/writer/utils/_parse_latex_logs.py +0 -138
  59. scitex/writer/utils/_parse_script_args.py +0 -156
  60. scitex/writer/utils/_verify_tree_structure.py +0 -205
  61. scitex/writer/utils/_watch.py +0 -96
  62. {scitex-2.16.0.dist-info → scitex-2.16.1.dist-info}/WHEEL +0 -0
  63. {scitex-2.16.0.dist-info → scitex-2.16.1.dist-info}/entry_points.txt +0 -0
  64. {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
@@ -1,7 +0,0 @@
1
- from ._Document import Document
2
- from ._DocumentSection import DocumentSection
3
-
4
- __all__ = [
5
- "Document",
6
- "DocumentSection",
7
- ]