rxiv-maker 1.18.4__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.4"
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)
@@ -256,9 +256,9 @@ class ContentProcessor(RecoveryEnhancedMixin):
256
256
  # Supplementary note headers must be processed early (before general headers)
257
257
  self.register_processor(
258
258
  "supplementary_notes",
259
- lambda content, **kwargs: process_supplementary_notes(content)
260
- if kwargs.get("is_supplementary", False)
261
- else content,
259
+ lambda content, **kwargs: (
260
+ process_supplementary_notes(content) if kwargs.get("is_supplementary", False) else content
261
+ ),
262
262
  ProcessorConfig(
263
263
  name="supplementary_notes",
264
264
  priority=ProcessorPriority.NORMAL,
@@ -408,9 +408,9 @@ class ContentProcessor(RecoveryEnhancedMixin):
408
408
 
409
409
  self.register_processor(
410
410
  "restore_supplementary_placeholders",
411
- lambda content, **kwargs: restore_supplementary_note_placeholders(content)
412
- if kwargs.get("is_supplementary", False)
413
- else content,
411
+ lambda content, **kwargs: (
412
+ restore_supplementary_note_placeholders(content) if kwargs.get("is_supplementary", False) else content
413
+ ),
414
414
  ProcessorConfig(
415
415
  name="restore_supplementary_placeholders",
416
416
  priority=ProcessorPriority.NORMAL,
@@ -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
@@ -504,20 +504,20 @@ def process_template_replacements(template_content, yaml_metadata, article_md, o
504
504
  if content_sections.get("introduction"):
505
505
  # If there's an introduction section, use it with "Introduction" header
506
506
  main_section_content = content_sections["introduction"]
507
- main_section_parts.append(f"\\section*{{Introduction}}\n{main_section_content}")
507
+ main_section_parts.append(f"\\section*{{Introduction}}\n\n{main_section_content}")
508
508
 
509
509
  # after_intro mode: insert Methods right after Introduction
510
510
  if methods_placement == "after_intro" and methods_content:
511
- main_section_parts.append(f"\\section*{{Methods}}\n{methods_content}")
511
+ main_section_parts.append(f"\\section*{{Methods}}\n\n{methods_content}")
512
512
 
513
513
  elif content_sections.get("main"):
514
514
  # If there's a main section (but no introduction), use it with "Main" header
515
515
  main_section_content = content_sections["main"]
516
- main_section_parts.append(f"\\section*{{Main}}\n{main_section_content}")
516
+ main_section_parts.append(f"\\section*{{Main}}\n\n{main_section_content}")
517
517
 
518
518
  # after_intro mode: insert Methods after Main section if no Introduction exists
519
519
  if methods_placement == "after_intro" and methods_content:
520
- main_section_parts.append(f"\\section*{{Methods}}\n{methods_content}")
520
+ main_section_parts.append(f"\\section*{{Methods}}\n\n{methods_content}")
521
521
 
522
522
  # Include all custom sections (sections that don't map to standard academic paper sections)
523
523
  standard_sections = {
@@ -542,7 +542,7 @@ def process_template_replacements(template_content, yaml_metadata, article_md, o
542
542
  if section_key not in standard_sections and section_content.strip():
543
543
  # Add section header using the original title
544
544
  section_title = section_titles.get(section_key, section_key.replace("_", " ").title())
545
- custom_section_with_header = f"\\section*{{{section_title}}}\n{section_content}"
545
+ custom_section_with_header = f"\\section*{{{section_title}}}\n\n{section_content}"
546
546
  custom_sections.append(custom_section_with_header)
547
547
 
548
548
  # Add all custom sections to the main section
@@ -558,7 +558,7 @@ def process_template_replacements(template_content, yaml_metadata, article_md, o
558
558
  # Results section
559
559
  results_content = content_sections.get("results", "").strip()
560
560
  if results_content:
561
- results_section = f"\\section*{{Results}}\n{results_content}"
561
+ results_section = f"\\section*{{Results}}\n\n{results_content}"
562
562
  else:
563
563
  results_section = ""
564
564
  template_content = template_content.replace("<PY-RPL:RESULTS-SECTION>", results_section)
@@ -566,7 +566,7 @@ def process_template_replacements(template_content, yaml_metadata, article_md, o
566
566
  # Discussion section
567
567
  discussion_content = content_sections.get("discussion", "").strip()
568
568
  if discussion_content:
569
- discussion_section = f"\\section*{{Discussion}}\n{discussion_content}"
569
+ discussion_section = f"\\section*{{Discussion}}\n\n{discussion_content}"
570
570
  else:
571
571
  discussion_section = ""
572
572
  template_content = template_content.replace("<PY-RPL:DISCUSSION-SECTION>", discussion_section)
@@ -574,26 +574,26 @@ def process_template_replacements(template_content, yaml_metadata, article_md, o
574
574
  # Conclusions section
575
575
  conclusions_content = content_sections.get("conclusion", "").strip()
576
576
  if conclusions_content:
577
- conclusions_section = f"\\section*{{Conclusions}}\n{conclusions_content}"
577
+ conclusions_section = f"\\section*{{Conclusions}}\n\n{conclusions_content}"
578
578
  else:
579
579
  conclusions_section = ""
580
580
  template_content = template_content.replace("<PY-RPL:CONCLUSIONS-SECTION>", conclusions_section)
581
581
 
582
582
  # Handle Methods section placement based on configuration
583
583
  if methods_placement == "after_results" and methods_content:
584
- methods_section = f"\\section*{{Methods}}\n{methods_content}"
584
+ methods_section = f"\\section*{{Methods}}\n\n{methods_content}"
585
585
  template_content = template_content.replace("<PY-RPL:METHODS-AFTER-RESULTS>", methods_section)
586
586
  else:
587
587
  template_content = template_content.replace("<PY-RPL:METHODS-AFTER-RESULTS>", "")
588
588
 
589
589
  if methods_placement == "after_discussion" and methods_content:
590
- methods_section = f"\\section*{{Methods}}\n{methods_content}"
590
+ methods_section = f"\\section*{{Methods}}\n\n{methods_content}"
591
591
  template_content = template_content.replace("<PY-RPL:METHODS-AFTER-DISCUSSION>", methods_section)
592
592
  else:
593
593
  template_content = template_content.replace("<PY-RPL:METHODS-AFTER-DISCUSSION>", "")
594
594
 
595
595
  if methods_placement == "after_bibliography" and methods_content:
596
- methods_section = f"\\section*{{Methods}}\n{methods_content}"
596
+ methods_section = f"\\section*{{Methods}}\n\n{methods_content}"
597
597
  template_content = template_content.replace("<PY-RPL:METHODS-AFTER-BIBLIOGRAPHY>", methods_section)
598
598
  else:
599
599
  template_content = template_content.replace("<PY-RPL:METHODS-AFTER-BIBLIOGRAPHY>", "")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rxiv-maker
3
- Version: 1.18.4
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=22c3XroKkBFqzb5Di5nc0cgd1neLZx9btfk3C7NilG4,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
@@ -59,7 +60,7 @@ rxiv_maker/converters/table_processor.py,sha256=0sCtDVlQwLcAC8_dJMijgVb7DCmtannC
59
60
  rxiv_maker/converters/text_formatters.py,sha256=pa2kVAqyPkGh5LViL5Iqqjjqw1ixB0GKBZJloaVBVi0,42816
60
61
  rxiv_maker/converters/types.py,sha256=mTLXVTBGfEi2dp_U2maSE7kZQFz_lfmLDQjlMAeU9lE,885
61
62
  rxiv_maker/converters/url_processor.py,sha256=xoZrgz7kgRtMzSay_14ZpYEwK9uXj3YloQW9K-G8LAc,6586
62
- rxiv_maker/core/content_processor.py,sha256=nES8uiFC8k589GOcCtHzHZo84Dcf7I22zrsoocZwOEM,33796
63
+ rxiv_maker/core/content_processor.py,sha256=m_jFgmhajlPKZc4FbVfJ30UOF6IPS3qwjim-2sPV8Dc,33812
63
64
  rxiv_maker/core/environment_bootstrap.py,sha256=hG1hAOdu-FGeVsn5NU0j0BY21kRquni4K2Cp6uBpEmw,13601
64
65
  rxiv_maker/core/environment_manager.py,sha256=hI8uhqGYd2PDMwHiYJkdaoiqztbATaL-XAklgMaIgfQ,10349
65
66
  rxiv_maker/core/error_codes.py,sha256=xbSK2VXPnGgHOndf_O69GpRpblyKnW_ahcmjJYPlz2I,15162
@@ -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
@@ -127,7 +129,7 @@ rxiv_maker/manuscript_utils/figure_utils.py,sha256=FsNtMiA1IOHeA6gQsENmAWLZSAvPp
127
129
  rxiv_maker/processors/__init__.py,sha256=8UmaeFkbPfcwAL5MnhN2DcOA2k8iuJu0B5dDA6_pmnA,720
128
130
  rxiv_maker/processors/author_processor.py,sha256=jC4qZ9M_GADelkp1v8pk46ESUf4DNraXLfMYGhn_7ZM,10016
129
131
  rxiv_maker/processors/markdown_preprocessor.py,sha256=Ez_2lhSFEdJY0CS2SKJJESRfnmSdMq4s0jP2nlFSKN8,11228
130
- rxiv_maker/processors/template_processor.py,sha256=V47kh5zyiZDPNXBfNkQeyU5r-3MBHM3UMWAjcfgs11s,30316
132
+ rxiv_maker/processors/template_processor.py,sha256=w6VAk_OgW8dTY8riOZ_C-8-6x7Kg1jMnCYda4p8eiE0,30338
131
133
  rxiv_maker/processors/yaml_processor.py,sha256=SSXHpwY1Cw81IDN4x40UFtosz-T9l29oh-CZpusmlYo,11499
132
134
  rxiv_maker/scripts/__init__.py,sha256=nKsDmj6UAc90uC6sKd-V6O80OMYjAl4Uha9zF_6ayX0,154
133
135
  rxiv_maker/scripts/custom_doc_generator.py,sha256=pMkdTI8a4qG8Z3gFUJtPwRuu2i0ioW3pKfs22es0KCk,6311
@@ -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.4.dist-info/METADATA,sha256=k5MqXx3R4y8-uuXXm6dc3nkTMBAzDzre78bTy_yewtQ,18432
198
- rxiv_maker-1.18.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
199
- rxiv_maker-1.18.4.dist-info/entry_points.txt,sha256=ghCN0hI9A1GlG7QY5F6E-xYPflA8CyS4B6bTQ1YLop0,97
200
- rxiv_maker-1.18.4.dist-info/licenses/LICENSE,sha256=GSZFoPIhWDNJEtSHTQ5dnELN38zFwRiQO2antBezGQk,1093
201
- rxiv_maker-1.18.4.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,,