jupyter-server-ydoc 2.1.2__py3-none-any.whl → 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jupyter_server_ydoc/_version.py +1 -1
- jupyter_server_ydoc/app.py +12 -1
- jupyter_server_ydoc/loaders.py +46 -4
- jupyter_server_ydoc/stores.py +5 -1
- jupyter_server_ydoc/test_utils.py +3 -0
- {jupyter_server_ydoc-2.1.2.dist-info → jupyter_server_ydoc-2.2.0.dist-info}/METADATA +1 -1
- {jupyter_server_ydoc-2.1.2.dist-info → jupyter_server_ydoc-2.2.0.dist-info}/RECORD +10 -10
- {jupyter_server_ydoc-2.1.2.dist-info → jupyter_server_ydoc-2.2.0.dist-info}/WHEEL +1 -1
- {jupyter_server_ydoc-2.1.2.data → jupyter_server_ydoc-2.2.0.data}/data/etc/jupyter/jupyter_server_config.d/jupyter_collaboration.json +0 -0
- {jupyter_server_ydoc-2.1.2.dist-info → jupyter_server_ydoc-2.2.0.dist-info}/licenses/LICENSE +0 -0
jupyter_server_ydoc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.
|
|
1
|
+
__version__ = "2.2.0"
|
jupyter_server_ydoc/app.py
CHANGED
|
@@ -50,6 +50,14 @@ class YDocExtension(ExtensionApp):
|
|
|
50
50
|
saving changes from the front-end.""",
|
|
51
51
|
)
|
|
52
52
|
|
|
53
|
+
file_stop_poll_on_errors_after = Float(
|
|
54
|
+
24 * 60 * 60,
|
|
55
|
+
allow_none=True,
|
|
56
|
+
config=True,
|
|
57
|
+
help="""The duration in seconds to stop polling a file after consecutive errors.
|
|
58
|
+
Defaults to 24 hours, if None then polling will not stop on errors.""",
|
|
59
|
+
)
|
|
60
|
+
|
|
53
61
|
document_cleanup_delay = Float(
|
|
54
62
|
60,
|
|
55
63
|
allow_none=True,
|
|
@@ -121,7 +129,10 @@ class YDocExtension(ExtensionApp):
|
|
|
121
129
|
# the global app settings in which the file id manager will register
|
|
122
130
|
# itself maybe at a later time.
|
|
123
131
|
self.file_loaders = FileLoaderMapping(
|
|
124
|
-
self.serverapp.web_app.settings,
|
|
132
|
+
self.serverapp.web_app.settings,
|
|
133
|
+
self.log,
|
|
134
|
+
self.file_poll_interval,
|
|
135
|
+
file_stop_poll_on_errors_after=self.file_stop_poll_on_errors_after,
|
|
125
136
|
)
|
|
126
137
|
|
|
127
138
|
self.handlers.extend(
|
jupyter_server_ydoc/loaders.py
CHANGED
|
@@ -5,7 +5,10 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
7
7
|
from logging import Logger, getLogger
|
|
8
|
+
from time import time
|
|
8
9
|
from typing import Any, Callable, Coroutine
|
|
10
|
+
from tornado.web import HTTPError
|
|
11
|
+
from http import HTTPStatus
|
|
9
12
|
|
|
10
13
|
from jupyter_server.services.contents.manager import (
|
|
11
14
|
AsyncContentsManager,
|
|
@@ -29,12 +32,16 @@ class FileLoader:
|
|
|
29
32
|
contents_manager: AsyncContentsManager | ContentsManager,
|
|
30
33
|
log: Logger | None = None,
|
|
31
34
|
poll_interval: float | None = None,
|
|
35
|
+
max_consecutive_logs: int = 3,
|
|
36
|
+
stop_poll_on_errors_after: float | None = None,
|
|
32
37
|
) -> None:
|
|
33
38
|
self._file_id: str = file_id
|
|
34
39
|
|
|
35
40
|
self._lock = asyncio.Lock()
|
|
36
41
|
self._poll_interval = poll_interval
|
|
42
|
+
self._stop_poll_on_errors_after = stop_poll_on_errors_after
|
|
37
43
|
self._file_id_manager = file_id_manager
|
|
44
|
+
self._max_consecutive_logs = max_consecutive_logs
|
|
38
45
|
self._contents_manager = contents_manager
|
|
39
46
|
|
|
40
47
|
self._log = log or getLogger(__name__)
|
|
@@ -125,6 +132,14 @@ class FileLoader:
|
|
|
125
132
|
model = await ensure_async(
|
|
126
133
|
self._contents_manager.get(self.path, format=format, type=file_type, content=True)
|
|
127
134
|
)
|
|
135
|
+
if (
|
|
136
|
+
file_type == "file"
|
|
137
|
+
and "content" in model
|
|
138
|
+
and model["content"]
|
|
139
|
+
and "\r\n" in model["content"]
|
|
140
|
+
):
|
|
141
|
+
model["content"] = model["content"].replace("\r\n", "\n")
|
|
142
|
+
self._log.debug("Normalizing line endings for %s file on content load", self.path)
|
|
128
143
|
self.last_modified = model["last_modified"]
|
|
129
144
|
return model
|
|
130
145
|
|
|
@@ -204,8 +219,8 @@ class FileLoader:
|
|
|
204
219
|
return
|
|
205
220
|
|
|
206
221
|
consecutive_error_logs = 0
|
|
207
|
-
max_consecutive_logs = 3
|
|
208
222
|
suppression_logged = False
|
|
223
|
+
consecutive_errors_started = None
|
|
209
224
|
|
|
210
225
|
while True:
|
|
211
226
|
try:
|
|
@@ -214,13 +229,37 @@ class FileLoader:
|
|
|
214
229
|
await self.maybe_notify()
|
|
215
230
|
consecutive_error_logs = 0
|
|
216
231
|
suppression_logged = False
|
|
232
|
+
consecutive_errors_started = None
|
|
217
233
|
except Exception as e:
|
|
218
|
-
if
|
|
219
|
-
|
|
234
|
+
# We do not want to terminate the watcher if the content manager request
|
|
235
|
+
# fails due to timeout, server error or similar temporary issue; we only
|
|
236
|
+
# terminate if the file is not found or we get unauthorized error for
|
|
237
|
+
# an extended period of time.
|
|
238
|
+
if isinstance(e, HTTPError) and e.status_code in {
|
|
239
|
+
HTTPStatus.NOT_FOUND,
|
|
240
|
+
HTTPStatus.UNAUTHORIZED,
|
|
241
|
+
}:
|
|
242
|
+
if (
|
|
243
|
+
consecutive_errors_started
|
|
244
|
+
and self._stop_poll_on_errors_after is not None
|
|
245
|
+
):
|
|
246
|
+
errors_duration = time() - consecutive_errors_started
|
|
247
|
+
if errors_duration > self._stop_poll_on_errors_after:
|
|
248
|
+
self._log.warning(
|
|
249
|
+
"Stopping watching file due to consecutive errors over %s seconds: %s",
|
|
250
|
+
self._stop_poll_on_errors_after,
|
|
251
|
+
self.path,
|
|
252
|
+
)
|
|
253
|
+
break
|
|
254
|
+
else:
|
|
255
|
+
consecutive_errors_started = time()
|
|
256
|
+
# Otherwise we just log the error
|
|
257
|
+
if consecutive_error_logs < self._max_consecutive_logs:
|
|
258
|
+
self._log.error("Error watching file %s: %s", self.path, e, exc_info=e)
|
|
220
259
|
consecutive_error_logs += 1
|
|
221
260
|
elif not suppression_logged:
|
|
222
261
|
self._log.warning(
|
|
223
|
-
"Too many errors while watching %s
|
|
262
|
+
"Too many errors while watching %s - suppressing further logs.",
|
|
224
263
|
self.path,
|
|
225
264
|
)
|
|
226
265
|
suppression_logged = True
|
|
@@ -268,6 +307,7 @@ class FileLoaderMapping:
|
|
|
268
307
|
settings: dict,
|
|
269
308
|
log: Logger | None = None,
|
|
270
309
|
file_poll_interval: float | None = None,
|
|
310
|
+
file_stop_poll_on_errors_after: float | None = None,
|
|
271
311
|
) -> None:
|
|
272
312
|
"""
|
|
273
313
|
Args:
|
|
@@ -279,6 +319,7 @@ class FileLoaderMapping:
|
|
|
279
319
|
self.__dict: dict[str, FileLoader] = {}
|
|
280
320
|
self.log = log or getLogger(__name__)
|
|
281
321
|
self.file_poll_interval = file_poll_interval
|
|
322
|
+
self._stop_poll_on_errors_after = file_stop_poll_on_errors_after
|
|
282
323
|
|
|
283
324
|
@property
|
|
284
325
|
def contents_manager(self) -> AsyncContentsManager | ContentsManager:
|
|
@@ -309,6 +350,7 @@ class FileLoaderMapping:
|
|
|
309
350
|
self.contents_manager,
|
|
310
351
|
self.log,
|
|
311
352
|
self.file_poll_interval,
|
|
353
|
+
stop_poll_on_errors_after=self._stop_poll_on_errors_after,
|
|
312
354
|
)
|
|
313
355
|
self.__dict[file_id] = file
|
|
314
356
|
|
jupyter_server_ydoc/stores.py
CHANGED
|
@@ -7,7 +7,11 @@ from traitlets import Int, Unicode
|
|
|
7
7
|
from traitlets.config import LoggingConfigurable
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class
|
|
10
|
+
class TempFileYStoreMetaclass(type(LoggingConfigurable), type(_TempFileYStore)): # type: ignore
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TempFileYStore(LoggingConfigurable, _TempFileYStore, metaclass=TempFileYStoreMetaclass):
|
|
11
15
|
prefix_dir = "jupyter_ystore_"
|
|
12
16
|
|
|
13
17
|
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
from typing import Any
|
|
8
|
+
from tornado.web import HTTPError
|
|
8
9
|
|
|
9
10
|
from jupyter_server import _tz as tz
|
|
10
11
|
|
|
@@ -40,6 +41,8 @@ class FakeContentsManager:
|
|
|
40
41
|
def get(
|
|
41
42
|
self, path: str, content: bool = True, format: str | None = None, type: str | None = None
|
|
42
43
|
) -> dict:
|
|
44
|
+
if not self.model:
|
|
45
|
+
raise HTTPError(404, f"File not found: {path}")
|
|
43
46
|
self.actions.append("get")
|
|
44
47
|
return self.model
|
|
45
48
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jupyter-server-ydoc
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: jupyter-server extension integrating collaborative shared models.
|
|
5
5
|
Project-URL: Documentation, https://jupyterlab-realtime-collaboration.readthedocs.io/
|
|
6
6
|
Project-URL: Repository, https://github.com/jupyterlab/jupyter-collaboration
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
jupyter_server_ydoc/__init__.py,sha256=B8H7XLhzgrTCQD8304Lx91FYXslwabsnV9OuYu4M4Hw,346
|
|
2
|
-
jupyter_server_ydoc/_version.py,sha256=
|
|
3
|
-
jupyter_server_ydoc/app.py,sha256=
|
|
2
|
+
jupyter_server_ydoc/_version.py,sha256=DKk-1b-rZsJFxFi1JoJ7TmEvIEQ0rf-C9HAZWwvjuM0,22
|
|
3
|
+
jupyter_server_ydoc/app.py,sha256=hDqOeb-CBr48ck0WGlJvjzUH_5JvKFw0Eu5FTcW_IqU,8543
|
|
4
4
|
jupyter_server_ydoc/handlers.py,sha256=1ZCdwtrlqY2Q9d_A-HV2RL7-bmzcgKvF4H_6PoYoOpQ,28794
|
|
5
|
-
jupyter_server_ydoc/loaders.py,sha256=
|
|
5
|
+
jupyter_server_ydoc/loaders.py,sha256=fnlxbj-TowKUsB0zAwgnz3S999pjuy-K-Au5u9oespM,13609
|
|
6
6
|
jupyter_server_ydoc/pytest_plugin.py,sha256=wpRsCss-TxgCeo8zqJru-YsKflfCGG-T2dbhcfQtMeY,8746
|
|
7
7
|
jupyter_server_ydoc/rooms.py,sha256=dKronZ09LeXS7B5zLQwT6JMbsat75C22-MTg1tjbIq4,13086
|
|
8
|
-
jupyter_server_ydoc/stores.py,sha256=
|
|
9
|
-
jupyter_server_ydoc/test_utils.py,sha256=
|
|
8
|
+
jupyter_server_ydoc/stores.py,sha256=9gI37OttJT17waClyDWssm7jtxCAXIcUHY7rc154OTk,1146
|
|
9
|
+
jupyter_server_ydoc/test_utils.py,sha256=pD4gP44O7le-PSis7qQoVAKRJvutDOmav-wHRiHs4_U,1661
|
|
10
10
|
jupyter_server_ydoc/utils.py,sha256=_wI4CFOEZK4MSMYCZe2moSbggUTRdGxY4qTzSDEFzdE,2183
|
|
11
11
|
jupyter_server_ydoc/websocketserver.py,sha256=4Pxl4EIVIz5TxzNWlSDF6KNzZVkzy5SsgvwJx0PoGtY,5099
|
|
12
12
|
jupyter_server_ydoc/events/awareness.yaml,sha256=2FrCci5rZIaU4rn8pIPZJkd132YAZdzKjSNSwjOY7Dk,755
|
|
13
13
|
jupyter_server_ydoc/events/fork.yaml,sha256=3OrhQjhVyLjlBJWMiffbnZodL3GzFafLwEmSBFrK33o,1303
|
|
14
14
|
jupyter_server_ydoc/events/session.yaml,sha256=PS0MxowpRwY5QFYm-LJvHUxKHnsictV8_6VEwfhYxcQ,1596
|
|
15
|
-
jupyter_server_ydoc-2.
|
|
16
|
-
jupyter_server_ydoc-2.
|
|
17
|
-
jupyter_server_ydoc-2.
|
|
18
|
-
jupyter_server_ydoc-2.
|
|
19
|
-
jupyter_server_ydoc-2.
|
|
15
|
+
jupyter_server_ydoc-2.2.0.data/data/etc/jupyter/jupyter_server_config.d/jupyter_collaboration.json,sha256=0thh2hJUxAKkZSmneJMG0U6QJRjdM6zGlwrTedEt-Jk,94
|
|
16
|
+
jupyter_server_ydoc-2.2.0.dist-info/METADATA,sha256=kLOSYxFwI2QUItWHhxVaNobR-11JIR8SUwfR3iAg_OI,5571
|
|
17
|
+
jupyter_server_ydoc-2.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
18
|
+
jupyter_server_ydoc-2.2.0.dist-info/licenses/LICENSE,sha256=mhO0ZW9EiWOPg0dUgB-lNbJ0CGwRmTdbeAg_se1SOnY,2833
|
|
19
|
+
jupyter_server_ydoc-2.2.0.dist-info/RECORD,,
|
|
File without changes
|
{jupyter_server_ydoc-2.1.2.dist-info → jupyter_server_ydoc-2.2.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|