rxiv-maker 1.18.5__py3-none-any.whl → 1.19.0__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.
rxiv_maker/__version__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Version information."""
2
2
 
3
- __version__ = "1.18.5"
3
+ __version__ = "1.19.0"
@@ -2,6 +2,7 @@
2
2
 
3
3
  from .arxiv import arxiv
4
4
  from .bibliography import bibliography
5
+ from .biorxiv import biorxiv
5
6
  from .build import build as pdf
6
7
  from .cache_management import cache_group as cache
7
8
  from .changelog import changelog
@@ -29,6 +30,7 @@ from .version import version
29
30
  __all__ = [
30
31
  "arxiv",
31
32
  "bibliography",
33
+ "biorxiv",
32
34
  "cache",
33
35
  "changelog",
34
36
  "config",
@@ -0,0 +1,51 @@
1
+ """bioRxiv submission package generation command."""
2
+
3
+ import rich_click as click
4
+
5
+ from ..framework.workflow_commands import BioRxivCommand
6
+
7
+
8
+ @click.command(context_settings={"help_option_names": ["-h", "--help"]})
9
+ @click.argument("manuscript_path", type=click.Path(exists=True, file_okay=False), required=False)
10
+ @click.option("--output-dir", "-o", default="output", help="Output directory for generated files")
11
+ @click.option(
12
+ "--biorxiv-dir",
13
+ "-b",
14
+ help="Custom bioRxiv submission directory (default: output/biorxiv_submission)",
15
+ )
16
+ @click.option(
17
+ "--zip-filename",
18
+ "-z",
19
+ help="Custom ZIP filename (default: {manuscript}_biorxiv.zip)",
20
+ )
21
+ @click.option(
22
+ "--no-zip",
23
+ is_flag=True,
24
+ help="Don't create ZIP file (only create submission directory)",
25
+ )
26
+ @click.pass_context
27
+ def biorxiv(ctx, manuscript_path, output_dir, biorxiv_dir, zip_filename, no_zip):
28
+ r"""Generate bioRxiv submission package.
29
+
30
+ Creates a complete submission package including:
31
+ - bioRxiv author template (TSV file)
32
+ - Manuscript PDF
33
+ - Source files (TeX, figures, bibliography)
34
+ - ZIP file for upload
35
+
36
+ \b
37
+ Example:
38
+ rxiv biorxiv # Full package with ZIP
39
+ rxiv biorxiv --no-zip # Package without ZIP
40
+ rxiv biorxiv -b custom_dir # Custom submission directory
41
+ rxiv biorxiv -z my_submission.zip # Custom ZIP filename
42
+ """
43
+ command = BioRxivCommand()
44
+ return command.run(
45
+ ctx,
46
+ manuscript_path=manuscript_path,
47
+ output_dir=output_dir,
48
+ biorxiv_dir=biorxiv_dir,
49
+ zip_filename=zip_filename,
50
+ no_zip=no_zip,
51
+ )
@@ -162,6 +162,97 @@ class BaseCommand(ABC):
162
162
  if suggestion:
163
163
  self.console.print(f"💡 {suggestion}", style="yellow")
164
164
 
165
+ def _clear_output_directory(self) -> None:
166
+ """Clear and recreate the output directory.
167
+
168
+ Raises:
169
+ CommandExecutionError: If path_manager is not initialized
170
+ """
171
+ import shutil
172
+
173
+ if not self.path_manager:
174
+ raise CommandExecutionError("Path manager not initialized")
175
+
176
+ if self.path_manager.output_dir.exists():
177
+ shutil.rmtree(self.path_manager.output_dir)
178
+ self.path_manager.output_dir.mkdir(parents=True, exist_ok=True)
179
+
180
+ def _ensure_pdf_built(self, progress_task=None, quiet: bool = True) -> None:
181
+ """Ensure PDF is built, building it if necessary.
182
+
183
+ Args:
184
+ progress_task: Optional progress task to update
185
+ quiet: Whether to suppress build output
186
+
187
+ Raises:
188
+ CommandExecutionError: If path_manager is not initialized or build fails
189
+ """
190
+ from ...engines.operations.build_manager import BuildManager
191
+
192
+ if not self.path_manager:
193
+ raise CommandExecutionError("Path manager not initialized")
194
+
195
+ pdf_filename = f"{self.path_manager.manuscript_name}.pdf"
196
+ pdf_path = self.path_manager.output_dir / pdf_filename
197
+
198
+ if not pdf_path.exists():
199
+ if progress_task:
200
+ progress_task.update(description="Building PDF first...")
201
+
202
+ build_manager = BuildManager(
203
+ manuscript_path=str(self.path_manager.manuscript_path),
204
+ output_dir=str(self.path_manager.output_dir),
205
+ verbose=self.verbose,
206
+ quiet=quiet,
207
+ )
208
+
209
+ try:
210
+ success = build_manager.build()
211
+ if not success:
212
+ raise CommandExecutionError("PDF build failed")
213
+ except Exception as e:
214
+ raise CommandExecutionError(f"Failed to build PDF: {e}") from e
215
+
216
+ def _set_submission_defaults(
217
+ self,
218
+ submission_type: str,
219
+ submission_dir: Optional[str] = None,
220
+ zip_filename: Optional[str] = None,
221
+ ) -> tuple[str, str]:
222
+ """Set default paths for submission directories and ZIP files.
223
+
224
+ Args:
225
+ submission_type: Type of submission ("arxiv" or "biorxiv")
226
+ submission_dir: Custom submission directory path (optional)
227
+ zip_filename: Custom ZIP filename (optional)
228
+
229
+ Returns:
230
+ Tuple of (submission_dir, zip_filename) with defaults applied
231
+
232
+ Raises:
233
+ CommandExecutionError: If path_manager is not initialized
234
+ """
235
+ from pathlib import Path
236
+
237
+ if not self.path_manager:
238
+ raise CommandExecutionError("Path manager not initialized")
239
+
240
+ manuscript_output_dir = str(self.path_manager.output_dir)
241
+
242
+ # Set default submission directory
243
+ if submission_dir is None:
244
+ submission_dir = str(Path(manuscript_output_dir) / f"{submission_type}_submission")
245
+
246
+ # Set default ZIP filename
247
+ if zip_filename is None:
248
+ manuscript_name = self.path_manager.manuscript_name
249
+ if submission_type == "arxiv":
250
+ zip_filename = str(Path(manuscript_output_dir) / "for_arxiv.zip")
251
+ else:
252
+ zip_filename = str(Path(manuscript_output_dir) / f"{manuscript_name}_{submission_type}.zip")
253
+
254
+ return submission_dir, zip_filename
255
+
165
256
  @abstractmethod
166
257
  def execute_operation(self, **kwargs) -> Any:
167
258
  """Execute the main command operation.
@@ -366,46 +366,25 @@ class ArxivCommand(BaseCommand):
366
366
  no_zip: Don't create zip file
367
367
  """
368
368
  import sys
369
- from pathlib import Path
370
369
 
371
- from rxiv_maker.engines.operations.build_manager import BuildManager
372
370
  from rxiv_maker.engines.operations.prepare_arxiv import main as prepare_arxiv_main
373
371
 
374
- if self.path_manager is None:
375
- raise CommandExecutionError("Path manager not initialized")
376
-
372
+ # Set defaults using shared helper
373
+ arxiv_dir, zip_filename = self._set_submission_defaults("arxiv", arxiv_dir, zip_filename)
377
374
  manuscript_output_dir = str(self.path_manager.output_dir)
378
375
 
379
- # Set defaults using PathManager
380
- if arxiv_dir is None:
381
- arxiv_dir = str(Path(manuscript_output_dir) / "arxiv_submission")
382
- if zip_filename is None:
383
- zip_filename = str(Path(manuscript_output_dir) / "for_arxiv.zip")
384
-
385
376
  with self.create_progress() as progress:
386
- # Clear output directory first (similar to PDF command)
377
+ # Clear output directory using shared helper
387
378
  task = progress.add_task("Clearing output directory...", total=None)
388
- if self.path_manager.output_dir.exists():
389
- shutil.rmtree(self.path_manager.output_dir)
390
- self.path_manager.output_dir.mkdir(parents=True, exist_ok=True)
379
+ self._clear_output_directory()
391
380
 
392
- # First, ensure PDF is built
381
+ # Ensure PDF is built using shared helper
393
382
  progress.update(task, description="Checking PDF exists...")
394
- pdf_filename = f"{self.path_manager.manuscript_name}.pdf"
395
- pdf_path = self.path_manager.output_dir / pdf_filename
396
-
397
- if not pdf_path.exists():
398
- progress.update(task, description="Building PDF first...")
399
- build_manager = BuildManager(
400
- manuscript_path=str(self.path_manager.manuscript_path),
401
- output_dir=str(self.path_manager.output_dir),
402
- verbose=self.verbose,
403
- quiet=False,
404
- )
405
- success = build_manager.run()
406
- if not success:
407
- self.error_message("PDF build failed. Cannot prepare arXiv package.")
408
- raise CommandExecutionError("PDF build failed")
383
+ try:
384
+ self._ensure_pdf_built(progress_task=task, quiet=False)
385
+ except CommandExecutionError:
386
+ self.error_message("PDF build failed. Cannot prepare arXiv package.")
387
+ raise
409
388
 
410
389
  # Prepare arXiv package
411
390
  progress.update(task, description="Preparing arXiv package...")
@@ -524,6 +503,95 @@ class ArxivCommand(BaseCommand):
524
503
  return year, first_author
525
504
 
526
505
 
506
+ class BioRxivCommand(BaseCommand):
507
+ """BioRxiv command implementation for generating submission package."""
508
+
509
+ def execute_operation(
510
+ self,
511
+ output_dir: str = "output",
512
+ biorxiv_dir: Optional[str] = None,
513
+ zip_filename: Optional[str] = None,
514
+ no_zip: bool = False,
515
+ ) -> None:
516
+ """Execute bioRxiv submission package preparation.
517
+
518
+ Args:
519
+ output_dir: Output directory for generated files
520
+ biorxiv_dir: Custom bioRxiv submission directory path
521
+ zip_filename: Custom zip filename
522
+ no_zip: Don't create zip file
523
+ """
524
+ from pathlib import Path
525
+
526
+ from ...engines.operations.prepare_biorxiv import (
527
+ BioRxivAuthorError,
528
+ create_biorxiv_zip,
529
+ generate_biorxiv_author_tsv,
530
+ prepare_biorxiv_package,
531
+ )
532
+
533
+ # Set defaults using shared helper
534
+ biorxiv_dir, zip_filename = self._set_submission_defaults("biorxiv", biorxiv_dir, zip_filename)
535
+
536
+ with self.create_progress() as progress:
537
+ # Clear output directory using shared helper
538
+ task = progress.add_task("Clearing output directory...", total=None)
539
+ self._clear_output_directory()
540
+
541
+ # Ensure PDF is built using shared helper
542
+ progress.update(task, description="Checking PDF exists...")
543
+ self._ensure_pdf_built(progress_task=task, quiet=True)
544
+
545
+ # Generate bioRxiv author template TSV
546
+ progress.update(task, description="Generating bioRxiv author template...")
547
+ output_path = self.path_manager.output_dir
548
+ tsv_file = output_path / "biorxiv_authors.tsv"
549
+
550
+ try:
551
+ generate_biorxiv_author_tsv(
552
+ config_path=self.path_manager.get_config_file_path(),
553
+ output_path=tsv_file,
554
+ )
555
+ except BioRxivAuthorError as e:
556
+ progress.update(task, completed=True)
557
+ raise CommandExecutionError(f"Failed to generate bioRxiv template: {e}") from e
558
+
559
+ # Prepare bioRxiv submission package
560
+ progress.update(task, description="Preparing bioRxiv submission package...")
561
+ try:
562
+ biorxiv_path = prepare_biorxiv_package(
563
+ manuscript_path=self.path_manager.manuscript_path,
564
+ output_dir=self.path_manager.output_dir,
565
+ biorxiv_dir=Path(biorxiv_dir),
566
+ )
567
+ except Exception as e:
568
+ progress.update(task, completed=True)
569
+ raise CommandExecutionError(f"Failed to prepare bioRxiv package: {e}") from e
570
+
571
+ # Create ZIP file if requested
572
+ zip_path = None
573
+ if not no_zip:
574
+ progress.update(task, description="Creating ZIP package...")
575
+ try:
576
+ zip_path = create_biorxiv_zip(
577
+ biorxiv_path=biorxiv_path,
578
+ zip_filename=zip_filename,
579
+ manuscript_path=self.path_manager.manuscript_path,
580
+ )
581
+ except Exception as e:
582
+ progress.update(task, completed=True)
583
+ raise CommandExecutionError(f"Failed to create ZIP: {e}") from e
584
+
585
+ progress.update(task, completed=True)
586
+
587
+ # Show success message
588
+ self.console.print("\n[green]✅ bioRxiv submission package ready![/green]")
589
+ self.console.print(f" 📁 Package directory: {biorxiv_path}")
590
+ if zip_path:
591
+ self.console.print(f" 📦 ZIP file: {zip_path}")
592
+ self.console.print("\n📤 Upload to: https://submit.biorxiv.org/")
593
+
594
+
527
595
  class TrackChangesCommand(BaseCommand):
528
596
  """Track changes command implementation using the framework."""
529
597
 
rxiv_maker/cli/main.py CHANGED
@@ -60,7 +60,7 @@ click.rich_click.COMMAND_GROUPS = {
60
60
  },
61
61
  {
62
62
  "name": "Workflow Commands",
63
- "commands": ["get-rxiv-preprint", "arxiv", "track-changes", "setup"],
63
+ "commands": ["get-rxiv-preprint", "arxiv", "biorxiv", "track-changes", "setup"],
64
64
  },
65
65
  {
66
66
  "name": "Configuration",
@@ -252,6 +252,7 @@ main.add_command(commands.docx)
252
252
  main.add_command(commands.figures)
253
253
  main.add_command(commands.get_rxiv_preprint, name="get-rxiv-preprint")
254
254
  main.add_command(commands.arxiv)
255
+ main.add_command(commands.biorxiv)
255
256
  main.add_command(commands.init)
256
257
  main.add_command(commands.bibliography)
257
258
  main.add_command(commands.track_changes)
@@ -0,0 +1,401 @@
1
+ """Prepare bioRxiv author submission template (TSV format).
2
+
3
+ This module generates a tab-separated values (TSV) file containing author information
4
+ formatted for bioRxiv submission system upload.
5
+ """
6
+
7
+ import csv
8
+ import html.entities
9
+ import logging
10
+ import shutil
11
+ import zipfile
12
+ from pathlib import Path
13
+
14
+ from ...core.managers.config_manager import ConfigManager
15
+ from ...utils.author_name_formatter import parse_author_name
16
+ from ...utils.email_encoder import decode_email
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def encode_html_entities(text: str) -> str:
22
+ """Convert Unicode characters to HTML entities for bioRxiv submission.
23
+
24
+ bioRxiv's TSV upload requires special characters to be encoded as HTML entities.
25
+ For example, "António" becomes "António", "Åbo" becomes "Åbo".
26
+
27
+ Args:
28
+ text: Text that may contain Unicode characters
29
+
30
+ Returns:
31
+ Text with Unicode characters converted to HTML entities
32
+ (e.g., "António" -> "António", "Åbo" -> "Åbo")
33
+
34
+ Examples:
35
+ >>> encode_html_entities("António")
36
+ 'António'
37
+ >>> encode_html_entities("Åbo")
38
+ 'Åbo'
39
+ >>> encode_html_entities("José García")
40
+ 'José García'
41
+ """
42
+ if not text:
43
+ return text
44
+
45
+ # Build reverse mapping: Unicode character -> HTML entity name
46
+ char_to_entity = {}
47
+ for entity_name, codepoint in html.entities.name2codepoint.items():
48
+ char = chr(codepoint)
49
+ # Skip basic ASCII characters and use named entities for special chars
50
+ if ord(char) > 127:
51
+ char_to_entity[char] = f"&{entity_name};"
52
+
53
+ # Convert each character to HTML entity if it has one
54
+ result = []
55
+ for char in text:
56
+ if char in char_to_entity:
57
+ result.append(char_to_entity[char])
58
+ else:
59
+ result.append(char)
60
+
61
+ return "".join(result)
62
+
63
+
64
+ class BioRxivAuthorError(Exception):
65
+ """Exception raised for bioRxiv author template generation errors."""
66
+
67
+ pass
68
+
69
+
70
+ def validate_author_data(authors: list[dict]) -> None:
71
+ """Validate author data for bioRxiv submission requirements.
72
+
73
+ Args:
74
+ authors: List of author dictionaries from config
75
+
76
+ Raises:
77
+ BioRxivAuthorError: If validation fails
78
+ """
79
+ if not authors:
80
+ raise BioRxivAuthorError("No authors found in configuration")
81
+
82
+ # Count corresponding authors
83
+ corresponding_count = sum(1 for author in authors if author.get("corresponding_author", False))
84
+
85
+ if corresponding_count == 0:
86
+ raise BioRxivAuthorError(
87
+ "No corresponding author found. "
88
+ "Exactly one author must be marked with 'corresponding_author: true' in 00_CONFIG.yml"
89
+ )
90
+
91
+ if corresponding_count > 1:
92
+ corresponding_names = [
93
+ author.get("name", "Unknown") for author in authors if author.get("corresponding_author", False)
94
+ ]
95
+ raise BioRxivAuthorError(
96
+ f"Multiple corresponding authors found: {', '.join(corresponding_names)}. "
97
+ "Only one author should be marked with 'corresponding_author: true' in 00_CONFIG.yml"
98
+ )
99
+
100
+ # Validate each author has a name
101
+ for i, author in enumerate(authors):
102
+ if not author.get("name"):
103
+ raise BioRxivAuthorError(f"Author at index {i} is missing the 'name' field")
104
+
105
+
106
+ def format_author_row(author_data: dict, affiliation_map: dict) -> list[str]:
107
+ """Format a single author's data as a bioRxiv TSV row.
108
+
109
+ Args:
110
+ author_data: Author dictionary with processed data
111
+ affiliation_map: Dictionary mapping affiliation shortnames to full data
112
+
113
+ Returns:
114
+ List of column values in bioRxiv order:
115
+ Email, Institution, First Name, Middle Name(s)/Initial(s), Last Name, Suffix,
116
+ Corresponding Author, Home Page URL, Collaborative Group/Consortium, ORCiD
117
+ """
118
+ # Email (decoded from email64)
119
+ email = author_data.get("email", "")
120
+
121
+ # Institution (first affiliation's full_name) - encode HTML entities for bioRxiv
122
+ institution = ""
123
+ affiliations = author_data.get("affiliations", [])
124
+ if affiliations and affiliations[0] in affiliation_map:
125
+ institution = encode_html_entities(affiliation_map[affiliations[0]].get("full_name", ""))
126
+
127
+ # Parse name into components and encode HTML entities for bioRxiv
128
+ name_str = author_data.get("name", "")
129
+ name_parts = parse_author_name(name_str)
130
+
131
+ first_name = encode_html_entities(name_parts.get("first", ""))
132
+ middle_name = encode_html_entities(name_parts.get("middle", ""))
133
+ last_name = encode_html_entities(name_parts.get("last", ""))
134
+ suffix = encode_html_entities(name_parts.get("suffix", ""))
135
+
136
+ # Corresponding author (any text for Yes, empty string for No)
137
+ corresponding = "Yes" if author_data.get("corresponding_author", False) else ""
138
+
139
+ # Home Page URL (empty - user preference)
140
+ home_page_url = ""
141
+
142
+ # Collaborative Group/Consortium (empty)
143
+ collaborative_group = ""
144
+
145
+ # ORCiD (if present)
146
+ orcid = author_data.get("orcid", "")
147
+
148
+ return [
149
+ email,
150
+ institution,
151
+ first_name,
152
+ middle_name,
153
+ last_name,
154
+ suffix,
155
+ corresponding,
156
+ home_page_url,
157
+ collaborative_group,
158
+ orcid,
159
+ ]
160
+
161
+
162
+ def generate_biorxiv_author_tsv(config_path: Path, output_path: Path) -> Path:
163
+ """Generate bioRxiv author submission template (TSV format).
164
+
165
+ Args:
166
+ config_path: Path to the manuscript 00_CONFIG.yml file
167
+ output_path: Path where the TSV file should be written
168
+
169
+ Returns:
170
+ Path to the generated TSV file
171
+
172
+ Raises:
173
+ BioRxivAuthorError: If author data is invalid or missing
174
+ FileNotFoundError: If config file doesn't exist
175
+ """
176
+ if not config_path.exists():
177
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
178
+
179
+ # Load manuscript configuration
180
+ config_manager = ConfigManager(config_path.parent)
181
+ config = config_manager.load_config(config_path)
182
+
183
+ # Extract authors and affiliations
184
+ authors = config.get("authors", [])
185
+ affiliations = config.get("affiliations", [])
186
+
187
+ # Handle multiple corresponding authors: keep only the last one
188
+ corresponding_indices = [i for i, author in enumerate(authors) if author.get("corresponding_author", False)]
189
+ if len(corresponding_indices) > 1:
190
+ # Unmark all but the last corresponding author
191
+ for idx in corresponding_indices[:-1]:
192
+ authors[idx]["corresponding_author"] = False
193
+ logger.warning(
194
+ f"Multiple corresponding authors found. Only keeping the last one: "
195
+ f"{authors[corresponding_indices[-1]].get('name', 'Unknown')}"
196
+ )
197
+
198
+ # Validate author data
199
+ validate_author_data(authors)
200
+
201
+ # Build affiliation map (shortname -> full data)
202
+ affiliation_map = {}
203
+ for affiliation in affiliations:
204
+ shortname = affiliation.get("shortname", "")
205
+ if shortname:
206
+ affiliation_map[shortname] = affiliation
207
+
208
+ # Process authors: decode emails
209
+ processed_authors = []
210
+ for author in authors:
211
+ author_copy = author.copy()
212
+
213
+ # Decode email64 if present
214
+ if "email64" in author_copy:
215
+ try:
216
+ author_copy["email"] = decode_email(author_copy["email64"])
217
+ except ValueError as e:
218
+ logger.warning(f"Failed to decode email64 for {author_copy.get('name', 'Unknown')}: {e}")
219
+ author_copy["email"] = ""
220
+ elif "email" not in author_copy:
221
+ author_copy["email"] = ""
222
+
223
+ processed_authors.append(author_copy)
224
+
225
+ # Generate TSV file
226
+ output_path.parent.mkdir(parents=True, exist_ok=True)
227
+
228
+ with open(output_path, "w", newline="", encoding="utf-8") as f:
229
+ writer = csv.writer(f, delimiter="\t", quoting=csv.QUOTE_MINIMAL, lineterminator="\n")
230
+
231
+ # Write header row
232
+ header = [
233
+ "Email",
234
+ "Institution",
235
+ "First Name",
236
+ "Middle Name(s)/Initial(s)",
237
+ "Last Name",
238
+ "Suffix",
239
+ "Corresponding Author",
240
+ "Home Page URL",
241
+ "Collaborative Group/Consortium",
242
+ "ORCiD",
243
+ ]
244
+ writer.writerow(header)
245
+
246
+ # Write author rows
247
+ for author in processed_authors:
248
+ row = format_author_row(author, affiliation_map)
249
+ writer.writerow(row)
250
+
251
+ logger.info(f"Generated bioRxiv author template: {output_path}")
252
+ return output_path
253
+
254
+
255
+ def prepare_biorxiv_package(
256
+ manuscript_path: Path,
257
+ output_dir: Path,
258
+ biorxiv_dir: Path | None = None,
259
+ ) -> Path:
260
+ """Prepare bioRxiv submission package.
261
+
262
+ Creates a directory containing:
263
+ - biorxiv_authors.tsv (author template)
264
+ - manuscript PDF
265
+ - source files (TeX, figures, bibliography)
266
+
267
+ Args:
268
+ manuscript_path: Path to the manuscript directory
269
+ output_dir: Path to the rxiv-maker output directory
270
+ biorxiv_dir: Path where bioRxiv submission files will be created.
271
+ If None, defaults to {output_dir}/biorxiv_submission
272
+
273
+ Returns:
274
+ Path to the bioRxiv submission directory
275
+
276
+ Raises:
277
+ FileNotFoundError: If required files are missing
278
+ """
279
+ output_path = Path(output_dir)
280
+
281
+ # Default bioRxiv directory to be inside the output directory
282
+ if biorxiv_dir is None:
283
+ biorxiv_dir = output_path / "biorxiv_submission"
284
+
285
+ biorxiv_path = Path(biorxiv_dir)
286
+
287
+ # Create clean bioRxiv directory
288
+ if biorxiv_path.exists():
289
+ shutil.rmtree(biorxiv_path)
290
+ biorxiv_path.mkdir(parents=True)
291
+
292
+ manuscript_name = manuscript_path.name if manuscript_path else "manuscript"
293
+ logger.info(f"Preparing bioRxiv submission package for '{manuscript_name}' in {biorxiv_path}")
294
+
295
+ # 1. Copy the bioRxiv authors TSV file (already generated)
296
+ tsv_source = output_path / "biorxiv_authors.tsv"
297
+ if not tsv_source.exists():
298
+ raise FileNotFoundError(
299
+ f"bioRxiv author template not found: {tsv_source}\n"
300
+ "Please run TSV generation first or ensure output directory is correct."
301
+ )
302
+ shutil.copy2(tsv_source, biorxiv_path / "biorxiv_authors.tsv")
303
+ logger.info("✓ Copied author template: biorxiv_authors.tsv")
304
+
305
+ # 2. Find and copy the manuscript PDF
306
+ pdf_files = list(output_path.glob("*.pdf"))
307
+ main_pdf = None
308
+ for pdf in pdf_files:
309
+ # Skip supplementary PDFs
310
+ if "supplementary" not in pdf.name.lower():
311
+ main_pdf = pdf
312
+ break
313
+
314
+ if not main_pdf:
315
+ logger.warning("⚠ No manuscript PDF found in output directory")
316
+ else:
317
+ shutil.copy2(main_pdf, biorxiv_path / main_pdf.name)
318
+ logger.info(f"✓ Copied manuscript PDF: {main_pdf.name}")
319
+
320
+ # 3. Copy source files for submission
321
+ # Copy TeX files
322
+ tex_files = list(output_path.glob("*.tex"))
323
+ for tex_file in tex_files:
324
+ shutil.copy2(tex_file, biorxiv_path / tex_file.name)
325
+ logger.info(f"✓ Copied TeX file: {tex_file.name}")
326
+
327
+ # Copy style file
328
+ style_file = output_path / "rxiv_maker_style.cls"
329
+ if style_file.exists():
330
+ shutil.copy2(style_file, biorxiv_path / "rxiv_maker_style.cls")
331
+ logger.info("✓ Copied style file: rxiv_maker_style.cls")
332
+
333
+ # Copy bibliography
334
+ bib_file = output_path / "03_REFERENCES.bib"
335
+ if bib_file.exists():
336
+ shutil.copy2(bib_file, biorxiv_path / "03_REFERENCES.bib")
337
+ logger.info("✓ Copied bibliography: 03_REFERENCES.bib")
338
+
339
+ # Copy FIGURES directory
340
+ figures_source = output_path / "FIGURES"
341
+ if figures_source.exists() and figures_source.is_dir():
342
+ figures_dest = biorxiv_path / "FIGURES"
343
+ shutil.copytree(figures_source, figures_dest)
344
+ figure_count = len(list(figures_dest.rglob("*")))
345
+ logger.info(f"✓ Copied FIGURES directory ({figure_count} files)")
346
+
347
+ logger.info(f"\n📦 bioRxiv package prepared in {biorxiv_path}")
348
+ return biorxiv_path
349
+
350
+
351
+ def create_biorxiv_zip(
352
+ biorxiv_path: Path,
353
+ zip_filename: str = "biorxiv_submission.zip",
354
+ manuscript_path: Path | None = None,
355
+ ) -> Path:
356
+ """Create a ZIP file for bioRxiv submission.
357
+
358
+ Args:
359
+ biorxiv_path: Path to the bioRxiv submission directory
360
+ zip_filename: Name of the ZIP file to create
361
+ manuscript_path: Optional manuscript path for naming
362
+
363
+ Returns:
364
+ Path to the created ZIP file
365
+ """
366
+ # Use manuscript-aware naming if manuscript path is provided
367
+ if manuscript_path and zip_filename == "biorxiv_submission.zip":
368
+ manuscript_name = manuscript_path.name
369
+ zip_filename = f"{manuscript_name}_biorxiv.zip"
370
+
371
+ zip_path = Path(zip_filename).resolve()
372
+
373
+ # Define auxiliary files that should be excluded
374
+ auxiliary_extensions = {".aux", ".blg", ".log", ".out", ".fls", ".fdb_latexmk", ".synctex.gz"}
375
+
376
+ logger.info(f"\n📁 Creating ZIP package: {zip_path}")
377
+
378
+ excluded_files = []
379
+ included_files = []
380
+
381
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
382
+ for file_path in biorxiv_path.rglob("*"):
383
+ if file_path.is_file():
384
+ # Check if file should be excluded (auxiliary files)
385
+ should_exclude = file_path.suffix.lower() in auxiliary_extensions
386
+
387
+ if should_exclude:
388
+ excluded_files.append(file_path.name)
389
+ continue
390
+
391
+ # Store files with relative paths
392
+ arcname = file_path.relative_to(biorxiv_path)
393
+ zipf.write(file_path, arcname)
394
+ included_files.append(str(arcname))
395
+
396
+ logger.info(f"✅ ZIP created: {zip_path}")
397
+ logger.info(f" Files included: {len(included_files)}")
398
+ if excluded_files:
399
+ logger.info(f" Files excluded: {len(excluded_files)} (auxiliary files)")
400
+
401
+ return zip_path
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rxiv-maker
3
- Version: 1.18.5
3
+ Version: 1.19.0
4
4
  Summary: Write scientific preprints in Markdown. Generate publication-ready PDFs efficiently.
5
5
  Project-URL: Homepage, https://github.com/HenriquesLab/rxiv-maker
6
6
  Project-URL: Documentation, https://github.com/HenriquesLab/rxiv-maker#readme
@@ -1,5 +1,5 @@
1
1
  rxiv_maker/__init__.py,sha256=p04JYC5ZhP6dLXkoWVlKNyiRvsDE1a4C88f9q4xO3tA,3268
2
- rxiv_maker/__version__.py,sha256=i8ukZW5rVDoXBNiuInaBgxVXM535F7b6jCi26fK-JBw,51
2
+ rxiv_maker/__version__.py,sha256=s7-5Jsk7L8O8kxknnMqV6RyUG2rsUkOD8DjGMhagzNo,51
3
3
  rxiv_maker/rxiv_maker_cli.py,sha256=9Lu_mhFPXwx5jzAR6StCNxwCm_fkmP5qiOYdNuh_AwI,120
4
4
  rxiv_maker/validate.py,sha256=AIzgP59KbCQJqC9WIGfUdVv0xI6ud9g1fFznQkaGz5Q,9373
5
5
  rxiv_maker/cli/__init__.py,sha256=Jw0DTFUSofN-02xpVrt1UUzRcgH5NNd-GPNidhmNwpU,77
@@ -7,10 +7,11 @@ rxiv_maker/cli/__main__.py,sha256=OEaQSb9q7MK3MMkEJfOdzzJuDvr5LAeILtHQesdYxWU,13
7
7
  rxiv_maker/cli/framework.py,sha256=enBb4d48ZsgB-4H6zjzSGnMFEgPd1T7cnUjNcOOPKKY,3895
8
8
  rxiv_maker/cli/interactive.py,sha256=lgfZVFf1rdJKZtHPogNiEPSwhMlnU2_dSFU9CJjM-7E,18907
9
9
  rxiv_maker/cli/interactive_prompts.py,sha256=jhyCdP54TIYHzgAD84lwcyat3tCGVeK9vmyfLHuwL6I,11830
10
- rxiv_maker/cli/main.py,sha256=AwqEcq46MPu8T72iqO39RWemtiD7WiAeUkfxACMUdgs,8697
11
- rxiv_maker/cli/commands/__init__.py,sha256=jp_dRdPBJNhgZxA7ccAplbtESUBtb81k-boLA5yi3Fg,1396
10
+ rxiv_maker/cli/main.py,sha256=ieSKR954s2ppE5SiwDYa5Xwr3vh_xzQxwI1tf2tMcYE,8743
11
+ rxiv_maker/cli/commands/__init__.py,sha256=Kcof7u2l2XE1M4g3AAuio5cTX6WSt4ly1ROujHH54xw,1440
12
12
  rxiv_maker/cli/commands/arxiv.py,sha256=nlAS36lgTNjd6Hn1cdporXFZskf7-Jl-fZLvjxc6gjo,1245
13
13
  rxiv_maker/cli/commands/bibliography.py,sha256=3a4gNtY7Lvd5-mwIj-vCD5WwRDgMqPT37tJETthKYKA,2956
14
+ rxiv_maker/cli/commands/biorxiv.py,sha256=nKu7nAeTVeWH_3Q-gQnY5Y_Q9YnXZEC50MqQzEwqua4,1670
14
15
  rxiv_maker/cli/commands/build.py,sha256=v513o3duOD9YvKUoOTggqshqQzwD4THBVIAClJTnx60,3525
15
16
  rxiv_maker/cli/commands/cache_management.py,sha256=y58QsuSjzCz_IhY6iXnir8OoblHW5ZBMqItfnfjTP-Y,4577
16
17
  rxiv_maker/cli/commands/changelog.py,sha256=OzEay8E8sWfXagjXyWiKwfE-OC9UKokWB7mbINp36t4,8461
@@ -33,13 +34,13 @@ rxiv_maker/cli/commands/upgrade.py,sha256=UpdqEQwbNYmDMbSrYGv_pVd-7u8PPT3US5RVEN
33
34
  rxiv_maker/cli/commands/validate.py,sha256=3JghFQevJvQDQII4p_QWbQXMEUyDpM-t9-WxtaT4edo,1629
34
35
  rxiv_maker/cli/commands/version.py,sha256=VMlfSxxsrZH02d24MXLUDZfHBW39yZRpWxUpMhQ-X0Y,2737
35
36
  rxiv_maker/cli/framework/__init__.py,sha256=4FPXdP8J6v4eeEn46mwY0VtnwxjR1jnW_kTrXykQlQs,2704
36
- rxiv_maker/cli/framework/base.py,sha256=GcDFFOGVrLSlRgwDPmMkEAL60TbgT-maTyiCp1g-BrA,7886
37
+ rxiv_maker/cli/framework/base.py,sha256=HqjVHVJzBKLk9-lH6PQrVxYedvs1efcLZ94IxVq7rmE,11242
37
38
  rxiv_maker/cli/framework/cache_commands.py,sha256=J91UYLTsLTRoNdzuhAbNL2bJJovYYfX3T9jI8cNUBWU,9897
38
39
  rxiv_maker/cli/framework/config_commands.py,sha256=a1uOQkCCw3d4qlro3OwHIorcoNg03T_R4-HbfVb-hmQ,19336
39
40
  rxiv_maker/cli/framework/content_commands.py,sha256=RilxKeG2c1m2fu0CtWAvP3cGh11DGx9P-nh2kIewAg4,22596
40
41
  rxiv_maker/cli/framework/decorators.py,sha256=fh085e3k1CaLSMoZevt8hvgnEuejrf-mcNS-dwXoY_A,10365
41
42
  rxiv_maker/cli/framework/utility_commands.py,sha256=drIAc1TAYpne76gj7SZeZhPozVAY5uL9GFPVT_Ez0-E,26437
42
- rxiv_maker/cli/framework/workflow_commands.py,sha256=Csls8VGmNCWPjpY9PMfdIAdzDhD_ZmSfRkhdcUihzpk,32699
43
+ rxiv_maker/cli/framework/workflow_commands.py,sha256=CguePd76EjdT53ab9a9clv3S-FSpBAXioQKmto1VS3w,35422
43
44
  rxiv_maker/config/defaults.py,sha256=vHyLGVxe5-z9TLxu5f6NhquPvqQkER_KZv_j1I4_dHQ,3055
44
45
  rxiv_maker/config/validator.py,sha256=9XDPfo_YgasGt6NLkl6HIhaGh1fr6XsFNiXU2DSsivw,38299
45
46
  rxiv_maker/converters/__init__.py,sha256=d7WGsRwWqRQWO117IkKDP0Ap0ERiK0N2-dXHInye3_A,685
@@ -99,6 +100,7 @@ rxiv_maker/engines/operations/generate_docs.py,sha256=8d_oVYUuRRqTuYN1KnJKqM5Ydp
99
100
  rxiv_maker/engines/operations/generate_figures.py,sha256=YeKzH6qVsuPGjtCsvWugLJoys6y73xTyO7Y5g30KM20,38730
100
101
  rxiv_maker/engines/operations/generate_preprint.py,sha256=wpKDAu2RLJ4amSdhX5GZ7hU-iTsTRt4etcEA7AZYp04,2662
101
102
  rxiv_maker/engines/operations/prepare_arxiv.py,sha256=cd0JN5IO-Wy9T8ab75eibyaA8_K8Gpwrz2F-95OMnx4,21551
103
+ rxiv_maker/engines/operations/prepare_biorxiv.py,sha256=-Ok4iwnLsgNk6xJ9HSk74_7UBammJRTF3r8ISy_qDjc,13858
102
104
  rxiv_maker/engines/operations/setup_environment.py,sha256=gERuThHTldH0YqgXn85995deHBP6csY1ZhCNgU6-vFg,12691
103
105
  rxiv_maker/engines/operations/track_changes.py,sha256=jJZ-XnTFx8TMvcnX8_9D7ydc0G01S1PnckLkxHRTX1g,24722
104
106
  rxiv_maker/engines/operations/validate.py,sha256=OVmtRVtG-r1hoA8IqYaNC-ijN1a5ixM3X5Z8Gda-O2M,17142
@@ -194,8 +196,8 @@ rxiv_maker/validators/doi/metadata_comparator.py,sha256=euqHhKP5sHQAdZbdoAahUn6Y
194
196
  rxiv_maker/tex/template.tex,sha256=_tPtxrurn3sKTt9Kfa44lPdPyT44vHbDUOGqldU9r2s,1378
195
197
  rxiv_maker/tex/style/rxiv_maker_style.bst,sha256=jbVqrJgAm6F88cow5vtZuPBwwmlcYykclTm8RvZIo6Y,24281
196
198
  rxiv_maker/tex/style/rxiv_maker_style.cls,sha256=6VDmZE0uvYWog6rcYi2K_NIM9-Pgjx9AFdRg_sTheK0,24374
197
- rxiv_maker-1.18.5.dist-info/METADATA,sha256=3MGWQNaVnAJM_7nypR3gtVuh5BTgnCwJ5eZ8S_MTrf8,18432
198
- rxiv_maker-1.18.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
199
- rxiv_maker-1.18.5.dist-info/entry_points.txt,sha256=ghCN0hI9A1GlG7QY5F6E-xYPflA8CyS4B6bTQ1YLop0,97
200
- rxiv_maker-1.18.5.dist-info/licenses/LICENSE,sha256=GSZFoPIhWDNJEtSHTQ5dnELN38zFwRiQO2antBezGQk,1093
201
- rxiv_maker-1.18.5.dist-info/RECORD,,
199
+ rxiv_maker-1.19.0.dist-info/METADATA,sha256=cM3QcAip52NGWPYbS5RqFTaP1Q3L6nsN6z0UmBt6YT4,18432
200
+ rxiv_maker-1.19.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
201
+ rxiv_maker-1.19.0.dist-info/entry_points.txt,sha256=ghCN0hI9A1GlG7QY5F6E-xYPflA8CyS4B6bTQ1YLop0,97
202
+ rxiv_maker-1.19.0.dist-info/licenses/LICENSE,sha256=GSZFoPIhWDNJEtSHTQ5dnELN38zFwRiQO2antBezGQk,1093
203
+ rxiv_maker-1.19.0.dist-info/RECORD,,