overleaf-comments-export 0.3.0__tar.gz → 0.4.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.
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/PKG-INFO +51 -8
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/README.md +49 -7
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/__main__.py +76 -2
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/export.py +13 -1
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/gui.py +12 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/render.py +97 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export.egg-info/PKG-INFO +51 -8
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export.egg-info/SOURCES.txt +2 -1
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/pyproject.toml +6 -2
- overleaf_comments_export-0.4.0/tests/test_response_letter.py +111 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/LICENSE +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/__init__.py +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/anchors.py +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/client.py +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/model.py +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/sections.py +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export.egg-info/dependency_links.txt +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export.egg-info/entry_points.txt +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export.egg-info/requires.txt +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export.egg-info/top_level.txt +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/setup.cfg +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_anchors_sections.py +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_client.py +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_errors_and_auth.py +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_export.py +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_render.py +0 -0
- {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_replies_and_changes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: overleaf-comments-export
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Export comment threads and tracked changes from an Overleaf project to Markdown + JSON, optimized for AI-agent consumption.
|
|
5
5
|
Author: Shivang
|
|
6
6
|
License: MIT
|
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
23
|
Classifier: Topic :: Text Processing :: Markup :: LaTeX
|
|
23
24
|
Classifier: Topic :: Scientific/Engineering
|
|
24
25
|
Requires-Python: >=3.10
|
|
@@ -69,7 +70,13 @@ an Overleaf project URL and a logged-in browser session.
|
|
|
69
70
|
pip install overleaf-comments-export
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
Requires Python 3.10
|
|
73
|
+
Requires Python 3.10 or newer (tested up to 3.14). Works on macOS, Linux, and
|
|
74
|
+
Windows.
|
|
75
|
+
|
|
76
|
+
The graphical window needs Python's Tk toolkit, which most Linux distributions
|
|
77
|
+
package separately (`sudo apt install python3-tk` on Debian/Ubuntu). The
|
|
78
|
+
command line never needs it, and `--gui` tells you what to install if it is
|
|
79
|
+
missing.
|
|
73
80
|
|
|
74
81
|
## Quick start
|
|
75
82
|
|
|
@@ -104,6 +111,7 @@ In your output folder, by default:
|
|
|
104
111
|
| `comments.json` | Structured data — `summary`, top-level `threads`, `files`, `comments`, `tracked_changes`, etc. Schema described in `agents.md`. |
|
|
105
112
|
| `comments.jsonl` | One self-contained JSON record per comment for streaming/pipelines. |
|
|
106
113
|
| `agents.md` | A brief instruction file telling an AI agent how to consume the batch. |
|
|
114
|
+
| `response-letter.md` | (Optional, `--response-letter`) A point-by-point reply document, pre-filled with every open comment grouped by who raised it, with blanks for your response. |
|
|
107
115
|
| `by-reviewer/<name>.md` | (Optional, `--per-reviewer`) One Markdown per reviewer with only their threads. |
|
|
108
116
|
| `comments.log` | Diagnostic log for the run. |
|
|
109
117
|
|
|
@@ -121,6 +129,9 @@ overleaf-comments-export --project-url … --out ./out --render-mode detailed
|
|
|
121
129
|
|
|
122
130
|
# Per-reviewer sub-reports under ./out/by-reviewer/
|
|
123
131
|
overleaf-comments-export --project-url … --out ./out --per-reviewer
|
|
132
|
+
|
|
133
|
+
# Draft a point-by-point response letter for the open comments
|
|
134
|
+
overleaf-comments-export --project-url … --out ./out --response-letter
|
|
124
135
|
```
|
|
125
136
|
|
|
126
137
|
Full flag reference: `overleaf-comments-export --help`.
|
|
@@ -162,14 +173,46 @@ Treat it like a password; it stops working when you sign out.
|
|
|
162
173
|
| "Could not read Overleaf cookies from chrome" | Use the paste-the-cookie method above. |
|
|
163
174
|
| "Overleaf could not find that project" | Wrong link, or this account has no access. |
|
|
164
175
|
|
|
165
|
-
##
|
|
176
|
+
## Feedback, questions, and contributing
|
|
177
|
+
|
|
178
|
+
This tool is actively maintained, and feedback shapes what gets built next.
|
|
179
|
+
|
|
180
|
+
- **Something broke, or the output was wrong?**
|
|
181
|
+
[Open an issue.](https://github.com/Mangluu/overleaf-comments-export/issues/new/choose)
|
|
182
|
+
You do not need to be a programmer — paste what the tool said and that is
|
|
183
|
+
plenty. If Overleaf changes something, everything here stops working at once,
|
|
184
|
+
and you may be the first person to notice.
|
|
185
|
+
- **Want it to do something it doesn't?**
|
|
186
|
+
[Suggest a feature.](https://github.com/Mangluu/overleaf-comments-export/issues/new/choose)
|
|
187
|
+
Tell me what you are trying to do, not just the feature — the real task
|
|
188
|
+
usually leads somewhere better.
|
|
189
|
+
- **Just a question, or want to show what you built with it?**
|
|
190
|
+
[Discussions.](https://github.com/Mangluu/overleaf-comments-export/discussions)
|
|
191
|
+
- **Want to contribute code?** See [CONTRIBUTING.md](CONTRIBUTING.md). It takes
|
|
192
|
+
about two minutes to get the tests running, and there are items marked
|
|
193
|
+
*help wanted* in [ROADMAP.md](ROADMAP.md).
|
|
194
|
+
|
|
195
|
+
Never include your session cookie in an issue — it is a password for your
|
|
196
|
+
Overleaf account, and nobody needs it to fix a bug.
|
|
197
|
+
|
|
198
|
+
Maintained by [Shivang Gupta](https://github.com/Mangluu), who wrote it to deal
|
|
199
|
+
with the review comments on his own papers.
|
|
200
|
+
|
|
201
|
+
## What's coming next
|
|
202
|
+
|
|
203
|
+
See [ROADMAP.md](ROADMAP.md). Short version: a response-letter scaffold,
|
|
204
|
+
writing out the full source so an AI can see more than a snippet, and a diff
|
|
205
|
+
between two exports so you can work through review comments in waves.
|
|
206
|
+
|
|
207
|
+
Changes are recorded in [CHANGELOG.md](CHANGELOG.md).
|
|
166
208
|
|
|
167
|
-
|
|
168
|
-
It is provided as-is, with no guaranteed maintenance, no SLA, and no roadmap.
|
|
169
|
-
Pull requests are welcome; issues may or may not be acted upon.
|
|
209
|
+
## A caution
|
|
170
210
|
|
|
171
|
-
|
|
172
|
-
|
|
211
|
+
This tool uses Overleaf's internal endpoints, which are undocumented and can
|
|
212
|
+
change without notice. It identifies itself honestly in every request, backs
|
|
213
|
+
off when asked to, and only ever reads — it cannot modify your project. Even
|
|
214
|
+
so, it may stop working the day Overleaf changes something. If that happens,
|
|
215
|
+
please say so in an issue.
|
|
173
216
|
|
|
174
217
|
## License
|
|
175
218
|
|
|
@@ -33,7 +33,13 @@ an Overleaf project URL and a logged-in browser session.
|
|
|
33
33
|
pip install overleaf-comments-export
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Requires Python 3.10
|
|
36
|
+
Requires Python 3.10 or newer (tested up to 3.14). Works on macOS, Linux, and
|
|
37
|
+
Windows.
|
|
38
|
+
|
|
39
|
+
The graphical window needs Python's Tk toolkit, which most Linux distributions
|
|
40
|
+
package separately (`sudo apt install python3-tk` on Debian/Ubuntu). The
|
|
41
|
+
command line never needs it, and `--gui` tells you what to install if it is
|
|
42
|
+
missing.
|
|
37
43
|
|
|
38
44
|
## Quick start
|
|
39
45
|
|
|
@@ -68,6 +74,7 @@ In your output folder, by default:
|
|
|
68
74
|
| `comments.json` | Structured data — `summary`, top-level `threads`, `files`, `comments`, `tracked_changes`, etc. Schema described in `agents.md`. |
|
|
69
75
|
| `comments.jsonl` | One self-contained JSON record per comment for streaming/pipelines. |
|
|
70
76
|
| `agents.md` | A brief instruction file telling an AI agent how to consume the batch. |
|
|
77
|
+
| `response-letter.md` | (Optional, `--response-letter`) A point-by-point reply document, pre-filled with every open comment grouped by who raised it, with blanks for your response. |
|
|
71
78
|
| `by-reviewer/<name>.md` | (Optional, `--per-reviewer`) One Markdown per reviewer with only their threads. |
|
|
72
79
|
| `comments.log` | Diagnostic log for the run. |
|
|
73
80
|
|
|
@@ -85,6 +92,9 @@ overleaf-comments-export --project-url … --out ./out --render-mode detailed
|
|
|
85
92
|
|
|
86
93
|
# Per-reviewer sub-reports under ./out/by-reviewer/
|
|
87
94
|
overleaf-comments-export --project-url … --out ./out --per-reviewer
|
|
95
|
+
|
|
96
|
+
# Draft a point-by-point response letter for the open comments
|
|
97
|
+
overleaf-comments-export --project-url … --out ./out --response-letter
|
|
88
98
|
```
|
|
89
99
|
|
|
90
100
|
Full flag reference: `overleaf-comments-export --help`.
|
|
@@ -126,14 +136,46 @@ Treat it like a password; it stops working when you sign out.
|
|
|
126
136
|
| "Could not read Overleaf cookies from chrome" | Use the paste-the-cookie method above. |
|
|
127
137
|
| "Overleaf could not find that project" | Wrong link, or this account has no access. |
|
|
128
138
|
|
|
129
|
-
##
|
|
139
|
+
## Feedback, questions, and contributing
|
|
140
|
+
|
|
141
|
+
This tool is actively maintained, and feedback shapes what gets built next.
|
|
142
|
+
|
|
143
|
+
- **Something broke, or the output was wrong?**
|
|
144
|
+
[Open an issue.](https://github.com/Mangluu/overleaf-comments-export/issues/new/choose)
|
|
145
|
+
You do not need to be a programmer — paste what the tool said and that is
|
|
146
|
+
plenty. If Overleaf changes something, everything here stops working at once,
|
|
147
|
+
and you may be the first person to notice.
|
|
148
|
+
- **Want it to do something it doesn't?**
|
|
149
|
+
[Suggest a feature.](https://github.com/Mangluu/overleaf-comments-export/issues/new/choose)
|
|
150
|
+
Tell me what you are trying to do, not just the feature — the real task
|
|
151
|
+
usually leads somewhere better.
|
|
152
|
+
- **Just a question, or want to show what you built with it?**
|
|
153
|
+
[Discussions.](https://github.com/Mangluu/overleaf-comments-export/discussions)
|
|
154
|
+
- **Want to contribute code?** See [CONTRIBUTING.md](CONTRIBUTING.md). It takes
|
|
155
|
+
about two minutes to get the tests running, and there are items marked
|
|
156
|
+
*help wanted* in [ROADMAP.md](ROADMAP.md).
|
|
157
|
+
|
|
158
|
+
Never include your session cookie in an issue — it is a password for your
|
|
159
|
+
Overleaf account, and nobody needs it to fix a bug.
|
|
160
|
+
|
|
161
|
+
Maintained by [Shivang Gupta](https://github.com/Mangluu), who wrote it to deal
|
|
162
|
+
with the review comments on his own papers.
|
|
163
|
+
|
|
164
|
+
## What's coming next
|
|
165
|
+
|
|
166
|
+
See [ROADMAP.md](ROADMAP.md). Short version: a response-letter scaffold,
|
|
167
|
+
writing out the full source so an AI can see more than a snippet, and a diff
|
|
168
|
+
between two exports so you can work through review comments in waves.
|
|
169
|
+
|
|
170
|
+
Changes are recorded in [CHANGELOG.md](CHANGELOG.md).
|
|
130
171
|
|
|
131
|
-
|
|
132
|
-
It is provided as-is, with no guaranteed maintenance, no SLA, and no roadmap.
|
|
133
|
-
Pull requests are welcome; issues may or may not be acted upon.
|
|
172
|
+
## A caution
|
|
134
173
|
|
|
135
|
-
|
|
136
|
-
|
|
174
|
+
This tool uses Overleaf's internal endpoints, which are undocumented and can
|
|
175
|
+
change without notice. It identifies itself honestly in every request, backs
|
|
176
|
+
off when asked to, and only ever reads — it cannot modify your project. Even
|
|
177
|
+
so, it may stop working the day Overleaf changes something. If that happens,
|
|
178
|
+
please say so in an issue.
|
|
137
179
|
|
|
138
180
|
## License
|
|
139
181
|
|
|
@@ -4,6 +4,7 @@ import argparse
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
|
+
import traceback
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
from . import __version__
|
|
@@ -11,6 +12,35 @@ from .client import OverleafClient, UserFacingError
|
|
|
11
12
|
from .export import ExportResult, run_export
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
def _no_tkinter_message() -> str:
|
|
16
|
+
"""Platform-specific instructions for installing Python's GUI toolkit."""
|
|
17
|
+
if sys.platform == "darwin":
|
|
18
|
+
fix = (
|
|
19
|
+
"Install Python from python.org (it includes the GUI toolkit), or\n"
|
|
20
|
+
"with Homebrew run:\n\n"
|
|
21
|
+
" brew install python-tk"
|
|
22
|
+
)
|
|
23
|
+
elif sys.platform.startswith("win"):
|
|
24
|
+
fix = (
|
|
25
|
+
"Re-run the Python installer from python.org, choose \"Modify\", and\n"
|
|
26
|
+
"tick \"tcl/tk and IDLE\"."
|
|
27
|
+
)
|
|
28
|
+
else:
|
|
29
|
+
fix = (
|
|
30
|
+
"Install it with your package manager, for example:\n\n"
|
|
31
|
+
" Debian/Ubuntu : sudo apt install python3-tk\n"
|
|
32
|
+
" Fedora : sudo dnf install python3-tkinter\n"
|
|
33
|
+
" Arch : sudo pacman -S tk"
|
|
34
|
+
)
|
|
35
|
+
return (
|
|
36
|
+
"The window cannot open because this Python has no GUI toolkit "
|
|
37
|
+
"installed.\n\n"
|
|
38
|
+
f"{fix}\n\n"
|
|
39
|
+
"Or skip the window entirely and use the command line:\n\n"
|
|
40
|
+
" overleaf-comments-export --project-url <your project link> --out ./comments"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
14
44
|
def main(argv: list[str] | None = None) -> int:
|
|
15
45
|
parser = argparse.ArgumentParser(
|
|
16
46
|
prog="overleaf-comments-export",
|
|
@@ -106,6 +136,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
106
136
|
action="store_false",
|
|
107
137
|
help="Skip writing comments.jsonl (the streaming-friendly companion).",
|
|
108
138
|
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--response-letter",
|
|
141
|
+
action="store_true",
|
|
142
|
+
help="Also write response-letter.md: a point-by-point reply document "
|
|
143
|
+
"pre-filled with every open comment, grouped by who raised it, with "
|
|
144
|
+
"blanks for your response.",
|
|
145
|
+
)
|
|
109
146
|
parser.add_argument(
|
|
110
147
|
"--per-reviewer",
|
|
111
148
|
action="store_true",
|
|
@@ -120,8 +157,31 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
120
157
|
args = parser.parse_args(argv)
|
|
121
158
|
|
|
122
159
|
if args.gui or (not args.project_url and not args.out):
|
|
123
|
-
|
|
124
|
-
|
|
160
|
+
try:
|
|
161
|
+
from .gui import launch_gui
|
|
162
|
+
except ImportError:
|
|
163
|
+
# tkinter is NOT bundled with Python everywhere — most Linux
|
|
164
|
+
# distributions ship it as a separate system package, and it is
|
|
165
|
+
# missing from some minimal/conda builds.
|
|
166
|
+
print(_no_tkinter_message(), file=sys.stderr)
|
|
167
|
+
return 1
|
|
168
|
+
try:
|
|
169
|
+
return launch_gui()
|
|
170
|
+
except Exception as e:
|
|
171
|
+
# Typically TclError on a headless machine (SSH, server, container).
|
|
172
|
+
if "display" in str(e).lower() or type(e).__name__ == "TclError":
|
|
173
|
+
print(
|
|
174
|
+
"There is no screen to open a window on.\n\n"
|
|
175
|
+
"This looks like a computer without a desktop (a server, or "
|
|
176
|
+
"a remote session). Use the command line instead, for "
|
|
177
|
+
"example:\n\n"
|
|
178
|
+
" overleaf-comments-export --project-url <your project link> "
|
|
179
|
+
"--out ./comments\n\n"
|
|
180
|
+
"Run with --help to see every option.",
|
|
181
|
+
file=sys.stderr,
|
|
182
|
+
)
|
|
183
|
+
return 1
|
|
184
|
+
raise
|
|
125
185
|
|
|
126
186
|
if not args.project_url or not args.out:
|
|
127
187
|
parser.error("--project-url and --out are required in CLI mode (or pass --gui).")
|
|
@@ -151,12 +211,26 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
151
211
|
render_mode=args.render_mode,
|
|
152
212
|
write_jsonl=args.write_jsonl,
|
|
153
213
|
per_reviewer_reports=args.per_reviewer,
|
|
214
|
+
response_letter=args.response_letter,
|
|
154
215
|
progress=lambda msg: print(msg, file=sys.stderr),
|
|
155
216
|
)
|
|
156
217
|
except UserFacingError as e:
|
|
157
218
|
# Expected, explainable failures: no traceback, just what to do next.
|
|
158
219
|
print(f"\n{e}", file=sys.stderr)
|
|
159
220
|
return 1
|
|
221
|
+
except Exception:
|
|
222
|
+
# Unexpected: show the traceback, but also tell people where to send it.
|
|
223
|
+
# The moment something breaks is the only moment we have their attention.
|
|
224
|
+
traceback.print_exc()
|
|
225
|
+
print(
|
|
226
|
+
f"\nThat looks like a bug in overleaf-comments-export {__version__}.\n"
|
|
227
|
+
"Please report it (copy the lines above) at\n"
|
|
228
|
+
" https://github.com/Mangluu/overleaf-comments-export/issues/new/choose\n"
|
|
229
|
+
"It probably affects other people too, and it cannot be fixed if "
|
|
230
|
+
"nobody says anything.",
|
|
231
|
+
file=sys.stderr,
|
|
232
|
+
)
|
|
233
|
+
return 2
|
|
160
234
|
print(f"\nDone. Open: {result.markdown_path}")
|
|
161
235
|
return 0
|
|
162
236
|
|
{overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/export.py
RENAMED
|
@@ -19,7 +19,7 @@ from .model import (
|
|
|
19
19
|
Thread,
|
|
20
20
|
TrackedChange,
|
|
21
21
|
)
|
|
22
|
-
from .render import render_markdown
|
|
22
|
+
from .render import render_markdown, render_response_letter
|
|
23
23
|
from .sections import find_headings, nearest_heading
|
|
24
24
|
|
|
25
25
|
SCHEMA_VERSION = "1.3"
|
|
@@ -76,6 +76,7 @@ class ExportResult:
|
|
|
76
76
|
jsonl_path: Path | None = None
|
|
77
77
|
by_reviewer_dir: Path | None = None
|
|
78
78
|
agents_path: Path | None = None
|
|
79
|
+
response_letter_path: Path | None = None
|
|
79
80
|
|
|
80
81
|
|
|
81
82
|
def _build_user_map(threads_raw: dict[str, Any]) -> dict[str, dict[str, str]]:
|
|
@@ -340,6 +341,7 @@ def run_export(
|
|
|
340
341
|
render_mode: str = "compact",
|
|
341
342
|
write_jsonl: bool = True,
|
|
342
343
|
per_reviewer_reports: bool = False,
|
|
344
|
+
response_letter: bool = False,
|
|
343
345
|
progress: ProgressCallback | None = None,
|
|
344
346
|
) -> ExportResult:
|
|
345
347
|
"""Programmatic entry point used by both the CLI and the GUI."""
|
|
@@ -668,6 +670,15 @@ def run_export(
|
|
|
668
670
|
written += 1
|
|
669
671
|
progress(f"Wrote {written} per-reviewer report(s) into by-reviewer/")
|
|
670
672
|
|
|
673
|
+
letter_path: Path | None = None
|
|
674
|
+
if response_letter:
|
|
675
|
+
letter_path = out_dir / "response-letter.md"
|
|
676
|
+
letter_path.write_text(
|
|
677
|
+
render_response_letter(title, project_id, threads, anchored),
|
|
678
|
+
encoding="utf-8",
|
|
679
|
+
)
|
|
680
|
+
progress(f"Wrote {letter_path.name}")
|
|
681
|
+
|
|
671
682
|
agents_path = out_dir / "agents.md"
|
|
672
683
|
agents_path.write_text(_build_agents_md(title, project_id, json_path.name, md_path.name), encoding="utf-8")
|
|
673
684
|
progress(f"Wrote {agents_path.name}")
|
|
@@ -688,6 +699,7 @@ def run_export(
|
|
|
688
699
|
jsonl_path=(out_dir / "comments.jsonl") if write_jsonl else None,
|
|
689
700
|
by_reviewer_dir=(out_dir / "by-reviewer") if per_reviewer_reports else None,
|
|
690
701
|
agents_path=agents_path,
|
|
702
|
+
response_letter_path=letter_path,
|
|
691
703
|
)
|
|
692
704
|
|
|
693
705
|
|
{overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/gui.py
RENAMED
|
@@ -425,6 +425,9 @@ class App:
|
|
|
425
425
|
self.per_reviewer_var = tk.BooleanVar(
|
|
426
426
|
value=bool(self.config.get("per_reviewer_reports", False))
|
|
427
427
|
)
|
|
428
|
+
self.response_letter_var = tk.BooleanVar(
|
|
429
|
+
value=bool(self.config.get("response_letter", False))
|
|
430
|
+
)
|
|
428
431
|
self.include_raw_var = tk.BooleanVar(
|
|
429
432
|
value=bool(self.config.get("include_raw", False))
|
|
430
433
|
)
|
|
@@ -433,6 +436,11 @@ class App:
|
|
|
433
436
|
text="comments.jsonl (streaming companion)",
|
|
434
437
|
variable=self.write_jsonl_var,
|
|
435
438
|
).pack(anchor="w")
|
|
439
|
+
ttk.Checkbutton(
|
|
440
|
+
extras_row,
|
|
441
|
+
text="Response letter draft (response-letter.md)",
|
|
442
|
+
variable=self.response_letter_var,
|
|
443
|
+
).pack(anchor="w")
|
|
436
444
|
ttk.Checkbutton(
|
|
437
445
|
extras_row,
|
|
438
446
|
text="Per-reviewer reports (by-reviewer/<name>.md)",
|
|
@@ -579,6 +587,7 @@ class App:
|
|
|
579
587
|
"render_mode": self.render_mode_var.get(),
|
|
580
588
|
"write_jsonl": bool(self.write_jsonl_var.get()),
|
|
581
589
|
"per_reviewer_reports": bool(self.per_reviewer_var.get()),
|
|
590
|
+
"response_letter": bool(self.response_letter_var.get()),
|
|
582
591
|
"include_raw": bool(self.include_raw_var.get()),
|
|
583
592
|
}
|
|
584
593
|
)
|
|
@@ -603,6 +612,7 @@ class App:
|
|
|
603
612
|
render_mode=self.render_mode_var.get(),
|
|
604
613
|
write_jsonl=bool(self.write_jsonl_var.get()),
|
|
605
614
|
per_reviewer_reports=bool(self.per_reviewer_var.get()),
|
|
615
|
+
response_letter=bool(self.response_letter_var.get()),
|
|
606
616
|
include_raw=bool(self.include_raw_var.get()),
|
|
607
617
|
)
|
|
608
618
|
self.worker = threading.Thread(
|
|
@@ -653,6 +663,8 @@ class App:
|
|
|
653
663
|
self._append_log(f"JSONL: {result.jsonl_path}")
|
|
654
664
|
if result.agents_path is not None:
|
|
655
665
|
self._append_log(f"Agents: {result.agents_path}")
|
|
666
|
+
if result.response_letter_path is not None:
|
|
667
|
+
self._append_log(f"Letter: {result.response_letter_path}")
|
|
656
668
|
if result.by_reviewer_dir is not None:
|
|
657
669
|
self._append_log(f"Per-reviewer: {result.by_reviewer_dir}")
|
|
658
670
|
|
{overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/render.py
RENAMED
|
@@ -277,6 +277,103 @@ def render_markdown(
|
|
|
277
277
|
return "\n".join(out).rstrip() + "\n"
|
|
278
278
|
|
|
279
279
|
|
|
280
|
+
def render_response_letter(
|
|
281
|
+
project_title: str,
|
|
282
|
+
project_id: str,
|
|
283
|
+
threads: dict[str, Thread],
|
|
284
|
+
anchored: list[AnchoredComment],
|
|
285
|
+
) -> str:
|
|
286
|
+
"""A point-by-point reply document, pre-filled with every open comment.
|
|
287
|
+
|
|
288
|
+
Grouped by the person who raised each point, because that is how journals
|
|
289
|
+
ask for rebuttals. Each entry carries its `C###` id so it can be traced
|
|
290
|
+
back to the full export.
|
|
291
|
+
"""
|
|
292
|
+
out: list[str] = []
|
|
293
|
+
pulled = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
294
|
+
|
|
295
|
+
# Only points that still need answering, keyed by whoever raised them.
|
|
296
|
+
by_reviewer: dict[str, list[AnchoredComment]] = defaultdict(list)
|
|
297
|
+
for c in anchored:
|
|
298
|
+
thread = threads.get(c.thread_id)
|
|
299
|
+
if thread is None or thread.resolved or not thread.messages:
|
|
300
|
+
continue
|
|
301
|
+
first = min(thread.messages, key=lambda m: m.timestamp_ms)
|
|
302
|
+
who = _humanize_user(first.user_name, first.user_email, first.user_id)
|
|
303
|
+
by_reviewer[who].append(c)
|
|
304
|
+
|
|
305
|
+
total = sum(len(v) for v in by_reviewer.values())
|
|
306
|
+
|
|
307
|
+
out.append(f"# Response to reviewers — {project_title}")
|
|
308
|
+
out.append("")
|
|
309
|
+
out.append(f"_Draft generated {pulled}. {total} point(s) to address._")
|
|
310
|
+
out.append("")
|
|
311
|
+
out.append(
|
|
312
|
+
"Fill in the **Response** and **Change made** lines under each point. "
|
|
313
|
+
"The `C###` ids match `comments.json`, so you can ask an AI assistant "
|
|
314
|
+
"to draft any of them by id."
|
|
315
|
+
)
|
|
316
|
+
out.append("")
|
|
317
|
+
out.append("---")
|
|
318
|
+
out.append("")
|
|
319
|
+
|
|
320
|
+
if not total:
|
|
321
|
+
out.append("No open comments. Nothing to respond to.")
|
|
322
|
+
out.append("")
|
|
323
|
+
return "\n".join(out).rstrip() + "\n"
|
|
324
|
+
|
|
325
|
+
out.append("## Summary of changes")
|
|
326
|
+
out.append("")
|
|
327
|
+
out.append("_A short paragraph on the main revisions goes here._")
|
|
328
|
+
out.append("")
|
|
329
|
+
|
|
330
|
+
for reviewer in sorted(by_reviewer):
|
|
331
|
+
items = by_reviewer[reviewer]
|
|
332
|
+
out.append(f"## {reviewer} — {len(items)} point(s)")
|
|
333
|
+
out.append("")
|
|
334
|
+
for c in items:
|
|
335
|
+
thread = threads[c.thread_id]
|
|
336
|
+
ordered = sorted(thread.messages, key=lambda m: m.timestamp_ms)
|
|
337
|
+
where = c.nearest_heading or "no enclosing section"
|
|
338
|
+
# Don't leak "<unknown-6a21dec…>" into a document someone sends to
|
|
339
|
+
# an editor; the line number alone is enough there.
|
|
340
|
+
if c.pathname.startswith("<unknown-"):
|
|
341
|
+
locus = f"line {c.line_no}"
|
|
342
|
+
else:
|
|
343
|
+
locus = f"`{c.pathname}` line {c.line_no}"
|
|
344
|
+
out.append(f"### {c.short_id} — § {where} ({locus})")
|
|
345
|
+
out.append("")
|
|
346
|
+
quote = (c.anchored_text or "").strip().replace("\n", " ")
|
|
347
|
+
if quote:
|
|
348
|
+
out.append(f"**Referring to:** “{quote}”")
|
|
349
|
+
out.append("")
|
|
350
|
+
out.append("**Comment:**")
|
|
351
|
+
out.append("")
|
|
352
|
+
for line in (ordered[0].content or "").strip().splitlines() or [""]:
|
|
353
|
+
out.append(f"> {line}")
|
|
354
|
+
out.append("")
|
|
355
|
+
if len(ordered) > 1:
|
|
356
|
+
out.append("**Discussion so far:**")
|
|
357
|
+
out.append("")
|
|
358
|
+
for msg in ordered[1:]:
|
|
359
|
+
who = _humanize_user(msg.user_name, msg.user_email, msg.user_id)
|
|
360
|
+
body = (msg.content or "").strip().replace("\n", " ")
|
|
361
|
+
out.append(f"> ↳ {who}: {body}")
|
|
362
|
+
out.append("")
|
|
363
|
+
out.append("**Response:**")
|
|
364
|
+
out.append("")
|
|
365
|
+
out.append("_TODO_")
|
|
366
|
+
out.append("")
|
|
367
|
+
out.append("**Change made:**")
|
|
368
|
+
out.append("")
|
|
369
|
+
out.append("_TODO — what changed, and where._")
|
|
370
|
+
out.append("")
|
|
371
|
+
out.append("---")
|
|
372
|
+
out.append("")
|
|
373
|
+
|
|
374
|
+
return "\n".join(out).rstrip() + "\n"
|
|
375
|
+
|
|
376
|
+
|
|
280
377
|
def _status_badge(thread: Thread | None, stale: bool) -> str:
|
|
281
378
|
bits = []
|
|
282
379
|
if thread and thread.resolved:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: overleaf-comments-export
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Export comment threads and tracked changes from an Overleaf project to Markdown + JSON, optimized for AI-agent consumption.
|
|
5
5
|
Author: Shivang
|
|
6
6
|
License: MIT
|
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
23
|
Classifier: Topic :: Text Processing :: Markup :: LaTeX
|
|
23
24
|
Classifier: Topic :: Scientific/Engineering
|
|
24
25
|
Requires-Python: >=3.10
|
|
@@ -69,7 +70,13 @@ an Overleaf project URL and a logged-in browser session.
|
|
|
69
70
|
pip install overleaf-comments-export
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
Requires Python 3.10
|
|
73
|
+
Requires Python 3.10 or newer (tested up to 3.14). Works on macOS, Linux, and
|
|
74
|
+
Windows.
|
|
75
|
+
|
|
76
|
+
The graphical window needs Python's Tk toolkit, which most Linux distributions
|
|
77
|
+
package separately (`sudo apt install python3-tk` on Debian/Ubuntu). The
|
|
78
|
+
command line never needs it, and `--gui` tells you what to install if it is
|
|
79
|
+
missing.
|
|
73
80
|
|
|
74
81
|
## Quick start
|
|
75
82
|
|
|
@@ -104,6 +111,7 @@ In your output folder, by default:
|
|
|
104
111
|
| `comments.json` | Structured data — `summary`, top-level `threads`, `files`, `comments`, `tracked_changes`, etc. Schema described in `agents.md`. |
|
|
105
112
|
| `comments.jsonl` | One self-contained JSON record per comment for streaming/pipelines. |
|
|
106
113
|
| `agents.md` | A brief instruction file telling an AI agent how to consume the batch. |
|
|
114
|
+
| `response-letter.md` | (Optional, `--response-letter`) A point-by-point reply document, pre-filled with every open comment grouped by who raised it, with blanks for your response. |
|
|
107
115
|
| `by-reviewer/<name>.md` | (Optional, `--per-reviewer`) One Markdown per reviewer with only their threads. |
|
|
108
116
|
| `comments.log` | Diagnostic log for the run. |
|
|
109
117
|
|
|
@@ -121,6 +129,9 @@ overleaf-comments-export --project-url … --out ./out --render-mode detailed
|
|
|
121
129
|
|
|
122
130
|
# Per-reviewer sub-reports under ./out/by-reviewer/
|
|
123
131
|
overleaf-comments-export --project-url … --out ./out --per-reviewer
|
|
132
|
+
|
|
133
|
+
# Draft a point-by-point response letter for the open comments
|
|
134
|
+
overleaf-comments-export --project-url … --out ./out --response-letter
|
|
124
135
|
```
|
|
125
136
|
|
|
126
137
|
Full flag reference: `overleaf-comments-export --help`.
|
|
@@ -162,14 +173,46 @@ Treat it like a password; it stops working when you sign out.
|
|
|
162
173
|
| "Could not read Overleaf cookies from chrome" | Use the paste-the-cookie method above. |
|
|
163
174
|
| "Overleaf could not find that project" | Wrong link, or this account has no access. |
|
|
164
175
|
|
|
165
|
-
##
|
|
176
|
+
## Feedback, questions, and contributing
|
|
177
|
+
|
|
178
|
+
This tool is actively maintained, and feedback shapes what gets built next.
|
|
179
|
+
|
|
180
|
+
- **Something broke, or the output was wrong?**
|
|
181
|
+
[Open an issue.](https://github.com/Mangluu/overleaf-comments-export/issues/new/choose)
|
|
182
|
+
You do not need to be a programmer — paste what the tool said and that is
|
|
183
|
+
plenty. If Overleaf changes something, everything here stops working at once,
|
|
184
|
+
and you may be the first person to notice.
|
|
185
|
+
- **Want it to do something it doesn't?**
|
|
186
|
+
[Suggest a feature.](https://github.com/Mangluu/overleaf-comments-export/issues/new/choose)
|
|
187
|
+
Tell me what you are trying to do, not just the feature — the real task
|
|
188
|
+
usually leads somewhere better.
|
|
189
|
+
- **Just a question, or want to show what you built with it?**
|
|
190
|
+
[Discussions.](https://github.com/Mangluu/overleaf-comments-export/discussions)
|
|
191
|
+
- **Want to contribute code?** See [CONTRIBUTING.md](CONTRIBUTING.md). It takes
|
|
192
|
+
about two minutes to get the tests running, and there are items marked
|
|
193
|
+
*help wanted* in [ROADMAP.md](ROADMAP.md).
|
|
194
|
+
|
|
195
|
+
Never include your session cookie in an issue — it is a password for your
|
|
196
|
+
Overleaf account, and nobody needs it to fix a bug.
|
|
197
|
+
|
|
198
|
+
Maintained by [Shivang Gupta](https://github.com/Mangluu), who wrote it to deal
|
|
199
|
+
with the review comments on his own papers.
|
|
200
|
+
|
|
201
|
+
## What's coming next
|
|
202
|
+
|
|
203
|
+
See [ROADMAP.md](ROADMAP.md). Short version: a response-letter scaffold,
|
|
204
|
+
writing out the full source so an AI can see more than a snippet, and a diff
|
|
205
|
+
between two exports so you can work through review comments in waves.
|
|
206
|
+
|
|
207
|
+
Changes are recorded in [CHANGELOG.md](CHANGELOG.md).
|
|
166
208
|
|
|
167
|
-
|
|
168
|
-
It is provided as-is, with no guaranteed maintenance, no SLA, and no roadmap.
|
|
169
|
-
Pull requests are welcome; issues may or may not be acted upon.
|
|
209
|
+
## A caution
|
|
170
210
|
|
|
171
|
-
|
|
172
|
-
|
|
211
|
+
This tool uses Overleaf's internal endpoints, which are undocumented and can
|
|
212
|
+
change without notice. It identifies itself honestly in every request, backs
|
|
213
|
+
off when asked to, and only ever reads — it cannot modify your project. Even
|
|
214
|
+
so, it may stop working the day Overleaf changes something. If that happens,
|
|
215
|
+
please say so in an issue.
|
|
173
216
|
|
|
174
217
|
## License
|
|
175
218
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "overleaf-comments-export"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Export comment threads and tracked changes from an Overleaf project to Markdown + JSON, optimized for AI-agent consumption."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -34,6 +34,7 @@ classifiers = [
|
|
|
34
34
|
"Programming Language :: Python :: 3.11",
|
|
35
35
|
"Programming Language :: Python :: 3.12",
|
|
36
36
|
"Programming Language :: Python :: 3.13",
|
|
37
|
+
"Programming Language :: Python :: 3.14",
|
|
37
38
|
"Topic :: Text Processing :: Markup :: LaTeX",
|
|
38
39
|
"Topic :: Scientific/Engineering",
|
|
39
40
|
]
|
|
@@ -46,7 +47,10 @@ dependencies = [
|
|
|
46
47
|
|
|
47
48
|
[project.optional-dependencies]
|
|
48
49
|
gui = [
|
|
49
|
-
# tkinter
|
|
50
|
+
# NOTE: tkinter itself cannot be installed from PyPI. It ships with the
|
|
51
|
+
# python.org installers, but most Linux distributions package it
|
|
52
|
+
# separately (python3-tk / python3-tkinter / tk). The CLI never needs it,
|
|
53
|
+
# and `--gui` explains how to install it if it is missing.
|
|
50
54
|
"sv-ttk>=2.6",
|
|
51
55
|
]
|
|
52
56
|
test = [
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from overleaf_comments_export.model import (
|
|
4
|
+
AnchoredComment,
|
|
5
|
+
Message,
|
|
6
|
+
SourceContext,
|
|
7
|
+
Thread,
|
|
8
|
+
)
|
|
9
|
+
from overleaf_comments_export.render import render_response_letter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _thread(tid, author, body, resolved=False, reply=None):
|
|
13
|
+
msgs = [
|
|
14
|
+
Message(id=f"{tid}m1", content=body, timestamp_ms=1_000, user_id=author,
|
|
15
|
+
user_name=author.title(), user_email=f"{author}@x.com")
|
|
16
|
+
]
|
|
17
|
+
if reply:
|
|
18
|
+
msgs.append(
|
|
19
|
+
Message(id=f"{tid}m2", content=reply, timestamp_ms=2_000, user_id="co",
|
|
20
|
+
user_name="Co Author", user_email="co@x.com")
|
|
21
|
+
)
|
|
22
|
+
return Thread(id=tid, messages=msgs, resolved=resolved)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _comment(tid, short_id, line=10, heading="Introduction", anchor="some phrase"):
|
|
26
|
+
return AnchoredComment(
|
|
27
|
+
thread_id=tid, short_id=short_id, doc_id="d1", pathname="main.tex",
|
|
28
|
+
offset=0, anchored_text=anchor, line_no=line, col=0,
|
|
29
|
+
nearest_heading=heading, stale=False,
|
|
30
|
+
context=SourceContext(before="a", anchor=anchor, after="b", line_no=line),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_letter_has_a_slot_for_every_open_comment():
|
|
35
|
+
threads = {
|
|
36
|
+
"t1": _thread("t1", "emma", "needs a citation"),
|
|
37
|
+
"t2": _thread("t2", "emma", "unclear phrasing"),
|
|
38
|
+
}
|
|
39
|
+
md = render_response_letter(
|
|
40
|
+
"My Paper", "abc", threads, [_comment("t1", "C001"), _comment("t2", "C002")]
|
|
41
|
+
)
|
|
42
|
+
assert "C001" in md and "C002" in md
|
|
43
|
+
assert md.count("**Response:**") == 2
|
|
44
|
+
assert md.count("**Change made:**") == 2
|
|
45
|
+
assert "2 point(s) to address" in md
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_letter_groups_by_who_raised_the_point():
|
|
49
|
+
threads = {
|
|
50
|
+
"t1": _thread("t1", "emma", "point one"),
|
|
51
|
+
"t2": _thread("t2", "xinyi", "point two"),
|
|
52
|
+
}
|
|
53
|
+
md = render_response_letter(
|
|
54
|
+
"P", "abc", threads, [_comment("t1", "C001"), _comment("t2", "C002")]
|
|
55
|
+
)
|
|
56
|
+
assert "## Emma — 1 point(s)" in md
|
|
57
|
+
assert "## Xinyi — 1 point(s)" in md
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_letter_skips_resolved_comments():
|
|
61
|
+
threads = {
|
|
62
|
+
"t1": _thread("t1", "emma", "already handled", resolved=True),
|
|
63
|
+
"t2": _thread("t2", "emma", "still open"),
|
|
64
|
+
}
|
|
65
|
+
md = render_response_letter(
|
|
66
|
+
"P", "abc", threads, [_comment("t1", "C001"), _comment("t2", "C002")]
|
|
67
|
+
)
|
|
68
|
+
assert "C002" in md
|
|
69
|
+
assert "C001" not in md
|
|
70
|
+
assert "1 point(s) to address" in md
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_letter_includes_the_discussion_so_far():
|
|
74
|
+
threads = {"t1": _thread("t1", "emma", "the ask", reply="we could cite Smith")}
|
|
75
|
+
md = render_response_letter("P", "abc", threads, [_comment("t1", "C001")])
|
|
76
|
+
assert "**Discussion so far:**" in md
|
|
77
|
+
assert "we could cite Smith" in md
|
|
78
|
+
assert "↳ Co Author" in md
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_letter_carries_location_and_quote():
|
|
82
|
+
threads = {"t1": _thread("t1", "emma", "vague")}
|
|
83
|
+
md = render_response_letter(
|
|
84
|
+
"P", "abc", threads,
|
|
85
|
+
[_comment("t1", "C001", line=42, heading="Method", anchor="novel framework")],
|
|
86
|
+
)
|
|
87
|
+
assert "§ Method" in md
|
|
88
|
+
assert "`main.tex` line 42" in md
|
|
89
|
+
assert "“novel framework”" in md
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_letter_with_nothing_open_says_so():
|
|
93
|
+
threads = {"t1": _thread("t1", "emma", "done", resolved=True)}
|
|
94
|
+
md = render_response_letter("P", "abc", threads, [_comment("t1", "C001")])
|
|
95
|
+
assert "No open comments" in md
|
|
96
|
+
assert "**Response:**" not in md
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_letter_survives_a_comment_whose_thread_is_missing():
|
|
100
|
+
md = render_response_letter("P", "abc", {}, [_comment("gone", "C001")])
|
|
101
|
+
assert "No open comments" in md
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_letter_hides_unmapped_filenames():
|
|
105
|
+
"""A reader of the letter should never see "<unknown-6a21dec…>"."""
|
|
106
|
+
threads = {"t1": _thread("t1", "emma", "vague")}
|
|
107
|
+
c = _comment("t1", "C001", line=42)
|
|
108
|
+
c.pathname = "<unknown-6a21dec6cb39c30917f5c477>"
|
|
109
|
+
md = render_response_letter("P", "abc", threads, [c])
|
|
110
|
+
assert "unknown-" not in md
|
|
111
|
+
assert "line 42" in md
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/client.py
RENAMED
|
File without changes
|
{overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_anchors_sections.py
RENAMED
|
File without changes
|
|
File without changes
|
{overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_errors_and_auth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_replies_and_changes.py
RENAMED
|
File without changes
|