agentic-scholarone 0.1.0__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.
- agentic_scholarone-0.1.0/.github/workflows/ci.yml +17 -0
- agentic_scholarone-0.1.0/.github/workflows/publish.yml +17 -0
- agentic_scholarone-0.1.0/.gitignore +16 -0
- agentic_scholarone-0.1.0/LICENSE +21 -0
- agentic_scholarone-0.1.0/PKG-INFO +207 -0
- agentic_scholarone-0.1.0/README.md +182 -0
- agentic_scholarone-0.1.0/agentic_scholarone/__init__.py +20 -0
- agentic_scholarone-0.1.0/agentic_scholarone/_session.py +3286 -0
- agentic_scholarone-0.1.0/pyproject.toml +45 -0
- agentic_scholarone-0.1.0/tests/test_extension_points.py +113 -0
- agentic_scholarone-0.1.0/tests/test_parsing.py +86 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
jobs:
|
|
7
|
+
test:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: actions/setup-python@v5
|
|
12
|
+
with:
|
|
13
|
+
python-version: "3.13"
|
|
14
|
+
- run: pip install -e ".[keyring,dev]"
|
|
15
|
+
- run: ruff check .
|
|
16
|
+
- run: mypy agentic_scholarone
|
|
17
|
+
- run: pytest --cov=agentic_scholarone
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
jobs:
|
|
6
|
+
publish:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
environment: pypi
|
|
9
|
+
permissions:
|
|
10
|
+
id-token: write # trusted publishing, no token
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.13"
|
|
16
|
+
- run: pip install build && python -m build
|
|
17
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Martin Monperrus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agentic-scholarone
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drive ScholarOne / ManuscriptCentral as a journal editor: a Python library for agents and an interactive REPL for humans
|
|
5
|
+
Project-URL: Homepage, https://github.com/monperrus/agentic-scholarone
|
|
6
|
+
Author: Martin Monperrus
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agent,editor,manuscriptcentral,peer review,scholarone
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
15
|
+
Requires-Dist: curl-cffi>=0.7
|
|
16
|
+
Requires-Dist: tqdm>=4.66
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
22
|
+
Provides-Extra: keyring
|
|
23
|
+
Requires-Dist: keyring>=24; extra == 'keyring'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# agentic-scholarone
|
|
27
|
+
|
|
28
|
+
Drive [ScholarOne Manuscripts](https://mc.manuscriptcentral.com) (ManuscriptCentral) as a journal
|
|
29
|
+
editor, without a browser. You can use it two ways:
|
|
30
|
+
|
|
31
|
+
* **as a library**, for coding agents and scripts: list your queues, read manuscripts, reviews and
|
|
32
|
+
decision letters, search and screen reviewers, and send invitations;
|
|
33
|
+
* **as an interactive REPL**, for humans at the keyboard: `agentic-scholarone`.
|
|
34
|
+
|
|
35
|
+
ScholarOne has no public API for editors. This client speaks the web app's own protocol: form POSTs
|
|
36
|
+
that carry the page's hidden `XIK_*` tokens, plus `popWindow(...)` popups fetched with GETs. The
|
|
37
|
+
HTTP layer is [`curl_cffi`](https://github.com/lexiforest/curl_cffi) with a consistent Chrome
|
|
38
|
+
fingerprint, so the login's device check treats the client as an ordinary browser.
|
|
39
|
+
|
|
40
|
+
> ⚠️ ScholarOne is a live production system with real authors and reviewers on the other end.
|
|
41
|
+
> Reading needs no care. **Invitations are real emails**: have a human confirm every one.
|
|
42
|
+
> Reuse one session instead of logging in per call, because each fresh login can send a
|
|
43
|
+
> verification code to the editor's inbox.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install agentic-scholarone # library + REPL
|
|
49
|
+
pip install "agentic-scholarone[keyring]" # + passwords from the OS secret store
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Python ≥ 3.10.
|
|
53
|
+
|
|
54
|
+
## Credentials
|
|
55
|
+
|
|
56
|
+
A `credentials.json` in the working directory (keep it out of version control). The client also
|
|
57
|
+
saves session cookies into it, so later runs resume without logging in again:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"journal": "abc-cs",
|
|
62
|
+
"email": "jane.doe@example.org",
|
|
63
|
+
"auth_method": "clarivate",
|
|
64
|
+
"password": "…"
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
* `journal`: the slug after `mc.manuscriptcentral.com/` in your journal's URL.
|
|
69
|
+
* `auth_method`: `"clarivate"` (default, Web of Science single sign-on) or `"direct"` (ScholarOne's
|
|
70
|
+
own login form, with your ScholarOne user id in `email` and the native password in
|
|
71
|
+
`direct_password`).
|
|
72
|
+
* Password lookup order: `MC_PASSWORD` env, then `password_keyring` (`"name"` or `"service/name"`,
|
|
73
|
+
read live from the OS secret store, so the file holds no password), then `direct_password`, then
|
|
74
|
+
`password`.
|
|
75
|
+
|
|
76
|
+
`MC_JOURNAL`, `MC_EMAIL`, `MC_PASSWORD`, `MC_AUTH` override the file.
|
|
77
|
+
|
|
78
|
+
**Two-factor codes.** When a login triggers the emailed device check, the code is taken from, in
|
|
79
|
+
order: `MC_2FA_CODE`, any provider you plug in (see below), the file named by `MC_2FA_CODE_FILE`
|
|
80
|
+
(polled for up to 15 min, so an agent can start the login and the code can be dropped in when the
|
|
81
|
+
mail arrives), then an interactive prompt.
|
|
82
|
+
|
|
83
|
+
## Use 1: library, for agents and scripts
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from agentic_scholarone import ScholarOneSession
|
|
87
|
+
|
|
88
|
+
mc = ScholarOneSession.from_credentials() # resumes saved cookies, else logs in
|
|
89
|
+
print(mc.ae_dashboard()) # papers needing reviewers, awaiting revision
|
|
90
|
+
|
|
91
|
+
# every queue you can see
|
|
92
|
+
link_map, src = mc._get_dashboard_link_map()
|
|
93
|
+
for queue in mc._ALL_QUEUE_KEYS:
|
|
94
|
+
if href := link_map.get(queue):
|
|
95
|
+
for p in mc.fetch_queue_papers_all_pages(href, src):
|
|
96
|
+
print(queue, p["ms_id"], p["counts"], p["stage"], p["title"][:60])
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`counts` reads `2 received / 1 waiting / 1 pending / 5 declined`. ScholarOne's own cell
|
|
100
|
+
(`3 invited; 2 agreed; 5 declined; 2 returned`) is cumulative, so the client turns it into
|
|
101
|
+
disjoint numbers. `stage` is the pending task, e.g. `Make Recommendation (due 28-Oct-2026)`.
|
|
102
|
+
|
|
103
|
+
The session is stateful. Navigate first, then read the page you are on.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
mc.navigate_to_any_manuscript("ABC-2026-04-0342")
|
|
107
|
+
meta = mc.current_manuscript_metadata() # ms_id, title, authors, abstract, keywords
|
|
108
|
+
pdf = mc.download_current_manuscript_pdf() # bytes
|
|
109
|
+
reviews = mc.current_reviews() # parsed score sheets
|
|
110
|
+
letters = mc.current_decision_letters() # [{label, text}]
|
|
111
|
+
mc.download_reviews_txt("ABC-2026-04-0342") # all reviews in one text file, headed by verdicts
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Reviewers:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
mc.navigate_to_reviewer_tab("ABC-2026-04-0342")
|
|
118
|
+
for row in mc.selected_reviewer_rows(): # already selected or invited
|
|
119
|
+
stats = mc.reviewer_history_analysis(row)
|
|
120
|
+
print(row["name"], row["status"], stats.get("average_turnaround_time"))
|
|
121
|
+
|
|
122
|
+
for c in mc.search_reviewer_by_name("Jane Doe"): # people usually have several accounts
|
|
123
|
+
info = mc.person_account_info(c) # email, affiliation, roles, keywords
|
|
124
|
+
stats = mc.reviewer_history_analysis(c) # agreed / declined / turnaround / ORCID
|
|
125
|
+
print(info.get("email"), stats.get("times_agreed"), stats.get("last_invitation_date"))
|
|
126
|
+
|
|
127
|
+
# outward-facing: sends the invitation email
|
|
128
|
+
result = mc.invite_by_email("ABC-2026-04-0342", "jane.doe@example.org")
|
|
129
|
+
print(result.ok, result.status, result.message) # result.ambiguous lists several matches
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
A row reading `Selected` was added but never mailed; `mc.send_pending_invitation(name)` sends it.
|
|
133
|
+
|
|
134
|
+
Every state-changing action is appended to `mc_actions.jsonl` in the state directory
|
|
135
|
+
(`MC_STATE_DIR`, default `~/.local/state/agentic-scholarone`); `mc.worklog.entries()` reads it back.
|
|
136
|
+
|
|
137
|
+
When a page parses to nothing, `MC_DEBUG=1` traces every navigation, and `mc.page_title()`,
|
|
138
|
+
`mc.page_text()`, `mc.list_links()`, `mc.show_form_fields()`, `mc.show_tables()` show what the
|
|
139
|
+
session is looking at.
|
|
140
|
+
|
|
141
|
+
## Use 2: interactive REPL, for humans
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
agentic-scholarone # reads credentials.json in the current directory
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
editor show all papers currently assigned to you as AE
|
|
149
|
+
select pick a paper by number and open its reviewer details
|
|
150
|
+
invitees show reviewers you've invited, sorted by turnaround
|
|
151
|
+
reviewers [ms_id] show invited reviewers + candidates for a manuscript
|
|
152
|
+
invite <ms_id> <email|name> search by email or "First Last" and send invitation
|
|
153
|
+
abstract <ms_id> show abstract for any accessible manuscript
|
|
154
|
+
download <ms_id> download proof PDF for any accessible manuscript
|
|
155
|
+
reviews <ms_id> download all reviews of a manuscript to one txt file
|
|
156
|
+
log [n|ms_id] show the last n logged state-changing actions
|
|
157
|
+
login (re)authenticate
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Extending: plug in your own infrastructure
|
|
161
|
+
|
|
162
|
+
Subclass `ScholarOneSession` and override what you need. The defaults are generic or do nothing.
|
|
163
|
+
|
|
164
|
+
| Extension point | Default | Typical override |
|
|
165
|
+
| --- | --- | --- |
|
|
166
|
+
| `default_journal` | `""` (then `MC_JOURNAL`) | your journal slug |
|
|
167
|
+
| `ms_id_pattern` | `[A-Z][A-Z0-9]+-\d{4}[-.\w]*` | your journal's id format |
|
|
168
|
+
| `keyring_service` | `agentic-scholarone` | your secret-store service name |
|
|
169
|
+
| `state_dir` | `~/.local/state/agentic-scholarone` | a project directory |
|
|
170
|
+
| `two_factor_code_providers()` | `[]` | read the code from a local mailbox |
|
|
171
|
+
| `invitation_paragraph(candidate, **opts)` | `""` | a paragraph added to every invitation |
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from agentic_scholarone import ScholarOneSession, main
|
|
175
|
+
|
|
176
|
+
class MyJournal(ScholarOneSession):
|
|
177
|
+
default_journal = "abc-cs"
|
|
178
|
+
ms_id_pattern = r"ABC-\d{4}-\d{2}-\d+[\w.]*"
|
|
179
|
+
|
|
180
|
+
def two_factor_code_providers(self):
|
|
181
|
+
return [read_code_from_my_mailbox] # fn(timeout_s) -> "123456" or ""
|
|
182
|
+
|
|
183
|
+
def invitation_paragraph(self, candidate, note="", **_):
|
|
184
|
+
return note # mc.invite_by_email(ms, email, note="…")
|
|
185
|
+
|
|
186
|
+
def cmd_hello(mc, arg): # an extra REPL command
|
|
187
|
+
print("hello", mc.journal, arg)
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
main(MyJournal, {"hello": cmd_hello})
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Keyword arguments given to `invite_by_email` / `invite_by_name` reach `invitation_paragraph`.
|
|
194
|
+
|
|
195
|
+
## Development
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
uv venv && uv pip install -e ".[keyring,dev]"
|
|
199
|
+
pytest && ruff check . && mypy agentic_scholarone
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Tests run on hand-written HTML fixtures. Never commit pages captured from a live session: they hold
|
|
203
|
+
the names of authors and reviewers.
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# agentic-scholarone
|
|
2
|
+
|
|
3
|
+
Drive [ScholarOne Manuscripts](https://mc.manuscriptcentral.com) (ManuscriptCentral) as a journal
|
|
4
|
+
editor, without a browser. You can use it two ways:
|
|
5
|
+
|
|
6
|
+
* **as a library**, for coding agents and scripts: list your queues, read manuscripts, reviews and
|
|
7
|
+
decision letters, search and screen reviewers, and send invitations;
|
|
8
|
+
* **as an interactive REPL**, for humans at the keyboard: `agentic-scholarone`.
|
|
9
|
+
|
|
10
|
+
ScholarOne has no public API for editors. This client speaks the web app's own protocol: form POSTs
|
|
11
|
+
that carry the page's hidden `XIK_*` tokens, plus `popWindow(...)` popups fetched with GETs. The
|
|
12
|
+
HTTP layer is [`curl_cffi`](https://github.com/lexiforest/curl_cffi) with a consistent Chrome
|
|
13
|
+
fingerprint, so the login's device check treats the client as an ordinary browser.
|
|
14
|
+
|
|
15
|
+
> ⚠️ ScholarOne is a live production system with real authors and reviewers on the other end.
|
|
16
|
+
> Reading needs no care. **Invitations are real emails**: have a human confirm every one.
|
|
17
|
+
> Reuse one session instead of logging in per call, because each fresh login can send a
|
|
18
|
+
> verification code to the editor's inbox.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install agentic-scholarone # library + REPL
|
|
24
|
+
pip install "agentic-scholarone[keyring]" # + passwords from the OS secret store
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Python ≥ 3.10.
|
|
28
|
+
|
|
29
|
+
## Credentials
|
|
30
|
+
|
|
31
|
+
A `credentials.json` in the working directory (keep it out of version control). The client also
|
|
32
|
+
saves session cookies into it, so later runs resume without logging in again:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"journal": "abc-cs",
|
|
37
|
+
"email": "jane.doe@example.org",
|
|
38
|
+
"auth_method": "clarivate",
|
|
39
|
+
"password": "…"
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
* `journal`: the slug after `mc.manuscriptcentral.com/` in your journal's URL.
|
|
44
|
+
* `auth_method`: `"clarivate"` (default, Web of Science single sign-on) or `"direct"` (ScholarOne's
|
|
45
|
+
own login form, with your ScholarOne user id in `email` and the native password in
|
|
46
|
+
`direct_password`).
|
|
47
|
+
* Password lookup order: `MC_PASSWORD` env, then `password_keyring` (`"name"` or `"service/name"`,
|
|
48
|
+
read live from the OS secret store, so the file holds no password), then `direct_password`, then
|
|
49
|
+
`password`.
|
|
50
|
+
|
|
51
|
+
`MC_JOURNAL`, `MC_EMAIL`, `MC_PASSWORD`, `MC_AUTH` override the file.
|
|
52
|
+
|
|
53
|
+
**Two-factor codes.** When a login triggers the emailed device check, the code is taken from, in
|
|
54
|
+
order: `MC_2FA_CODE`, any provider you plug in (see below), the file named by `MC_2FA_CODE_FILE`
|
|
55
|
+
(polled for up to 15 min, so an agent can start the login and the code can be dropped in when the
|
|
56
|
+
mail arrives), then an interactive prompt.
|
|
57
|
+
|
|
58
|
+
## Use 1: library, for agents and scripts
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from agentic_scholarone import ScholarOneSession
|
|
62
|
+
|
|
63
|
+
mc = ScholarOneSession.from_credentials() # resumes saved cookies, else logs in
|
|
64
|
+
print(mc.ae_dashboard()) # papers needing reviewers, awaiting revision
|
|
65
|
+
|
|
66
|
+
# every queue you can see
|
|
67
|
+
link_map, src = mc._get_dashboard_link_map()
|
|
68
|
+
for queue in mc._ALL_QUEUE_KEYS:
|
|
69
|
+
if href := link_map.get(queue):
|
|
70
|
+
for p in mc.fetch_queue_papers_all_pages(href, src):
|
|
71
|
+
print(queue, p["ms_id"], p["counts"], p["stage"], p["title"][:60])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`counts` reads `2 received / 1 waiting / 1 pending / 5 declined`. ScholarOne's own cell
|
|
75
|
+
(`3 invited; 2 agreed; 5 declined; 2 returned`) is cumulative, so the client turns it into
|
|
76
|
+
disjoint numbers. `stage` is the pending task, e.g. `Make Recommendation (due 28-Oct-2026)`.
|
|
77
|
+
|
|
78
|
+
The session is stateful. Navigate first, then read the page you are on.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
mc.navigate_to_any_manuscript("ABC-2026-04-0342")
|
|
82
|
+
meta = mc.current_manuscript_metadata() # ms_id, title, authors, abstract, keywords
|
|
83
|
+
pdf = mc.download_current_manuscript_pdf() # bytes
|
|
84
|
+
reviews = mc.current_reviews() # parsed score sheets
|
|
85
|
+
letters = mc.current_decision_letters() # [{label, text}]
|
|
86
|
+
mc.download_reviews_txt("ABC-2026-04-0342") # all reviews in one text file, headed by verdicts
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Reviewers:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
mc.navigate_to_reviewer_tab("ABC-2026-04-0342")
|
|
93
|
+
for row in mc.selected_reviewer_rows(): # already selected or invited
|
|
94
|
+
stats = mc.reviewer_history_analysis(row)
|
|
95
|
+
print(row["name"], row["status"], stats.get("average_turnaround_time"))
|
|
96
|
+
|
|
97
|
+
for c in mc.search_reviewer_by_name("Jane Doe"): # people usually have several accounts
|
|
98
|
+
info = mc.person_account_info(c) # email, affiliation, roles, keywords
|
|
99
|
+
stats = mc.reviewer_history_analysis(c) # agreed / declined / turnaround / ORCID
|
|
100
|
+
print(info.get("email"), stats.get("times_agreed"), stats.get("last_invitation_date"))
|
|
101
|
+
|
|
102
|
+
# outward-facing: sends the invitation email
|
|
103
|
+
result = mc.invite_by_email("ABC-2026-04-0342", "jane.doe@example.org")
|
|
104
|
+
print(result.ok, result.status, result.message) # result.ambiguous lists several matches
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A row reading `Selected` was added but never mailed; `mc.send_pending_invitation(name)` sends it.
|
|
108
|
+
|
|
109
|
+
Every state-changing action is appended to `mc_actions.jsonl` in the state directory
|
|
110
|
+
(`MC_STATE_DIR`, default `~/.local/state/agentic-scholarone`); `mc.worklog.entries()` reads it back.
|
|
111
|
+
|
|
112
|
+
When a page parses to nothing, `MC_DEBUG=1` traces every navigation, and `mc.page_title()`,
|
|
113
|
+
`mc.page_text()`, `mc.list_links()`, `mc.show_form_fields()`, `mc.show_tables()` show what the
|
|
114
|
+
session is looking at.
|
|
115
|
+
|
|
116
|
+
## Use 2: interactive REPL, for humans
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
agentic-scholarone # reads credentials.json in the current directory
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
editor show all papers currently assigned to you as AE
|
|
124
|
+
select pick a paper by number and open its reviewer details
|
|
125
|
+
invitees show reviewers you've invited, sorted by turnaround
|
|
126
|
+
reviewers [ms_id] show invited reviewers + candidates for a manuscript
|
|
127
|
+
invite <ms_id> <email|name> search by email or "First Last" and send invitation
|
|
128
|
+
abstract <ms_id> show abstract for any accessible manuscript
|
|
129
|
+
download <ms_id> download proof PDF for any accessible manuscript
|
|
130
|
+
reviews <ms_id> download all reviews of a manuscript to one txt file
|
|
131
|
+
log [n|ms_id] show the last n logged state-changing actions
|
|
132
|
+
login (re)authenticate
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Extending: plug in your own infrastructure
|
|
136
|
+
|
|
137
|
+
Subclass `ScholarOneSession` and override what you need. The defaults are generic or do nothing.
|
|
138
|
+
|
|
139
|
+
| Extension point | Default | Typical override |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| `default_journal` | `""` (then `MC_JOURNAL`) | your journal slug |
|
|
142
|
+
| `ms_id_pattern` | `[A-Z][A-Z0-9]+-\d{4}[-.\w]*` | your journal's id format |
|
|
143
|
+
| `keyring_service` | `agentic-scholarone` | your secret-store service name |
|
|
144
|
+
| `state_dir` | `~/.local/state/agentic-scholarone` | a project directory |
|
|
145
|
+
| `two_factor_code_providers()` | `[]` | read the code from a local mailbox |
|
|
146
|
+
| `invitation_paragraph(candidate, **opts)` | `""` | a paragraph added to every invitation |
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from agentic_scholarone import ScholarOneSession, main
|
|
150
|
+
|
|
151
|
+
class MyJournal(ScholarOneSession):
|
|
152
|
+
default_journal = "abc-cs"
|
|
153
|
+
ms_id_pattern = r"ABC-\d{4}-\d{2}-\d+[\w.]*"
|
|
154
|
+
|
|
155
|
+
def two_factor_code_providers(self):
|
|
156
|
+
return [read_code_from_my_mailbox] # fn(timeout_s) -> "123456" or ""
|
|
157
|
+
|
|
158
|
+
def invitation_paragraph(self, candidate, note="", **_):
|
|
159
|
+
return note # mc.invite_by_email(ms, email, note="…")
|
|
160
|
+
|
|
161
|
+
def cmd_hello(mc, arg): # an extra REPL command
|
|
162
|
+
print("hello", mc.journal, arg)
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
main(MyJournal, {"hello": cmd_hello})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Keyword arguments given to `invite_by_email` / `invite_by_name` reach `invitation_paragraph`.
|
|
169
|
+
|
|
170
|
+
## Development
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
uv venv && uv pip install -e ".[keyring,dev]"
|
|
174
|
+
pytest && ruff check . && mypy agentic_scholarone
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Tests run on hand-written HTML fixtures. Never commit pages captured from a live session: they hold
|
|
178
|
+
the names of authors and reviewers.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""agentic-scholarone: drive ScholarOne / ManuscriptCentral as an editor, from code or a REPL."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from agentic_scholarone._session import (
|
|
5
|
+
BASE_URL,
|
|
6
|
+
InviteResult,
|
|
7
|
+
ScholarOneSession,
|
|
8
|
+
WorkJournal,
|
|
9
|
+
load_credentials,
|
|
10
|
+
main,
|
|
11
|
+
run_repl,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
ManuscriptCentralSession = ScholarOneSession
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"BASE_URL", "InviteResult", "ManuscriptCentralSession", "ScholarOneSession",
|
|
18
|
+
"WorkJournal", "load_credentials", "main", "run_repl",
|
|
19
|
+
]
|
|
20
|
+
__version__ = "0.1.0"
|