paperpush 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {paperpush-0.2.0 → paperpush-0.2.1}/PKG-INFO +6 -3
- {paperpush-0.2.0 → paperpush-0.2.1}/README.md +4 -1
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/__init__.py +1 -1
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/cli.py +39 -2
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/database.py +120 -5
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/references.py +1 -1
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/requirements.py +167 -4
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/sensitive.py +1 -1
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/iclr_2027.sub +1 -0
- paperpush-0.2.1/paperpush/venue_data.py +374 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues.json +1 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush.egg-info/PKG-INFO +6 -3
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush.egg-info/SOURCES.txt +2 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/pyproject.toml +2 -2
- {paperpush-0.2.0 → paperpush-0.2.1}/tests/test_login.py +501 -2
- paperpush-0.2.1/tests/test_venue_data.py +498 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/LICENSE +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/__main__.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/_docs/agents.md +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/_logging.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/autofill.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/credentials.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/manuscript.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/manuscript_requirements.json +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/manuscript_requirements.schema.json +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/mcp_server.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/orcid.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/requirements_check.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/schema_models.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/arxiv.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/bioinformatics.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/biorxiv.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/bmc_bioinformatics.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/cell.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/cell_genomics.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/cell_systems.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/combinatorica.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/discrete_mathematics.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/genome_biology.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/medrxiv.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/nature.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/nature_biotech.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/nature_methods.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/nucleic_acids_research.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/plos_compbio.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/science.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/science_advances.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/science_immunology.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/science_robotics.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/science_signaling.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/subfile_templates/science_translational_medicine.sub +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/validate.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/__init__.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/aaai_2027_countries.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/aaai_2027_topics.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/arxiv_categories.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/bioinformatics_keywords.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/genome_biology_institution_countries.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/iclr_2027_primary_areas.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/nature_biotech_categories.json +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/nature_categories.json +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/nature_methods_categories.json +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/plos_compbio_classifications.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/plos_compbio_funding_countries.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/science_advances_subjects.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/science_immunology_subjects.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/science_robotics_subjects.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/science_signaling_subjects.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/science_subjects.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/_assets/science_translational_medicine_subjects.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/arxiv/__init__.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/arxiv/arxiv.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/base.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/common.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/discrete_mathematics/__init__.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/discrete_mathematics/discrete_mathematics.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/editflow/__init__.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/editflow/combinatorica.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/editflow/main.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/editorialmanager/__init__.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/editorialmanager/cell.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/editorialmanager/cell_genomics.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/editorialmanager/cell_systems.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/editorialmanager/main.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/editorialmanager/plos_compbio.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/login.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/nature/__init__.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/nature/main.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/nature/nature.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/nature/nature_biotech.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/nature/nature_methods.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/openreview/__init__.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/openreview/aaai_2027.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/openreview/iclr_2027.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/openreview/main.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/openrxiv/__init__.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/openrxiv/biorxiv.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/openrxiv/main.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/openrxiv/medrxiv.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/scholarone/__init__.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/scholarone/bioinformatics.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/scholarone/main.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/scholarone/nucleic_acids_research.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/science/__init__.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/science/science.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/snapp/__init__.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/snapp/bmc_bioinformatics.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/snapp/genome_biology.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/snapp/main.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/template/__init__.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues/template/template.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush/venues.schema.json +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush.egg-info/dependency_links.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush.egg-info/entry_points.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush.egg-info/requires.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/paperpush.egg-info/top_level.txt +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/setup.cfg +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/tests/test_autofill.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/tests/test_editorialmanager_login.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/tests/test_internals.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/tests/test_logging.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/tests/test_mcp_server.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/tests/test_portal_drift.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/tests/test_references.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/tests/test_requirements.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/tests/test_sensitive.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/tests/test_subfile.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/tests/test_submit.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/tests/test_validate.py +0 -0
- {paperpush-0.2.0 → paperpush-0.2.1}/tests/test_venue_interface.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: paperpush
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A Python library for automating the submission of manuscripts to academic venues.
|
|
5
5
|
Author-email: Joseph Rich <josephrich98@gmail.com>
|
|
6
6
|
Maintainer-email: Joseph Rich <josephrich98@gmail.com>
|
|
7
|
-
License-Expression:
|
|
7
|
+
License-Expression: BSD-2-Clause
|
|
8
8
|
Project-URL: Homepage, https://github.com/pachterlab/paperpush
|
|
9
9
|
Project-URL: Repository, https://github.com/pachterlab/paperpush
|
|
10
10
|
Project-URL: Documentation, https://github.com/pachterlab/paperpush#readme
|
|
@@ -51,7 +51,7 @@ playwright install chromium
|
|
|
51
51
|
## Use with AI
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
git clone https://github.com/
|
|
54
|
+
git clone https://github.com/pachterlab/paperpush.git
|
|
55
55
|
cd paperpush
|
|
56
56
|
```
|
|
57
57
|
|
|
@@ -97,6 +97,7 @@ the tool list and the contract agents should follow.
|
|
|
97
97
|
|
|
98
98
|
Helper commands:
|
|
99
99
|
- `paperpush requirements VENUE`: print VENUE's author guidelines, if available.
|
|
100
|
+
- `paperpush update-venues`: Update venues.json and manuscript_requirements.json from the remote venue-data branch.
|
|
100
101
|
- `paperpush options VENUE.FIELD`: print the allowed values for FIELD in VENUE's submission form.
|
|
101
102
|
|
|
102
103
|
See the YouTube tutorial [PaperPush Tutorial](https://youtu.be/ChCYoud0uBk)
|
|
@@ -123,6 +124,8 @@ See `python scripts/paperpush_pipeline.py --help` for the full list of options,
|
|
|
123
124
|
|
|
124
125
|
View the list on the command line with `paperpush --venues`.
|
|
125
126
|
|
|
127
|
+
Venue details (field options, limits, author-guideline rules) are updated independently of releases: paperpush fetches the latest published data at most once a day, and `paperpush update-venues` fetches it right away. New venues arrive with new releases (`pip install -U paperpush`).
|
|
128
|
+
|
|
126
129
|
For more details, see [`venues.md`](venues.md)
|
|
127
130
|
|
|
128
131
|
## Documentation
|
|
@@ -19,7 +19,7 @@ playwright install chromium
|
|
|
19
19
|
## Use with AI
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
git clone https://github.com/
|
|
22
|
+
git clone https://github.com/pachterlab/paperpush.git
|
|
23
23
|
cd paperpush
|
|
24
24
|
```
|
|
25
25
|
|
|
@@ -65,6 +65,7 @@ the tool list and the contract agents should follow.
|
|
|
65
65
|
|
|
66
66
|
Helper commands:
|
|
67
67
|
- `paperpush requirements VENUE`: print VENUE's author guidelines, if available.
|
|
68
|
+
- `paperpush update-venues`: Update venues.json and manuscript_requirements.json from the remote venue-data branch.
|
|
68
69
|
- `paperpush options VENUE.FIELD`: print the allowed values for FIELD in VENUE's submission form.
|
|
69
70
|
|
|
70
71
|
See the YouTube tutorial [PaperPush Tutorial](https://youtu.be/ChCYoud0uBk)
|
|
@@ -91,6 +92,8 @@ See `python scripts/paperpush_pipeline.py --help` for the full list of options,
|
|
|
91
92
|
|
|
92
93
|
View the list on the command line with `paperpush --venues`.
|
|
93
94
|
|
|
95
|
+
Venue details (field options, limits, author-guideline rules) are updated independently of releases: paperpush fetches the latest published data at most once a day, and `paperpush update-venues` fetches it right away. New venues arrive with new releases (`pip install -U paperpush`).
|
|
96
|
+
|
|
94
97
|
For more details, see [`venues.md`](venues.md)
|
|
95
98
|
|
|
96
99
|
## Documentation
|
|
@@ -20,6 +20,7 @@ Implemented so far:
|
|
|
20
20
|
paperpush login --orcid <j> store an ORCID iD/password for a journal that
|
|
21
21
|
offers "Sign in with ORCID"
|
|
22
22
|
paperpush submit <subfile> open bioRxiv and run the submission
|
|
23
|
+
paperpush update-venues fetch the latest published venue data now
|
|
23
24
|
paperpush --agent-guide print the guide for AI agents (AGENTS.md)
|
|
24
25
|
|
|
25
26
|
``submit`` drives the bioRxiv wizard, typing in the field values read from the
|
|
@@ -40,7 +41,7 @@ from pathlib import Path
|
|
|
40
41
|
|
|
41
42
|
from pydantic import ConfigDict, validate_call
|
|
42
43
|
|
|
43
|
-
from . import __url__, __version__, credentials
|
|
44
|
+
from . import __url__, __version__, credentials, venue_data
|
|
44
45
|
from ._logging import configure_logging
|
|
45
46
|
from .database import get_venue, list_venues
|
|
46
47
|
from .subfile import default_filename, write_template
|
|
@@ -112,6 +113,37 @@ def _print_venues() -> int:
|
|
|
112
113
|
print(f" {venue.name} ({venue.slug})")
|
|
113
114
|
|
|
114
115
|
print(f"\nFor more details, see {_VENUES_URL}")
|
|
116
|
+
print(f"Venue data: {venue_data.active_source().describe()} (refresh with 'paperpush update-venues')")
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@_validate
|
|
121
|
+
def _cmd_update_venues(args: argparse.Namespace) -> int:
|
|
122
|
+
"""Fetch the published venue data now, or clear the cached copy.
|
|
123
|
+
|
|
124
|
+
The data is otherwise refreshed on its own at most once a day; this is for
|
|
125
|
+
picking up a fix right away, and for seeing which copy is in use.
|
|
126
|
+
"""
|
|
127
|
+
from .database import reload
|
|
128
|
+
|
|
129
|
+
if args.clear:
|
|
130
|
+
venue_data.clear_cache()
|
|
131
|
+
print(f"Removed the cached venue data; using the data {venue_data.bundled_source().describe()}.")
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
if not venue_data.uses_published():
|
|
135
|
+
print("note: published venue data is not used here (PAPERPUSH_VENUE_DATA, or a source " "checkout); fetching it into the cache anyway.", file=sys.stderr)
|
|
136
|
+
try:
|
|
137
|
+
result = venue_data.refresh(force=True)
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
print(f"error: could not fetch the venue data from {venue_data.data_url()}: {exc}", file=sys.stderr)
|
|
140
|
+
return 1
|
|
141
|
+
reload()
|
|
142
|
+
print(result.message if result.status != "incompatible" else f"warning: {result.message}")
|
|
143
|
+
source = venue_data.active_source()
|
|
144
|
+
print(f"In use: {source.describe()}")
|
|
145
|
+
if source.kind != "bundled":
|
|
146
|
+
print(f"Files: {source.root}")
|
|
115
147
|
return 0
|
|
116
148
|
|
|
117
149
|
|
|
@@ -946,6 +978,7 @@ def _cmd_requirements(args: argparse.Namespace) -> int:
|
|
|
946
978
|
print(f" {url}")
|
|
947
979
|
if reqs.inherits:
|
|
948
980
|
print(f" (starts from {reqs.inherits}'s rules)")
|
|
981
|
+
print(f" venue data: {venue_data.active_source().describe()}")
|
|
949
982
|
if article_types and not args.article_type:
|
|
950
983
|
print(f" article types with their own rules: {', '.join(article_types)} (use --article-type)")
|
|
951
984
|
for section in ("manuscript", "title_page", "abstract", "keywords", "sections", "statements", "figures", "tables", "supplementary", "references", "cover_letter", "upload"):
|
|
@@ -1050,7 +1083,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1050
1083
|
|
|
1051
1084
|
# metavar lists only the public commands; the internal 'schema' command is
|
|
1052
1085
|
# registered below but deliberately left out so it does not appear in --help.
|
|
1053
|
-
sub = parser.add_subparsers(dest="command", metavar="{subfile,options,autofill,validate,requirements,login,submit}")
|
|
1086
|
+
sub = parser.add_subparsers(dest="command", metavar="{subfile,options,autofill,validate,requirements,login,submit,update-venues}")
|
|
1054
1087
|
|
|
1055
1088
|
p_subfile = sub.add_parser("subfile", parents=[verbosity], help="create a <venue>.sub submission template")
|
|
1056
1089
|
p_subfile.add_argument("venue", help="venue slug, e.g. biorxiv")
|
|
@@ -1172,6 +1205,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1172
1205
|
)
|
|
1173
1206
|
p_submit.set_defaults(func=_cmd_submit)
|
|
1174
1207
|
|
|
1208
|
+
p_update = sub.add_parser("update-venues", parents=[verbosity], help="fetch the latest published venue data (otherwise refreshed daily)")
|
|
1209
|
+
p_update.add_argument("--clear", action="store_true", help="delete the cached copy and use the data bundled with this version")
|
|
1210
|
+
p_update.set_defaults(func=_cmd_update_venues)
|
|
1211
|
+
|
|
1175
1212
|
# Internal command: the autofill front-ends (the Claude skill and the API
|
|
1176
1213
|
# engine) read field roles from here. Hidden from --help (use 'subfile' to
|
|
1177
1214
|
# inspect a venue's fields by hand).
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
The database is a single JSON file (``venues.json``) shipped with the
|
|
4
4
|
package. Each top-level key is a venue slug (e.g. ``"biorxiv"``) mapping to a
|
|
5
5
|
venue definition that lists the fields required for submission.
|
|
6
|
+
|
|
7
|
+
A newer copy may also be published separately from releases and cached locally
|
|
8
|
+
(see :mod:`paperpush.venue_data`). The bundled file is always the baseline: a
|
|
9
|
+
published entry replaces a bundled one only when the installed runner can
|
|
10
|
+
drive it (see :func:`merge_published`).
|
|
6
11
|
"""
|
|
7
12
|
|
|
8
13
|
from __future__ import annotations
|
|
@@ -18,8 +23,12 @@ from typing import Annotated, Any, Literal, Optional
|
|
|
18
23
|
|
|
19
24
|
from pydantic import Field as PField
|
|
20
25
|
|
|
26
|
+
from . import venue_data
|
|
27
|
+
|
|
21
28
|
logger = logging.getLogger(__name__)
|
|
22
29
|
|
|
30
|
+
# The copies shipped with the package. The files actually read may come from a
|
|
31
|
+
# published copy instead; see paperpush.venue_data.
|
|
23
32
|
DATABASE_PATH = Path(__file__).with_name("venues.json")
|
|
24
33
|
|
|
25
34
|
# Directory holding shared keyword/vocabulary lists that a field can pull in via
|
|
@@ -36,7 +45,10 @@ def _load_options_file(name: str) -> list[str]:
|
|
|
36
45
|
portal keywords or the PLOS Computational Biology classifications) lives in
|
|
37
46
|
one shared file rather than being inlined in ``venues.json``.
|
|
38
47
|
"""
|
|
39
|
-
|
|
48
|
+
# A published copy may add or update a vocabulary; the bundled one backs it.
|
|
49
|
+
path = venue_data.active_source().assets / name
|
|
50
|
+
if not path.is_file():
|
|
51
|
+
path = ASSETS_DIR / name
|
|
40
52
|
logger.debug("Loading field options from %s", path)
|
|
41
53
|
lines = [line.strip() for line in path.read_text(encoding="utf-8").splitlines()]
|
|
42
54
|
return [line for line in lines if line]
|
|
@@ -444,15 +456,118 @@ class Venue:
|
|
|
444
456
|
)
|
|
445
457
|
|
|
446
458
|
|
|
459
|
+
def _read_database(path: Path) -> dict[str, Any]:
|
|
460
|
+
logger.debug("Loading venue database from %s", path)
|
|
461
|
+
with path.open(encoding="utf-8") as fh:
|
|
462
|
+
return json.load(fh)
|
|
463
|
+
|
|
464
|
+
|
|
447
465
|
@lru_cache(maxsize=1)
|
|
448
466
|
def _load_raw() -> dict[str, Any]:
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
467
|
+
source = venue_data.active_source()
|
|
468
|
+
bundled = _read_database(DATABASE_PATH)
|
|
469
|
+
if source.kind == "bundled":
|
|
470
|
+
data = bundled
|
|
471
|
+
else:
|
|
472
|
+
try:
|
|
473
|
+
published = _read_database(source.path(venue_data.VENUES_FILE))
|
|
474
|
+
except (OSError, ValueError) as exc:
|
|
475
|
+
logger.warning("Could not read the venue data from %s (%s); using the bundled copy", source.root, exc)
|
|
476
|
+
published = bundled
|
|
477
|
+
# A local override is the author's own data, used as is; a published
|
|
478
|
+
# copy is merged venue by venue onto what this version can drive.
|
|
479
|
+
data = published if source.kind == "override" else merge_published(bundled, published)
|
|
480
|
+
logger.info("Loaded %d venue entr(y/ies) from the database (%s)", len(data), source.describe())
|
|
453
481
|
return data
|
|
454
482
|
|
|
455
483
|
|
|
484
|
+
def _field_shape(slug: str, raw: dict[str, Any]) -> list[tuple[str, str]]:
|
|
485
|
+
"""The (id, type) of each field of ``slug``, after inheritance, in order.
|
|
486
|
+
|
|
487
|
+
This is what a venue's runner is written against: it reads fields by id and
|
|
488
|
+
fills each according to its type. Everything else about a field -- label,
|
|
489
|
+
help, options, limits, whether it is required -- is data the runner does not
|
|
490
|
+
hard-code, so it can change without a code release.
|
|
491
|
+
"""
|
|
492
|
+
return [(f.get("id", ""), f.get("type", "text")) for f in _resolve_entry(slug, raw).get("fields", [])]
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def merge_published(bundled: dict[str, Any], published: dict[str, Any]) -> dict[str, Any]:
|
|
496
|
+
"""Overlay a published venue database onto the bundled one, venue by venue.
|
|
497
|
+
|
|
498
|
+
A published entry replaces the bundled entry only when
|
|
499
|
+
|
|
500
|
+
* the bundled database has the venue (a new venue needs its runner, which
|
|
501
|
+
only a release can bring),
|
|
502
|
+
* its fields keep the bundled shape (:func:`_field_shape`) -- adding,
|
|
503
|
+
removing, renaming, reordering or retyping a field needs runner changes,
|
|
504
|
+
* it resolves (e.g. its ``inherits`` base exists), and
|
|
505
|
+
* any venue it inherits from is itself taken from the published copy, so an
|
|
506
|
+
entry is never resolved against a mix of old and new bases.
|
|
507
|
+
|
|
508
|
+
Every other venue keeps its bundled entry, and a note is logged so a user on
|
|
509
|
+
an old version knows an upgrade would bring more.
|
|
510
|
+
"""
|
|
511
|
+
accepted: set[str] = set()
|
|
512
|
+
for slug in bundled:
|
|
513
|
+
if slug not in published:
|
|
514
|
+
continue
|
|
515
|
+
try:
|
|
516
|
+
if _field_shape(slug, published) == _field_shape(slug, bundled):
|
|
517
|
+
accepted.add(slug)
|
|
518
|
+
except KeyError as exc:
|
|
519
|
+
logger.warning("Published venue data: %s does not resolve (%s); keeping the bundled entry", slug, exc)
|
|
520
|
+
|
|
521
|
+
# Drop entries whose base was not accepted, until nothing changes (a chain
|
|
522
|
+
# of inheritance can take several passes).
|
|
523
|
+
changed = True
|
|
524
|
+
while changed:
|
|
525
|
+
changed = False
|
|
526
|
+
for slug in sorted(accepted):
|
|
527
|
+
base = str(published[slug].get("inherits", "") or "")
|
|
528
|
+
if base and base not in accepted and base.lower() not in accepted:
|
|
529
|
+
accepted.discard(slug)
|
|
530
|
+
changed = True
|
|
531
|
+
|
|
532
|
+
held_back = sorted(set(published) - accepted)
|
|
533
|
+
if held_back:
|
|
534
|
+
logger.info("Published venue data for %s needs a newer paperpush; using the bundled entries. " "Run 'pip install -U paperpush' to get them.", ", ".join(held_back))
|
|
535
|
+
return {slug: published[slug] if slug in accepted else entry for slug, entry in bundled.items()}
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def check_venue_data(root: Path) -> None:
|
|
539
|
+
"""Raise if the venue data in directory ``root`` does not load.
|
|
540
|
+
|
|
541
|
+
Run on a downloaded copy before it replaces the cache: every venue must
|
|
542
|
+
resolve and build, every ``options_file`` it names must exist, and the
|
|
543
|
+
manuscript requirements must load too.
|
|
544
|
+
"""
|
|
545
|
+
from .requirements import check_requirements_file
|
|
546
|
+
|
|
547
|
+
raw = _read_database(root / venue_data.VENUES_FILE)
|
|
548
|
+
if not isinstance(raw, dict) or not raw:
|
|
549
|
+
raise ValueError("venues.json holds no venues")
|
|
550
|
+
for slug in raw:
|
|
551
|
+
entry = _resolve_entry(slug, raw)
|
|
552
|
+
Venue.from_dict(slug, {**entry, "fields": []})
|
|
553
|
+
for f in entry.get("fields", []):
|
|
554
|
+
name = f.get("options_file")
|
|
555
|
+
if name and not (root / venue_data.ASSETS_SUBDIR / name).is_file() and not (ASSETS_DIR / name).is_file():
|
|
556
|
+
raise ValueError(f"{slug}.{f.get('id')} names a missing options_file {name!r}")
|
|
557
|
+
Field.from_dict({k: v for k, v in f.items() if k != "options_file"})
|
|
558
|
+
check_requirements_file(root / venue_data.REQUIREMENTS_FILE)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def reload() -> None:
|
|
562
|
+
"""Re-resolve the venue data source and drop every cached load of it."""
|
|
563
|
+
from . import requirements
|
|
564
|
+
|
|
565
|
+
venue_data.reset()
|
|
566
|
+
_load_raw.cache_clear()
|
|
567
|
+
_load_options_file.cache_clear()
|
|
568
|
+
requirements._load_raw.cache_clear()
|
|
569
|
+
|
|
570
|
+
|
|
456
571
|
# Keys an inheriting entry uses to declare how it derives from its base rather
|
|
457
572
|
# than to describe a key of the resulting venue; they are consumed by
|
|
458
573
|
# _resolve_entry and not copied verbatim onto the merged venue.
|
|
@@ -637,7 +637,7 @@ def _resolve_doi(doi: str, *, timeout: float = DOI_CHECK_TIMEOUT) -> tuple[str,
|
|
|
637
637
|
},
|
|
638
638
|
)
|
|
639
639
|
try:
|
|
640
|
-
with urllib.request.urlopen(req, timeout=timeout) as resp: #
|
|
640
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp: # scheme is a literal https above # nosec B310
|
|
641
641
|
payload = resp.read(MAX_TEXT_BYTES)
|
|
642
642
|
except urllib.error.HTTPError as exc:
|
|
643
643
|
if exc.code in _MISSING_STATUS:
|
|
@@ -40,9 +40,14 @@ from pathlib import Path
|
|
|
40
40
|
from typing import Annotated, Any, Literal, Optional
|
|
41
41
|
|
|
42
42
|
from pydantic import Field as PField
|
|
43
|
+
from pydantic import TypeAdapter, ValidationError
|
|
44
|
+
|
|
45
|
+
from . import venue_data
|
|
43
46
|
|
|
44
47
|
logger = logging.getLogger(__name__)
|
|
45
48
|
|
|
49
|
+
# The copy shipped with the package; the file actually read may be a newer
|
|
50
|
+
# published copy (see paperpush.venue_data).
|
|
46
51
|
REQUIREMENTS_PATH = Path(__file__).with_name("manuscript_requirements.json")
|
|
47
52
|
|
|
48
53
|
# Where figures/tables sit for the initial submission.
|
|
@@ -383,14 +388,34 @@ def _section_to_dict(section) -> dict[str, Any]:
|
|
|
383
388
|
return out
|
|
384
389
|
|
|
385
390
|
|
|
391
|
+
def _read_requirements(path: Path) -> dict[str, Any]:
|
|
392
|
+
logger.debug("Loading manuscript requirements from %s", path)
|
|
393
|
+
with path.open(encoding="utf-8") as fh:
|
|
394
|
+
data = json.load(fh)
|
|
395
|
+
if not isinstance(data, dict):
|
|
396
|
+
raise ValueError(f"{path} is not a JSON object")
|
|
397
|
+
return data
|
|
398
|
+
|
|
399
|
+
|
|
386
400
|
@lru_cache(maxsize=1)
|
|
387
401
|
def _load_raw() -> dict[str, Any]:
|
|
388
|
-
|
|
402
|
+
source = venue_data.active_source()
|
|
389
403
|
if not REQUIREMENTS_PATH.exists():
|
|
390
404
|
logger.warning("No manuscript requirements database at %s", REQUIREMENTS_PATH)
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
405
|
+
bundled: dict[str, Any] = {}
|
|
406
|
+
else:
|
|
407
|
+
bundled = _read_requirements(REQUIREMENTS_PATH)
|
|
408
|
+
data = bundled
|
|
409
|
+
if source.kind != "bundled":
|
|
410
|
+
path = source.path(venue_data.REQUIREMENTS_FILE)
|
|
411
|
+
try:
|
|
412
|
+
published = _read_requirements(path)
|
|
413
|
+
except (OSError, ValueError) as exc:
|
|
414
|
+
logger.warning("Could not read %s (%s); using the bundled copy", path, exc)
|
|
415
|
+
else:
|
|
416
|
+
# A local override is the author's own data, used as is; a published
|
|
417
|
+
# copy is merged entry by entry onto what this version can read.
|
|
418
|
+
data = published if source.kind == "override" else merge_published(bundled, published)
|
|
394
419
|
logger.info("Loaded %d manuscript requirement entr(y/ies)", sum(1 for k in data if not k.startswith("$")))
|
|
395
420
|
return data
|
|
396
421
|
|
|
@@ -462,6 +487,144 @@ def _resolve_entry(slug: str, raw: dict[str, Any], seen: tuple[str, ...] = ()) -
|
|
|
462
487
|
return merged
|
|
463
488
|
|
|
464
489
|
|
|
490
|
+
# Keys an entry may carry at its top level (``slug`` is the database key, not a
|
|
491
|
+
# key inside the entry), and keys an ``article_types`` override may carry.
|
|
492
|
+
_ENTRY_KEYS = frozenset(f.name for f in dataclass_fields(ManuscriptRequirements)) - {"slug"}
|
|
493
|
+
_OVERRIDE_KEYS = frozenset(SECTION_TYPES) | {"notes"}
|
|
494
|
+
_ALIASES_ADAPTER = TypeAdapter(dict[str, dict[str, list[str]]])
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@lru_cache(maxsize=None)
|
|
498
|
+
def _adapter(typ: type) -> TypeAdapter:
|
|
499
|
+
return TypeAdapter(typ)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _without_nulls(section: dict[str, Any]) -> dict[str, Any]:
|
|
503
|
+
# A null drops an inherited key (see merge_entries), so it is always allowed.
|
|
504
|
+
return {k: v for k, v in section.items() if v is not None}
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _rules_problem(rules: Any, allowed: frozenset[str], where: str) -> Optional[str]:
|
|
508
|
+
"""Why this version cannot read ``rules`` (an entry or an override), or None."""
|
|
509
|
+
if not isinstance(rules, dict):
|
|
510
|
+
return f"{where} is not an object"
|
|
511
|
+
unknown = sorted(set(rules) - allowed)
|
|
512
|
+
if unknown:
|
|
513
|
+
return f"{where} has key(s) this version does not know: {', '.join(unknown)}"
|
|
514
|
+
for key, typ in SECTION_TYPES.items():
|
|
515
|
+
section = rules.get(key)
|
|
516
|
+
if section is None:
|
|
517
|
+
continue
|
|
518
|
+
if not isinstance(section, dict):
|
|
519
|
+
return f"{where}.{key} is not an object"
|
|
520
|
+
known = {f.name for f in dataclass_fields(typ)}
|
|
521
|
+
unknown = sorted(set(section) - known)
|
|
522
|
+
if unknown:
|
|
523
|
+
return f"{where}.{key} has rule(s) this version does not know: {', '.join(unknown)}"
|
|
524
|
+
try:
|
|
525
|
+
_adapter(typ).validate_python(_without_nulls(section))
|
|
526
|
+
except ValidationError as exc:
|
|
527
|
+
return f"{where}.{key} does not match this version's rule types ({exc.errors()[0]['msg']})"
|
|
528
|
+
if not isinstance(rules.get("notes") or [], list):
|
|
529
|
+
return f"{where}.notes is not a list"
|
|
530
|
+
return None
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _entry_problem(slug: str, entry: Any) -> Optional[str]:
|
|
534
|
+
"""Why this version cannot apply the published ``entry``, or None if it can.
|
|
535
|
+
|
|
536
|
+
The rules are only data, but ``validate`` is code: it measures the uploads
|
|
537
|
+
against the keys and value types the installed section dataclasses define. A
|
|
538
|
+
newer copy may add a rule this version would silently skip, or change a
|
|
539
|
+
value's type in a way that would break the check at validation time, so
|
|
540
|
+
such an entry is caught here, when the copy is loaded, instead.
|
|
541
|
+
"""
|
|
542
|
+
if slug.startswith("$"):
|
|
543
|
+
if slug == "$aliases":
|
|
544
|
+
try:
|
|
545
|
+
_ALIASES_ADAPTER.validate_python(entry)
|
|
546
|
+
except ValidationError as exc:
|
|
547
|
+
return f"$aliases does not match this version's shape ({exc.errors()[0]['msg']})"
|
|
548
|
+
return None
|
|
549
|
+
problem = _rules_problem(entry, _ENTRY_KEYS, slug)
|
|
550
|
+
if problem:
|
|
551
|
+
return problem
|
|
552
|
+
top = {k: entry[k] for k in ("source_urls", "retrieved", "inherits", "article_type_field", "notes") if entry.get(k) is not None}
|
|
553
|
+
try:
|
|
554
|
+
_adapter(ManuscriptRequirements).validate_python({"slug": slug, **top})
|
|
555
|
+
except ValidationError as exc:
|
|
556
|
+
return f"{slug} does not match this version's entry types ({exc.errors()[0]['msg']})"
|
|
557
|
+
overrides = entry.get("article_types") or {}
|
|
558
|
+
if not isinstance(overrides, dict):
|
|
559
|
+
return f"{slug}.article_types is not an object"
|
|
560
|
+
for name, override in overrides.items():
|
|
561
|
+
problem = _rules_problem(override, _OVERRIDE_KEYS, f"{slug}.article_types[{name!r}]")
|
|
562
|
+
if problem:
|
|
563
|
+
return problem
|
|
564
|
+
return None
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def merge_published(bundled: dict[str, Any], published: dict[str, Any]) -> dict[str, Any]:
|
|
568
|
+
"""Overlay a published requirements database onto the bundled one, entry by entry.
|
|
569
|
+
|
|
570
|
+
The published copy is authoritative for the data: it may add, change, or
|
|
571
|
+
drop an entry (no runner depends on these rules, so, unlike a venue in
|
|
572
|
+
``venues.json``, a new entry needs no release). Each entry is still checked
|
|
573
|
+
against this version's rule vocabulary (:func:`_entry_problem`). An entry
|
|
574
|
+
this version cannot read keeps its bundled version, if there is one. So does
|
|
575
|
+
an entry that inherits from a held-back or missing base, so an entry is never
|
|
576
|
+
resolved against a mix of old and new rules.
|
|
577
|
+
"""
|
|
578
|
+
held_back: dict[str, str] = {}
|
|
579
|
+
accepted: set[str] = set()
|
|
580
|
+
for slug, entry in published.items():
|
|
581
|
+
problem = _entry_problem(slug, entry)
|
|
582
|
+
if problem:
|
|
583
|
+
held_back[slug] = problem
|
|
584
|
+
else:
|
|
585
|
+
accepted.add(slug)
|
|
586
|
+
|
|
587
|
+
# Drop entries whose base was not accepted, until nothing changes (a chain
|
|
588
|
+
# of inheritance can take several passes).
|
|
589
|
+
lowered = {s.lower(): s for s in accepted}
|
|
590
|
+
changed = True
|
|
591
|
+
while changed:
|
|
592
|
+
changed = False
|
|
593
|
+
for slug in sorted(accepted):
|
|
594
|
+
base = str(published[slug].get("inherits", "") or "") if not slug.startswith("$") else ""
|
|
595
|
+
if base and base.lower() not in lowered:
|
|
596
|
+
accepted.discard(slug)
|
|
597
|
+
lowered.pop(slug.lower(), None)
|
|
598
|
+
held_back[slug] = f"{slug} inherits from {base!r}, which is held back or missing"
|
|
599
|
+
changed = True
|
|
600
|
+
|
|
601
|
+
for slug, problem in sorted(held_back.items()):
|
|
602
|
+
logger.info("Published manuscript requirements: %s; using the bundled entry. " "Run 'pip install -U paperpush' to get it.", problem)
|
|
603
|
+
|
|
604
|
+
merged: dict[str, Any] = {}
|
|
605
|
+
for slug, entry in published.items():
|
|
606
|
+
if slug in accepted:
|
|
607
|
+
merged[slug] = entry
|
|
608
|
+
elif slug in bundled:
|
|
609
|
+
merged[slug] = bundled[slug]
|
|
610
|
+
return merged
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def check_requirements_file(path: Path) -> None:
|
|
614
|
+
"""Raise if the requirements database at ``path`` does not load.
|
|
615
|
+
|
|
616
|
+
Checks the database this version would actually use -- the file merged onto
|
|
617
|
+
the bundled copy by :func:`merge_published` -- so every entry must resolve
|
|
618
|
+
and build.
|
|
619
|
+
"""
|
|
620
|
+
published = _read_requirements(path)
|
|
621
|
+
bundled = _read_requirements(REQUIREMENTS_PATH) if REQUIREMENTS_PATH.exists() else {}
|
|
622
|
+
raw = merge_published(bundled, published)
|
|
623
|
+
for slug in raw:
|
|
624
|
+
if not slug.startswith("$"):
|
|
625
|
+
ManuscriptRequirements.from_dict(slug, _resolve_entry(slug, raw))
|
|
626
|
+
|
|
627
|
+
|
|
465
628
|
def list_requirements() -> list[ManuscriptRequirements]:
|
|
466
629
|
"""Every venue entry in the database, sorted by slug."""
|
|
467
630
|
raw = _load_raw()
|
|
@@ -694,7 +694,7 @@ def _url_is_reachable(url: str, *, timeout: float = LINK_CHECK_TIMEOUT) -> bool
|
|
|
694
694
|
for method in ("HEAD", "GET"):
|
|
695
695
|
req = urllib.request.Request(url, method=method, headers={"User-Agent": "paperpush-validate"})
|
|
696
696
|
try:
|
|
697
|
-
with urllib.request.urlopen(req, timeout=timeout): #
|
|
697
|
+
with urllib.request.urlopen(req, timeout=timeout): # scheme constrained to http(s) by _URL_RE # nosec B310
|
|
698
698
|
return True
|
|
699
699
|
except urllib.error.HTTPError as exc:
|
|
700
700
|
if exc.code in _BROKEN_STATUS:
|