overleaf-sync 1.2.0__tar.gz → 2.0.0a2__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_sync-2.0.0a2/PKG-INFO +148 -0
- {overleaf-sync-1.2.0 → overleaf_sync-2.0.0a2}/README.md +15 -4
- {overleaf-sync-1.2.0 → overleaf_sync-2.0.0a2}/olsync/__init__.py +1 -1
- {overleaf-sync-1.2.0 → overleaf_sync-2.0.0a2}/olsync/olbrowserlogin.py +0 -1
- {overleaf-sync-1.2.0 → overleaf_sync-2.0.0a2}/olsync/olclient.py +146 -45
- {overleaf-sync-1.2.0 → overleaf_sync-2.0.0a2}/olsync/olsync.py +81 -11
- overleaf_sync-2.0.0a2/pyproject.toml +60 -0
- overleaf-sync-1.2.0/.gitignore +0 -118
- overleaf-sync-1.2.0/PKG-INFO +0 -7
- overleaf-sync-1.2.0/pyproject.toml +0 -26
- overleaf-sync-1.2.0/requirements.txt +0 -7
- overleaf-sync-1.2.0/setup.py +0 -35
- {overleaf-sync-1.2.0 → overleaf_sync-2.0.0a2}/LICENSE +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: overleaf-sync
|
|
3
|
+
Version: 2.0.0a2
|
|
4
|
+
Summary: Overleaf Two-Way Sync Tool
|
|
5
|
+
Keywords: overleaf,sync,latex,tex
|
|
6
|
+
Author-email: Moritz Glöckl <moritzgloeckl@users.noreply.github.com>
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: requests >=2.31,<3
|
|
14
|
+
Requires-Dist: beautifulsoup4 >=4.12,<5
|
|
15
|
+
Requires-Dist: yaspin >=3,<4
|
|
16
|
+
Requires-Dist: python-dateutil >=2.8,<3
|
|
17
|
+
Requires-Dist: click >=8.1,<9
|
|
18
|
+
Requires-Dist: socketIO-client ==0.5.7.2
|
|
19
|
+
Requires-Dist: websocket-client >=1,<2
|
|
20
|
+
Requires-Dist: pytest >=8 ; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov >=5 ; extra == "dev"
|
|
22
|
+
Requires-Dist: responses >=0.25 ; extra == "dev"
|
|
23
|
+
Requires-Dist: ruff >=0.6 ; extra == "dev"
|
|
24
|
+
Requires-Dist: PySide6 >=6.5,<7 ; extra == "gui"
|
|
25
|
+
Project-URL: Homepage, https://github.com/moritzgloeckl/overleaf-sync
|
|
26
|
+
Project-URL: Issues, https://github.com/moritzgloeckl/overleaf-sync/issues
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Provides-Extra: gui
|
|
29
|
+
|
|
30
|
+
# Overleaf-Sync
|
|
31
|
+
### Easy Overleaf Two-Way Synchronization
|
|
32
|
+
|
|
33
|
+
   
|
|
34
|
+
|
|
35
|
+
This tool provides an easy way to synchronize Overleaf projects from and to your local computer. No paid account necessary.
|
|
36
|
+
|
|
37
|
+
----
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
- Sync your locally modified `.tex` (and other) files to your Overleaf projects
|
|
41
|
+
- Sync your remotely modified `.tex` (and other) files to computer
|
|
42
|
+
- Works with free Overleaf account
|
|
43
|
+
- No Git or Dropbox required
|
|
44
|
+
- Does not steal or store your login credentials (works with a persisted cookie, logging in is done on the original Overleaf website)
|
|
45
|
+
|
|
46
|
+
## How To Use
|
|
47
|
+
### Install
|
|
48
|
+
The package is available via [PyPI](https://pypi.org/project/overleaf-sync/) and needs Python 3.11 or newer. Install it including the login window (`gui` extra):
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
moritz@github:~/test$ pip3 install "overleaf-sync[gui]"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Depending on your local Python installation, you might need to use `pip` instead of `pip3`. [pipx](https://pipx.pypa.io/) works too: `pipx install "overleaf-sync[gui]"`.
|
|
55
|
+
|
|
56
|
+
> **2.0 pre-release:** Overleaf changed its website in a way that broke version 1.2.0. The fixes are available as a pre-release: `pip3 install --pre "overleaf-sync[gui]"`. Please report any problems in the [issues](https://github.com/moritzgloeckl/overleaf-sync/issues).
|
|
57
|
+
|
|
58
|
+
### Prerequisites
|
|
59
|
+
- Create your project on [Overleaf](https://www.overleaf.com/project), for example a project named `test`. Overleaf-sync is not able to create projects (yet).
|
|
60
|
+
- Create a folder, preferably with the same name as the project (`test`) on your computer.
|
|
61
|
+
- Execute the script from that folder (`test`).
|
|
62
|
+
- If you do not specify the project name, overleaf-sync uses the current folder's name as the project name.
|
|
63
|
+
|
|
64
|
+
### Usage
|
|
65
|
+
#### Login
|
|
66
|
+
```
|
|
67
|
+
moritz@github:~/test$ ols login [--path]
|
|
68
|
+
Login successful. Cookie persisted as `.olauth`. You may now sync your project.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Logging in will be handled by a mini web browser opening on your device (using Qt via PySide6, from the `gui` extra). You can then enter your username and password securely on the official Overleaf website. You might get asked to solve a CAPTCHA in the process. Your credentials are sent to Overleaf over HTTPS.
|
|
72
|
+
|
|
73
|
+
It then stores your *cookie* (**not** your login credentials) in a hidden file called `.olauth` in the same folder you run the command from. It is possible to store the cookie elsewhere using the `--path` option. The cookie file will not be synced to or from Overleaf.
|
|
74
|
+
|
|
75
|
+
Keep the `.olauth` file save, as it can be used to log in into your account.
|
|
76
|
+
|
|
77
|
+
#### Login without the login window
|
|
78
|
+
On a server without a display, or if the login window doesn't work for your account (e.g. single sign-on via ORCID or your institution), log in with a session cookie instead:
|
|
79
|
+
|
|
80
|
+
1. Log in to Overleaf in your normal browser.
|
|
81
|
+
2. Open the developer tools → Application (Firefox: Storage) → Cookies → `https://www.overleaf.com` and copy the value of `overleaf_session2`.
|
|
82
|
+
3. Run `ols login --cookie` and paste the value at the (hidden) prompt, or provide it via the `OLSYNC_COOKIE` environment variable.
|
|
83
|
+
|
|
84
|
+
This works without installing the `gui` extra.
|
|
85
|
+
|
|
86
|
+
### Listing all projects
|
|
87
|
+
```
|
|
88
|
+
moritz@github:~/test$ ols list [--store-path -v/--verbose]
|
|
89
|
+
10/31/2021, 01:23:45 - Project A
|
|
90
|
+
09/21/2020, 01:23:45 - Project B
|
|
91
|
+
08/11/2019, 01:23:45 - Project C
|
|
92
|
+
07/01/2018, 01:23:45 - Project D
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Use `ols list` to conveniently list all projects in your account available for syncing.
|
|
96
|
+
|
|
97
|
+
### Downloading project's PDF
|
|
98
|
+
```
|
|
99
|
+
moritz@github:~/test$ ols download [--name --download-path --store-path -v/--verbose]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use `ols download` to compile and download your project's PDF. Specify a download path if you do not want to store the PDF file in the current folder. Currently only downloads the first PDF file it finds.
|
|
103
|
+
|
|
104
|
+
### Syncing
|
|
105
|
+
```
|
|
106
|
+
moritz@github:~/test$ ols [-l/--local-only -r/--remote-only --store-path -p/--path -i/--olignore]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Just calling `ols` will two-way sync your project. When there are changes both locally, and remotely you will be asked which file to keep. Using the `-l` or `-r` option you can specify to either sync local project files to Overleaf only or Overleaf files to local ones only respectively. When using these options you can also sync deleted files. If a file has been deleted it can either be deleted on the target (remote when `-l`, local when `-r`) as well, restored on the source (local when `-l`, remote when `-r`) or ignored.
|
|
110
|
+
|
|
111
|
+
The option `--store-path` specifies the path of the cookie file created by the `login` command. If you did not change its path, you do not need to specify this argument. The `-p/--path` option allows you to specify a different sync folder than the one you're calling `ols` from. The `-i/--olignore` option allows you to specify the path of an `.olignore` file. It uses `fnmatch` internally, so it may have some similarity to `.gitignore` but doesn't work exactly the same. For example, if you wish to exclude a specific folder named `out`, you need to specify it as `out/*`. See [here](https://docs.python.org/3/library/fnmatch.html) for more information.
|
|
112
|
+
|
|
113
|
+
Sample Output:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
Project queried successfully.
|
|
117
|
+
✅ Querying project
|
|
118
|
+
Project downloaded successfully.
|
|
119
|
+
✅ Downloading project
|
|
120
|
+
|
|
121
|
+
Syncing files from remote to local
|
|
122
|
+
====================
|
|
123
|
+
|
|
124
|
+
[SYNCING] report.tex
|
|
125
|
+
report.tex does not exist on local. Creating file.
|
|
126
|
+
|
|
127
|
+
[SYNCING] other-report.tex
|
|
128
|
+
other-report.tex does not exist on local. Creating file.
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
✅ Syncing files from remote to local
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Known Bugs
|
|
135
|
+
- When modifying a file on Overleaf and immediately syncing afterwards, the tool might not detect the changes. Please allow 1-2 minutes after modifying a file on Overleaf before syncing it to your local computer.
|
|
136
|
+
|
|
137
|
+
## Contributing
|
|
138
|
+
|
|
139
|
+
All pull requests and change/feature requests are welcome.
|
|
140
|
+
|
|
141
|
+
## Disclaimer
|
|
142
|
+
THE AUTHOR OF THIS SOFTWARE AND THIS SOFTWARE IS NOT ENDORSED BY, DIRECTLY AFFILIATED WITH, MAINTAINED, AUTHORIZED, OR SPONSORED BY OVERLEAF OR WRITELATEX LIMITED. ALL PRODUCT AND COMPANY NAMES ARE THE REGISTERED TRADEMARKS OF THEIR ORIGINAL OWNERS. THE USE OF ANY TRADE NAME OR TRADEMARK IS FOR IDENTIFICATION AND REFERENCE PURPOSES ONLY AND DOES NOT IMPLY ANY ASSOCIATION WITH THE TRADEMARK HOLDER OF THEIR PRODUCT BRAND.
|
|
143
|
+
|
|
144
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
145
|
+
|
|
146
|
+
THIS SOFTWARE WAS DESIGNED TO BE USED ONLY FOR RESEARCH PURPOSES. THIS SOFTWARE COMES WITH NO WARRANTIES OF ANY KIND WHATSOEVER. USE IT AT YOUR OWN RISK! IF THESE TERMS ARE NOT ACCEPTABLE, YOU AREN'T ALLOWED TO USE THE CODE.
|
|
147
|
+
|
|
148
|
+
|
|
@@ -16,13 +16,15 @@ This tool provides an easy way to synchronize Overleaf projects from and to your
|
|
|
16
16
|
|
|
17
17
|
## How To Use
|
|
18
18
|
### Install
|
|
19
|
-
The package is available via [PyPI](https://pypi.org/project/overleaf-sync/).
|
|
19
|
+
The package is available via [PyPI](https://pypi.org/project/overleaf-sync/) and needs Python 3.11 or newer. Install it including the login window (`gui` extra):
|
|
20
20
|
|
|
21
21
|
```
|
|
22
|
-
moritz@github:~/test$ pip3 install overleaf-sync
|
|
22
|
+
moritz@github:~/test$ pip3 install "overleaf-sync[gui]"
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Depending on your local Python installation, you might need to use `pip` instead of `pip3`. [pipx](https://pipx.pypa.io/) works too: `pipx install "overleaf-sync[gui]"`.
|
|
26
|
+
|
|
27
|
+
> **2.0 pre-release:** Overleaf changed its website in a way that broke version 1.2.0. The fixes are available as a pre-release: `pip3 install --pre "overleaf-sync[gui]"`. Please report any problems in the [issues](https://github.com/moritzgloeckl/overleaf-sync/issues).
|
|
26
28
|
|
|
27
29
|
### Prerequisites
|
|
28
30
|
- Create your project on [Overleaf](https://www.overleaf.com/project), for example a project named `test`. Overleaf-sync is not able to create projects (yet).
|
|
@@ -37,12 +39,21 @@ moritz@github:~/test$ ols login [--path]
|
|
|
37
39
|
Login successful. Cookie persisted as `.olauth`. You may now sync your project.
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
Logging in will be handled by a mini web browser opening on your device (using
|
|
42
|
+
Logging in will be handled by a mini web browser opening on your device (using Qt via PySide6, from the `gui` extra). You can then enter your username and password securely on the official Overleaf website. You might get asked to solve a CAPTCHA in the process. Your credentials are sent to Overleaf over HTTPS.
|
|
41
43
|
|
|
42
44
|
It then stores your *cookie* (**not** your login credentials) in a hidden file called `.olauth` in the same folder you run the command from. It is possible to store the cookie elsewhere using the `--path` option. The cookie file will not be synced to or from Overleaf.
|
|
43
45
|
|
|
44
46
|
Keep the `.olauth` file save, as it can be used to log in into your account.
|
|
45
47
|
|
|
48
|
+
#### Login without the login window
|
|
49
|
+
On a server without a display, or if the login window doesn't work for your account (e.g. single sign-on via ORCID or your institution), log in with a session cookie instead:
|
|
50
|
+
|
|
51
|
+
1. Log in to Overleaf in your normal browser.
|
|
52
|
+
2. Open the developer tools → Application (Firefox: Storage) → Cookies → `https://www.overleaf.com` and copy the value of `overleaf_session2`.
|
|
53
|
+
3. Run `ols login --cookie` and paste the value at the (hidden) prompt, or provide it via the `OLSYNC_COOKIE` environment variable.
|
|
54
|
+
|
|
55
|
+
This works without installing the `gui` extra.
|
|
56
|
+
|
|
46
57
|
### Listing all projects
|
|
47
58
|
```
|
|
48
59
|
moritz@github:~/test$ ols list [--store-path -v/--verbose]
|
|
@@ -6,16 +6,24 @@
|
|
|
6
6
|
# Description: Overleaf API Wrapper
|
|
7
7
|
# Author: Moritz Glöckl
|
|
8
8
|
# License: MIT
|
|
9
|
-
# Version: 1.2.0
|
|
10
9
|
##################################################
|
|
11
10
|
|
|
12
11
|
import requests as reqs
|
|
13
12
|
from bs4 import BeautifulSoup
|
|
14
13
|
import json
|
|
14
|
+
import ssl
|
|
15
15
|
import uuid
|
|
16
|
+
import websocket
|
|
16
17
|
from socketIO_client import SocketIO
|
|
17
18
|
import time
|
|
18
19
|
|
|
20
|
+
# socketIO_client/transports.py has `except websocket.SSLError`, an attribute
|
|
21
|
+
# recent websocket-client (1.9.0+) no longer exposes; that raises AttributeError
|
|
22
|
+
# and masks whatever the actual connection error is. Keep this shim for as long
|
|
23
|
+
# as we depend on socketIO-client.
|
|
24
|
+
if not hasattr(websocket, "SSLError"):
|
|
25
|
+
websocket.SSLError = ssl.SSLError
|
|
26
|
+
|
|
19
27
|
# Where to get the CSRF Token and where to send the login request to
|
|
20
28
|
LOGIN_URL = "https://www.overleaf.com/login"
|
|
21
29
|
PROJECT_URL = "https://www.overleaf.com/project" # The dashboard URL
|
|
@@ -23,7 +31,7 @@ PROJECT_URL = "https://www.overleaf.com/project" # The dashboard URL
|
|
|
23
31
|
DOWNLOAD_URL = "https://www.overleaf.com/project/{}/download/zip"
|
|
24
32
|
UPLOAD_URL = "https://www.overleaf.com/project/{}/upload" # The URL to upload files
|
|
25
33
|
FOLDER_URL = "https://www.overleaf.com/project/{}/folder" # The URL to create folders
|
|
26
|
-
DELETE_URL = "https://www.overleaf.com/project/{}/
|
|
34
|
+
DELETE_URL = "https://www.overleaf.com/project/{}/{}/{}" # The URL to delete files (entity type: doc or file)
|
|
27
35
|
COMPILE_URL = "https://www.overleaf.com/project/{}/compile?enable_pdf_caching=true" # The URL to compile the project
|
|
28
36
|
BASE_URL = "https://www.overleaf.com" # The Overleaf Base URL
|
|
29
37
|
PATH_SEP = "/" # Use hardcoded path separator for both windows and posix system
|
|
@@ -82,14 +90,45 @@ class OverleafClient(object):
|
|
|
82
90
|
|
|
83
91
|
return {"cookie": self._cookie, "csrf": self._csrf}
|
|
84
92
|
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _extract_projects(page_content):
|
|
95
|
+
"""
|
|
96
|
+
Extract the project list from the Overleaf dashboard page.
|
|
97
|
+
|
|
98
|
+
Overleaf used to embed the list as a bare JSON array in a
|
|
99
|
+
<meta name="ol-projects"> tag. It now embeds
|
|
100
|
+
{"totalSize": N, "projects": [...]} in a
|
|
101
|
+
<meta name="ol-prefetchedProjectsBlob"> tag instead. Support both so
|
|
102
|
+
this keeps working if Overleaf reverts or if an older cookie/page is
|
|
103
|
+
cached somewhere.
|
|
104
|
+
"""
|
|
105
|
+
soup = BeautifulSoup(page_content, 'html.parser')
|
|
106
|
+
tag = soup.find('meta', {'name': 'ol-prefetchedProjectsBlob'}) or soup.find('meta', {'name': 'ol-projects'})
|
|
107
|
+
json_content = json.loads(tag.get('content'))
|
|
108
|
+
if isinstance(json_content, dict):
|
|
109
|
+
json_content = json_content.get('projects', [])
|
|
110
|
+
return json_content
|
|
111
|
+
|
|
112
|
+
def fetch_csrf(self):
|
|
113
|
+
"""
|
|
114
|
+
Get the CSRF token from the project dashboard, e.g. after logging in with a cookie copied from a browser
|
|
115
|
+
Returns: the CSRF token, or None if the session cookie is not (or no longer) valid
|
|
116
|
+
"""
|
|
117
|
+
projects_page = reqs.get(PROJECT_URL, cookies=self._cookie, allow_redirects=False)
|
|
118
|
+
if not projects_page.ok or projects_page.is_redirect:
|
|
119
|
+
return None
|
|
120
|
+
csrf_tag = BeautifulSoup(projects_page.content, 'html.parser').find('meta', {'name': 'ol-csrfToken'})
|
|
121
|
+
if csrf_tag is None:
|
|
122
|
+
return None
|
|
123
|
+
return csrf_tag.get('content')
|
|
124
|
+
|
|
85
125
|
def all_projects(self):
|
|
86
126
|
"""
|
|
87
127
|
Get all of a user's active projects (= not archived and not trashed)
|
|
88
128
|
Returns: List of project objects
|
|
89
129
|
"""
|
|
90
130
|
projects_page = reqs.get(PROJECT_URL, cookies=self._cookie)
|
|
91
|
-
json_content =
|
|
92
|
-
BeautifulSoup(projects_page.content, 'html.parser').find('meta', {'name': 'ol-projects'}).get('content'))
|
|
131
|
+
json_content = OverleafClient._extract_projects(projects_page.content)
|
|
93
132
|
return list(OverleafClient.filter_projects(json_content))
|
|
94
133
|
|
|
95
134
|
def get_project(self, project_name):
|
|
@@ -100,8 +139,7 @@ class OverleafClient(object):
|
|
|
100
139
|
"""
|
|
101
140
|
|
|
102
141
|
projects_page = reqs.get(PROJECT_URL, cookies=self._cookie)
|
|
103
|
-
json_content =
|
|
104
|
-
BeautifulSoup(projects_page.content, 'html.parser').find('meta', {'name': 'ol-projects'}).get('content'))
|
|
142
|
+
json_content = OverleafClient._extract_projects(projects_page.content)
|
|
105
143
|
return next(OverleafClient.filter_projects(json_content, {"name": project_name}), None)
|
|
106
144
|
|
|
107
145
|
def download_project(self, project_id):
|
|
@@ -138,8 +176,11 @@ class OverleafClient(object):
|
|
|
138
176
|
|
|
139
177
|
if r.ok:
|
|
140
178
|
return json.loads(r.content)
|
|
141
|
-
elif r.status_code ==
|
|
142
|
-
# Folder already exists
|
|
179
|
+
elif r.status_code == 400:
|
|
180
|
+
# Folder already exists. status_code is an int; comparing to
|
|
181
|
+
# str(400) was always False, so this branch never actually
|
|
182
|
+
# fired -- a real folder-exists response fell through to the
|
|
183
|
+
# HTTPError below instead of silently continuing.
|
|
143
184
|
return
|
|
144
185
|
else:
|
|
145
186
|
raise reqs.HTTPError()
|
|
@@ -155,23 +196,50 @@ class OverleafClient(object):
|
|
|
155
196
|
"""
|
|
156
197
|
project_infos = None
|
|
157
198
|
|
|
158
|
-
# Callback
|
|
159
|
-
|
|
160
|
-
|
|
199
|
+
# Callback for the joinProjectResponse event the server pushes right
|
|
200
|
+
# after a successful connect+auto-join (see below) -- delivered as a
|
|
201
|
+
# plain named event, not as an ack to an explicit emit, and shaped as
|
|
202
|
+
# {"publicId": ..., "project": {...}}. Downstream code (upload_file,
|
|
203
|
+
# delete_file) indexes project_infos['rootFolder'][0][...] directly,
|
|
204
|
+
# so unwrap to the inner "project" dict here rather than returning
|
|
205
|
+
# the wrapper.
|
|
206
|
+
def set_project_infos(data):
|
|
161
207
|
nonlocal project_infos
|
|
162
|
-
project_infos =
|
|
163
|
-
|
|
164
|
-
#
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
208
|
+
project_infos = data.get('project')
|
|
209
|
+
|
|
210
|
+
# The Socket.IO handshake below (done again internally by the SocketIO()
|
|
211
|
+
# constructor) has Overleaf issue a *fresh* load-balancer affinity cookie
|
|
212
|
+
# (currently named GCLB, see olbrowserlogin.COOKIE_NAMES and commit
|
|
213
|
+
# 0cc61e7) on its response -- but socketIO_client calls requests.get()
|
|
214
|
+
# directly rather than through a Session, so that Set-Cookie is silently
|
|
215
|
+
# dropped, and the WebSocket upgrade that follows then 502s without it.
|
|
216
|
+
# Perform the same handshake ourselves first, purely to capture that
|
|
217
|
+
# cookie, and merge it into the header used for the real connection.
|
|
218
|
+
# (Built from whatever cookies are present rather than hardcoded names:
|
|
219
|
+
# a session may not carry every cookie -- this previously raised
|
|
220
|
+
# KeyError here even though overleaf_session2 alone is enough to connect.)
|
|
221
|
+
handshake = reqs.get(
|
|
222
|
+
"{}/socket.io/1/".format(BASE_URL),
|
|
223
|
+
params={"t": int(time.time() * 1000)},
|
|
224
|
+
cookies=self._cookie,
|
|
225
|
+
)
|
|
226
|
+
cookie_dict = dict(self._cookie)
|
|
227
|
+
cookie_dict.update(handshake.cookies.get_dict())
|
|
228
|
+
cookie = "; ".join(
|
|
229
|
+
"{}={}".format(name, value) for name, value in cookie_dict.items()
|
|
169
230
|
)
|
|
170
231
|
|
|
171
|
-
#
|
|
232
|
+
# projectId must be a query parameter on THIS handshake -- the one
|
|
233
|
+
# socketIO_client performs internally when SocketIO() connects, not
|
|
234
|
+
# the pre-fetch above. Overleaf now binds the session to a project at
|
|
235
|
+
# handshake time and rejects the connection otherwise
|
|
236
|
+
# (connectionRejected: "missing/bad ?projectId=... query flag on
|
|
237
|
+
# handshake"); confirmed it rejects even when projectId is only on
|
|
238
|
+
# the WebSocket upgrade URL -- it must be on the initial
|
|
239
|
+
# /socket.io/1/ GET specifically.
|
|
172
240
|
socket_io = SocketIO(
|
|
173
241
|
BASE_URL,
|
|
174
|
-
params={'t': int(time.time())},
|
|
242
|
+
params={'t': int(time.time()), 'projectId': project_id},
|
|
175
243
|
headers={'Cookie': cookie}
|
|
176
244
|
)
|
|
177
245
|
|
|
@@ -179,9 +247,19 @@ class OverleafClient(object):
|
|
|
179
247
|
socket_io.on('connect', lambda: None)
|
|
180
248
|
socket_io.wait_for_callbacks()
|
|
181
249
|
|
|
182
|
-
#
|
|
183
|
-
|
|
184
|
-
|
|
250
|
+
# With projectId on the handshake, the server auto-joins and pushes
|
|
251
|
+
# the result as a joinProjectResponse event -- no explicit joinProject
|
|
252
|
+
# emit needed (confirmed live: the emit's ack callback never fires
|
|
253
|
+
# since the server responds with a named event, not an ack).
|
|
254
|
+
# wait_for_callbacks() only waits for a pending *ack* callback, which
|
|
255
|
+
# .on() never registers -- it would return immediately without
|
|
256
|
+
# reading anything. Wait in short slices instead, stopping as soon as
|
|
257
|
+
# the event has actually been processed.
|
|
258
|
+
socket_io.on('joinProjectResponse', set_project_infos)
|
|
259
|
+
for _ in range(20):
|
|
260
|
+
if project_infos is not None:
|
|
261
|
+
break
|
|
262
|
+
socket_io.wait(seconds=1)
|
|
185
263
|
|
|
186
264
|
# Disconnect from the socket if still connected
|
|
187
265
|
if socket_io.connected:
|
|
@@ -233,13 +311,26 @@ class OverleafClient(object):
|
|
|
233
311
|
"qqtotalfilesize": file_size,
|
|
234
312
|
}
|
|
235
313
|
files = {
|
|
314
|
+
# Overleaf's upload endpoint now rejects the request with 422
|
|
315
|
+
# {"success":false,"error":"invalid_filename"} unless the
|
|
316
|
+
# filename is also present as a plain multipart field, not just
|
|
317
|
+
# in qqfilename above. Confirmed live: identical request without
|
|
318
|
+
# this field gets 422; with it, 200 and the file is actually
|
|
319
|
+
# updated. It must be the bare file name: Overleaf rejects names
|
|
320
|
+
# containing "/" (SafePath), the folder is chosen via folder_id.
|
|
321
|
+
"name": (None, file_name.split(PATH_SEP)[-1]),
|
|
236
322
|
"qqfile": file
|
|
237
323
|
}
|
|
238
324
|
|
|
239
325
|
# Upload the file to the predefined folder
|
|
240
326
|
r = reqs.post(UPLOAD_URL.format(project_id), cookies=self._cookie, params=params, files=files)
|
|
241
327
|
|
|
242
|
-
|
|
328
|
+
# status_code is an int; comparing to str(200) was always False here,
|
|
329
|
+
# so this reported failure on every call regardless of the real
|
|
330
|
+
# result. Harmless today only because callers don't check the return
|
|
331
|
+
# value, but worth fixing since it's the only signal this method
|
|
332
|
+
# gives back.
|
|
333
|
+
return r.status_code == 200 and json.loads(r.content)["success"]
|
|
243
334
|
|
|
244
335
|
def delete_file(self, project_id, project_infos, file_name):
|
|
245
336
|
"""
|
|
@@ -252,23 +343,21 @@ class OverleafClient(object):
|
|
|
252
343
|
Returns: True on success, False on fail
|
|
253
344
|
"""
|
|
254
345
|
|
|
255
|
-
file
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
else:
|
|
271
|
-
file = next((v for v in project_infos['rootFolder'][0]['docs'] if v['name'] == file_name), None)
|
|
346
|
+
# Walk down to the folder that contains the file (folder names match case-insensitively, as in upload_file)
|
|
347
|
+
*local_folders, name = file_name.split(PATH_SEP)
|
|
348
|
+
current_folder = project_infos['rootFolder'][0]
|
|
349
|
+
for local_folder in local_folders:
|
|
350
|
+
current_folder = next((f for f in current_folder['folders'] if f['name'].lower() == local_folder.lower()),
|
|
351
|
+
None)
|
|
352
|
+
if current_folder is None:
|
|
353
|
+
return False
|
|
354
|
+
|
|
355
|
+
# Text files are "docs", binary files (images, PDFs, ...) are "fileRefs" with their own endpoint
|
|
356
|
+
file = next((v for v in current_folder.get('docs', []) if v['name'] == name), None)
|
|
357
|
+
entity_type = "doc"
|
|
358
|
+
if file is None:
|
|
359
|
+
file = next((v for v in current_folder.get('fileRefs', []) if v['name'] == name), None)
|
|
360
|
+
entity_type = "file"
|
|
272
361
|
|
|
273
362
|
# File not found!
|
|
274
363
|
if file is None:
|
|
@@ -278,9 +367,13 @@ class OverleafClient(object):
|
|
|
278
367
|
"X-Csrf-Token": self._csrf
|
|
279
368
|
}
|
|
280
369
|
|
|
281
|
-
r = reqs.delete(DELETE_URL.format(project_id, file['_id']), cookies=self._cookie,
|
|
370
|
+
r = reqs.delete(DELETE_URL.format(project_id, entity_type, file['_id']), cookies=self._cookie,
|
|
371
|
+
headers=headers, json={})
|
|
282
372
|
|
|
283
|
-
|
|
373
|
+
# status_code is an int; comparing to str(204) was always False
|
|
374
|
+
# regardless of the real result, same bug class as upload_file and
|
|
375
|
+
# create_folder above.
|
|
376
|
+
return r.status_code == 204
|
|
284
377
|
|
|
285
378
|
def download_pdf(self, project_id):
|
|
286
379
|
"""
|
|
@@ -299,7 +392,7 @@ class OverleafClient(object):
|
|
|
299
392
|
"check": "silent",
|
|
300
393
|
"draft": False,
|
|
301
394
|
"incrementalCompilesEnabled": True,
|
|
302
|
-
|
|
395
|
+
# No rootDoc_id: Overleaf validates it as an optional ObjectId and rejects "" (400)
|
|
303
396
|
"stopOnFirstError": False
|
|
304
397
|
}
|
|
305
398
|
|
|
@@ -315,7 +408,15 @@ class OverleafClient(object):
|
|
|
315
408
|
|
|
316
409
|
pdf_file = next(v for v in compile_result['outputFiles'] if v['type'] == 'pdf')
|
|
317
410
|
|
|
318
|
-
|
|
411
|
+
# The output is only served (otherwise 404) when the compile group and CLSI server are passed along,
|
|
412
|
+
# like Overleaf's own frontend does. Self-hosted servers may not send them.
|
|
413
|
+
params = {}
|
|
414
|
+
if compile_result.get("compileGroup"):
|
|
415
|
+
params["compileGroup"] = compile_result["compileGroup"]
|
|
416
|
+
if compile_result.get("clsiServerId"):
|
|
417
|
+
params["clsiserverid"] = compile_result["clsiServerId"]
|
|
418
|
+
|
|
419
|
+
download_req = reqs.get(BASE_URL + pdf_file['url'], cookies=self._cookie, headers=headers, params=params)
|
|
319
420
|
|
|
320
421
|
if download_req.ok:
|
|
321
422
|
return pdf_file['path'], download_req.content
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
# Description: Overleaf Two-Way Sync
|
|
7
7
|
# Author: Moritz Glöckl
|
|
8
8
|
# License: MIT
|
|
9
|
-
# Version: 1.2.0
|
|
10
9
|
##################################################
|
|
11
10
|
|
|
12
11
|
import click
|
|
13
12
|
import os
|
|
13
|
+
import sys
|
|
14
14
|
from yaspin import yaspin
|
|
15
15
|
import pickle
|
|
16
16
|
import zipfile
|
|
@@ -24,11 +24,21 @@ from pathlib import Path
|
|
|
24
24
|
try:
|
|
25
25
|
# Import for pip installation / wheel
|
|
26
26
|
from olsync.olclient import OverleafClient
|
|
27
|
-
import olsync.olbrowserlogin as olbrowserlogin
|
|
28
27
|
except ImportError:
|
|
29
28
|
# Import for development
|
|
30
29
|
from olclient import OverleafClient
|
|
31
|
-
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def configure_console():
|
|
33
|
+
"""
|
|
34
|
+
Windows consoles often default to a legacy codepage (e.g. cp1252) that can't
|
|
35
|
+
encode the emoji/braille characters used for status output and the spinner,
|
|
36
|
+
which crashes stdout.write() with UnicodeEncodeError. Force UTF-8 output.
|
|
37
|
+
Done when the CLI starts rather than on import, so importing olsync has no side effects.
|
|
38
|
+
"""
|
|
39
|
+
for stream in (sys.stdout, sys.stderr):
|
|
40
|
+
if hasattr(stream, "reconfigure"):
|
|
41
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
32
42
|
|
|
33
43
|
|
|
34
44
|
@click.group(invoke_without_command=True)
|
|
@@ -48,6 +58,7 @@ except ImportError:
|
|
|
48
58
|
@click.version_option(package_name='overleaf-sync')
|
|
49
59
|
@click.pass_context
|
|
50
60
|
def main(ctx, local, remote, project_name, cookie_path, sync_path, olignore_path, verbose):
|
|
61
|
+
configure_console()
|
|
51
62
|
if ctx.invoked_subcommand is None:
|
|
52
63
|
if not os.path.isfile(cookie_path):
|
|
53
64
|
raise click.ClickException(
|
|
@@ -87,9 +98,11 @@ def main(ctx, local, remote, project_name, cookie_path, sync_path, olignore_path
|
|
|
87
98
|
sync = not (local or remote)
|
|
88
99
|
|
|
89
100
|
if remote or sync:
|
|
101
|
+
remote_files = zip_file.namelist()
|
|
102
|
+
deleted_files = [] if sync else [f for f in olignore_keep_list(olignore_path) if f not in remote_files]
|
|
90
103
|
sync_func(
|
|
91
|
-
files_from=
|
|
92
|
-
deleted_files=
|
|
104
|
+
files_from=remote_files,
|
|
105
|
+
deleted_files=deleted_files,
|
|
93
106
|
create_file_at_to=lambda name: write_file(name, zip_file.read(name)),
|
|
94
107
|
delete_file_at_to=lambda name: delete_file(name),
|
|
95
108
|
create_file_at_from=lambda name: overleaf_client.upload_file(
|
|
@@ -102,9 +115,12 @@ def main(ctx, local, remote, project_name, cookie_path, sync_path, olignore_path
|
|
|
102
115
|
to_name="local",
|
|
103
116
|
verbose=verbose)
|
|
104
117
|
if local or sync:
|
|
118
|
+
# Computed once (#60), and after the remote -> local pass so newly downloaded files are included
|
|
119
|
+
local_files = olignore_keep_list(olignore_path)
|
|
120
|
+
deleted_files = [] if sync else [f for f in zip_file.namelist() if f not in local_files]
|
|
105
121
|
sync_func(
|
|
106
|
-
files_from=
|
|
107
|
-
deleted_files=
|
|
122
|
+
files_from=local_files,
|
|
123
|
+
deleted_files=deleted_files,
|
|
108
124
|
create_file_at_to=lambda name: overleaf_client.upload_file(
|
|
109
125
|
project["id"], project_infos, name, os.path.getsize(name), open(name, 'rb')),
|
|
110
126
|
delete_file_at_to=lambda name: overleaf_client.delete_file(project["id"], project_infos, name),
|
|
@@ -121,13 +137,27 @@ def main(ctx, local, remote, project_name, cookie_path, sync_path, olignore_path
|
|
|
121
137
|
@main.command()
|
|
122
138
|
@click.option('--path', 'cookie_path', default=".olauth", type=click.Path(exists=False),
|
|
123
139
|
help="Path to store the persisted Overleaf cookie.")
|
|
140
|
+
@click.option('--cookie', 'use_cookie', is_flag=True,
|
|
141
|
+
help="Log in without the login window, using the `overleaf_session2` cookie from a browser that is "
|
|
142
|
+
"logged in to Overleaf (asked for at a hidden prompt, or read from OLSYNC_COOKIE). Useful on "
|
|
143
|
+
"servers without a display and for single sign-on accounts.")
|
|
124
144
|
@click.option('-v', '--verbose', 'verbose', is_flag=True, help="Enable extended error logging.")
|
|
125
|
-
def login(cookie_path, verbose):
|
|
145
|
+
def login(cookie_path, use_cookie, verbose):
|
|
126
146
|
if os.path.isfile(cookie_path) and not click.confirm(
|
|
127
147
|
'Persisted Overleaf cookie already exist. Do you want to override it?'):
|
|
128
148
|
return
|
|
149
|
+
if use_cookie:
|
|
150
|
+
# Never accept the cookie as a command line argument, it would end up in the shell history
|
|
151
|
+
cookie = os.environ.get("OLSYNC_COOKIE") or click.prompt("Value of the overleaf_session2 cookie",
|
|
152
|
+
hide_input=True)
|
|
153
|
+
execute_action(lambda: cookie_login_handler(cookie.strip(), cookie_path), "Login",
|
|
154
|
+
"Login successful. Cookie persisted as `" + click.format_filename(
|
|
155
|
+
cookie_path) + "`. You may now sync your project.",
|
|
156
|
+
"Login failed: the cookie is not valid (expired, or copied incompletely).", verbose)
|
|
157
|
+
return
|
|
158
|
+
olbrowserlogin = load_browser_login()
|
|
129
159
|
click.clear()
|
|
130
|
-
execute_action(lambda: login_handler(cookie_path), "Login",
|
|
160
|
+
execute_action(lambda: login_handler(olbrowserlogin, cookie_path), "Login",
|
|
131
161
|
"Login successful. Cookie persisted as `" + click.format_filename(
|
|
132
162
|
cookie_path) + "`. You may now sync your project.",
|
|
133
163
|
"Login failed. Please try again.", verbose)
|
|
@@ -203,7 +233,36 @@ def download_pdf(project_name, download_path, cookie_path, verbose):
|
|
|
203
233
|
"Downloading project's PDF failed. Please try again.", verbose)
|
|
204
234
|
|
|
205
235
|
|
|
206
|
-
def
|
|
236
|
+
def load_browser_login():
|
|
237
|
+
"""
|
|
238
|
+
Import the login window module. PySide6 is an optional dependency (the "gui" extra),
|
|
239
|
+
so it is only imported when logging in.
|
|
240
|
+
"""
|
|
241
|
+
try:
|
|
242
|
+
try:
|
|
243
|
+
# Import for pip installation / wheel
|
|
244
|
+
import olsync.olbrowserlogin as olbrowserlogin
|
|
245
|
+
except ImportError as e:
|
|
246
|
+
if not (e.name or "").startswith("olsync"):
|
|
247
|
+
raise
|
|
248
|
+
# Import for development (running olsync.py from inside the olsync folder)
|
|
249
|
+
import olbrowserlogin
|
|
250
|
+
except ImportError as e:
|
|
251
|
+
if (e.name or "").startswith("PySide6"):
|
|
252
|
+
raise click.ClickException(
|
|
253
|
+
"The login window needs PySide6. Install it with: pip install \"overleaf-sync[gui]\"\n"
|
|
254
|
+
"Alternatively, log in without the window: ols login --cookie")
|
|
255
|
+
# PySide6 is installed but can't be loaded, e.g. because another Qt installation (such as Anaconda's)
|
|
256
|
+
# conflicts with it (#59)
|
|
257
|
+
raise click.ClickException(
|
|
258
|
+
f"The login window could not be loaded: {e}\n"
|
|
259
|
+
"This usually means another Qt installation (e.g. from Anaconda) conflicts with PySide6. Installing "
|
|
260
|
+
"overleaf-sync into a fresh virtual environment based on a non-conda Python usually helps.\n"
|
|
261
|
+
"Alternatively, log in without the window: ols login --cookie")
|
|
262
|
+
return olbrowserlogin
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def login_handler(olbrowserlogin, path):
|
|
207
266
|
store = olbrowserlogin.login()
|
|
208
267
|
if store is None:
|
|
209
268
|
return False
|
|
@@ -212,6 +271,16 @@ def login_handler(path):
|
|
|
212
271
|
return True
|
|
213
272
|
|
|
214
273
|
|
|
274
|
+
def cookie_login_handler(cookie, path):
|
|
275
|
+
cookies = {"overleaf_session2": cookie}
|
|
276
|
+
csrf = OverleafClient(cookies).fetch_csrf()
|
|
277
|
+
if csrf is None:
|
|
278
|
+
return False
|
|
279
|
+
with open(path, 'wb+') as f:
|
|
280
|
+
pickle.dump({"cookie": cookies, "csrf": csrf}, f)
|
|
281
|
+
return True
|
|
282
|
+
|
|
283
|
+
|
|
215
284
|
def delete_file(path):
|
|
216
285
|
_dir = os.path.dirname(path)
|
|
217
286
|
if _dir == path:
|
|
@@ -376,7 +445,8 @@ def olignore_keep_list(olignore_path):
|
|
|
376
445
|
else:
|
|
377
446
|
click.echo("\n.olignore: using %s to filter items" % olignore_path)
|
|
378
447
|
with open(olignore_path, 'r') as f:
|
|
379
|
-
|
|
448
|
+
# "./main.tex" and "main.tex" mean the same file, matched paths have no "./" prefix (#72)
|
|
449
|
+
ignore_pattern = [pattern.removeprefix("./") for pattern in f.read().splitlines()]
|
|
380
450
|
|
|
381
451
|
keep_list = [f for f in files if not any(
|
|
382
452
|
fnmatch.fnmatch(f, ignore) for ignore in ignore_pattern)]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=3.4,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "overleaf-sync"
|
|
7
|
+
authors = [{ name = "Moritz Glöckl", email = "moritzgloeckl@users.noreply.github.com" }]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = { file = "LICENSE" }
|
|
10
|
+
classifiers = [
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Intended Audience :: Science/Research",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
]
|
|
15
|
+
keywords = ["overleaf", "sync", "latex", "tex"]
|
|
16
|
+
requires-python = ">=3.11"
|
|
17
|
+
dynamic = ["version", "description"]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"requests >=2.31,<3",
|
|
20
|
+
"beautifulsoup4 >=4.12,<5",
|
|
21
|
+
"yaspin >=3,<4",
|
|
22
|
+
"python-dateutil >=2.8,<3",
|
|
23
|
+
"click >=8.1,<9",
|
|
24
|
+
"socketIO-client ==0.5.7.2", # Do not upgrade, see AGENTS.md
|
|
25
|
+
"websocket-client >=1,<2",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
gui = ["PySide6 >=6.5,<7"]
|
|
30
|
+
dev = ["pytest >=8", "pytest-cov >=5", "responses >=0.25", "ruff >=0.6"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/moritzgloeckl/overleaf-sync"
|
|
34
|
+
Issues = "https://github.com/moritzgloeckl/overleaf-sync/issues"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
ols = "olsync.olsync:main"
|
|
38
|
+
overleaf-sync = "olsync.olsync:main"
|
|
39
|
+
|
|
40
|
+
[tool.flit.module]
|
|
41
|
+
name = "olsync"
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
45
|
+
addopts = "-m 'not integration and not smoke'"
|
|
46
|
+
filterwarnings = ["ignore:color, on_color and attrs are not supported:UserWarning:yaspin"]
|
|
47
|
+
markers = [
|
|
48
|
+
"integration: needs the Overleaf Community Edition container (tests/integration/compose.yml)",
|
|
49
|
+
"smoke: runs against overleaf.com with a real session, local only (see tests/smoke/)",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
target-version = "py311"
|
|
54
|
+
line-length = 120
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["E9", "F"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.per-file-ignores]
|
|
60
|
+
"olsync/olbrowserlogin.py" = ["F403", "F405"]
|
overleaf-sync-1.2.0/.gitignore
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# Byte-compiled / optimized / DLL files
|
|
2
|
-
__pycache__/
|
|
3
|
-
*.py[cod]
|
|
4
|
-
*$py.class
|
|
5
|
-
|
|
6
|
-
# C extensions
|
|
7
|
-
*.so
|
|
8
|
-
|
|
9
|
-
# Distribution / packaging
|
|
10
|
-
.Python
|
|
11
|
-
build/
|
|
12
|
-
develop-eggs/
|
|
13
|
-
dist/
|
|
14
|
-
downloads/
|
|
15
|
-
eggs/
|
|
16
|
-
.eggs/
|
|
17
|
-
lib/
|
|
18
|
-
lib64/
|
|
19
|
-
parts/
|
|
20
|
-
sdist/
|
|
21
|
-
var/
|
|
22
|
-
wheels/
|
|
23
|
-
*.egg-info/
|
|
24
|
-
.installed.cfg
|
|
25
|
-
*.egg
|
|
26
|
-
MANIFEST
|
|
27
|
-
pip-wheel-metadata/
|
|
28
|
-
|
|
29
|
-
# PyInstaller
|
|
30
|
-
# Usually these files are written by a python script from a template
|
|
31
|
-
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
-
*.manifest
|
|
33
|
-
*.spec
|
|
34
|
-
|
|
35
|
-
# Installer logs
|
|
36
|
-
pip-log.txt
|
|
37
|
-
pip-delete-this-directory.txt
|
|
38
|
-
|
|
39
|
-
# Unit test / coverage reports
|
|
40
|
-
htmlcov/
|
|
41
|
-
.tox/
|
|
42
|
-
.coverage
|
|
43
|
-
.coverage.*
|
|
44
|
-
.cache
|
|
45
|
-
nosetests.xml
|
|
46
|
-
coverage.xml
|
|
47
|
-
*.cover
|
|
48
|
-
.hypothesis/
|
|
49
|
-
.pytest_cache/
|
|
50
|
-
|
|
51
|
-
# Translations
|
|
52
|
-
*.mo
|
|
53
|
-
*.pot
|
|
54
|
-
|
|
55
|
-
# Django stuff:
|
|
56
|
-
*.log
|
|
57
|
-
local_settings.py
|
|
58
|
-
db.sqlite3
|
|
59
|
-
|
|
60
|
-
# Flask stuff:
|
|
61
|
-
instance/
|
|
62
|
-
.webassets-cache
|
|
63
|
-
|
|
64
|
-
# Scrapy stuff:
|
|
65
|
-
.scrapy
|
|
66
|
-
|
|
67
|
-
# Sphinx documentation
|
|
68
|
-
docs/_build/
|
|
69
|
-
|
|
70
|
-
# PyBuilder
|
|
71
|
-
target/
|
|
72
|
-
|
|
73
|
-
# Jupyter Notebook
|
|
74
|
-
.ipynb_checkpoints
|
|
75
|
-
|
|
76
|
-
# pyenv
|
|
77
|
-
.python-version
|
|
78
|
-
|
|
79
|
-
# celery beat schedule file
|
|
80
|
-
celerybeat-schedule
|
|
81
|
-
|
|
82
|
-
# SageMath parsed files
|
|
83
|
-
*.sage.py
|
|
84
|
-
|
|
85
|
-
# Environments
|
|
86
|
-
.env
|
|
87
|
-
.venv
|
|
88
|
-
env/
|
|
89
|
-
venv/
|
|
90
|
-
ENV/
|
|
91
|
-
env.bak/
|
|
92
|
-
venv.bak/
|
|
93
|
-
|
|
94
|
-
# Spyder project settings
|
|
95
|
-
.spyderproject
|
|
96
|
-
.spyproject
|
|
97
|
-
|
|
98
|
-
# Rope project settings
|
|
99
|
-
.ropeproject
|
|
100
|
-
|
|
101
|
-
# mkdocs documentation
|
|
102
|
-
/site
|
|
103
|
-
|
|
104
|
-
# mypy
|
|
105
|
-
.mypy_cache/
|
|
106
|
-
|
|
107
|
-
#PyCharm
|
|
108
|
-
.idea/
|
|
109
|
-
|
|
110
|
-
# vscode
|
|
111
|
-
.vscode
|
|
112
|
-
.DS_Store
|
|
113
|
-
|
|
114
|
-
# dev testing
|
|
115
|
-
setup.py
|
|
116
|
-
test*
|
|
117
|
-
.olauth
|
|
118
|
-
.olignore
|
overleaf-sync-1.2.0/PKG-INFO
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["flit"]
|
|
3
|
-
build-backend = "flit.buildapi"
|
|
4
|
-
|
|
5
|
-
[tool.flit.metadata]
|
|
6
|
-
module = "olsync"
|
|
7
|
-
dist-name = "overleaf-sync"
|
|
8
|
-
description-file = "README.md"
|
|
9
|
-
author = "Moritz Glöckl"
|
|
10
|
-
author-email = "moritzgloeckl@users.noreply.github.com"
|
|
11
|
-
home-page = "https://github.com/moritzgloeckl/overleaf-sync"
|
|
12
|
-
classifiers = ["License :: OSI Approved :: MIT License", "Intended Audience :: Science/Research", "Programming Language :: Python :: 3"]
|
|
13
|
-
requires-python = ">=3"
|
|
14
|
-
requires = [
|
|
15
|
-
"requests == 2.*",
|
|
16
|
-
"beautifulsoup4 == 4.11.1",
|
|
17
|
-
"yaspin == 2.*",
|
|
18
|
-
"python-dateutil~=2.8.1",
|
|
19
|
-
"click == 8.*",
|
|
20
|
-
"socketIO-client == 0.5.7.2",
|
|
21
|
-
"PySide6 == 6.*"
|
|
22
|
-
]
|
|
23
|
-
keywords = "overleaf sync latex tex"
|
|
24
|
-
|
|
25
|
-
[tool.flit.scripts]
|
|
26
|
-
ols = "olsync.olsync:main"
|
overleaf-sync-1.2.0/setup.py
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
# setup.py generated by flit for tools that don't yet use PEP 517
|
|
3
|
-
|
|
4
|
-
from distutils.core import setup
|
|
5
|
-
|
|
6
|
-
packages = \
|
|
7
|
-
['olsync']
|
|
8
|
-
|
|
9
|
-
package_data = \
|
|
10
|
-
{'': ['*']}
|
|
11
|
-
|
|
12
|
-
install_requires = \
|
|
13
|
-
['requests == 2.*',
|
|
14
|
-
'beautifulsoup4 == 4.11.1',
|
|
15
|
-
'yaspin == 2.*',
|
|
16
|
-
'python-dateutil~=2.8.1',
|
|
17
|
-
'click == 8.*',
|
|
18
|
-
'socketIO-client == 0.5.7.2',
|
|
19
|
-
'PySide6 == 6.*']
|
|
20
|
-
|
|
21
|
-
entry_points = \
|
|
22
|
-
{'console_scripts': ['ols = olsync.olsync:main']}
|
|
23
|
-
|
|
24
|
-
setup(name='overleaf-sync',
|
|
25
|
-
version='1.2.0',
|
|
26
|
-
description='Overleaf Two-Way Sync Tool',
|
|
27
|
-
author='Moritz Glöckl',
|
|
28
|
-
author_email='moritzgloeckl@users.noreply.github.com',
|
|
29
|
-
url='https://github.com/moritzgloeckl/overleaf-sync',
|
|
30
|
-
packages=packages,
|
|
31
|
-
package_data=package_data,
|
|
32
|
-
install_requires=install_requires,
|
|
33
|
-
entry_points=entry_points,
|
|
34
|
-
python_requires='>=3',
|
|
35
|
-
)
|
|
File without changes
|