jupyter-server-ydoc 2.1.2__tar.gz → 2.2.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.
Files changed (29) hide show
  1. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/PKG-INFO +1 -1
  2. jupyter_server_ydoc-2.2.0/jupyter_server_ydoc/_version.py +1 -0
  3. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/jupyter_server_ydoc/app.py +12 -1
  4. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/jupyter_server_ydoc/loaders.py +46 -4
  5. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/jupyter_server_ydoc/stores.py +5 -1
  6. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/jupyter_server_ydoc/test_utils.py +3 -0
  7. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/tests/test_loaders.py +39 -0
  8. jupyter_server_ydoc-2.1.2/jupyter_server_ydoc/_version.py +0 -1
  9. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/.gitignore +0 -0
  10. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/LICENSE +0 -0
  11. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/README.md +0 -0
  12. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/jupyter-config/jupyter_server_ydoc.json +0 -0
  13. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/jupyter_server_ydoc/__init__.py +0 -0
  14. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/jupyter_server_ydoc/events/awareness.yaml +0 -0
  15. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/jupyter_server_ydoc/events/fork.yaml +0 -0
  16. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/jupyter_server_ydoc/events/session.yaml +0 -0
  17. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/jupyter_server_ydoc/handlers.py +0 -0
  18. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/jupyter_server_ydoc/pytest_plugin.py +0 -0
  19. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/jupyter_server_ydoc/rooms.py +0 -0
  20. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/jupyter_server_ydoc/utils.py +0 -0
  21. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/jupyter_server_ydoc/websocketserver.py +0 -0
  22. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/pyproject.toml +0 -0
  23. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/setup.py +0 -0
  24. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/tests/__init__.py +0 -0
  25. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/tests/conftest.py +0 -0
  26. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/tests/test_app.py +0 -0
  27. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/tests/test_documents.py +0 -0
  28. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/tests/test_handlers.py +0 -0
  29. {jupyter_server_ydoc-2.1.2 → jupyter_server_ydoc-2.2.0}/tests/test_rooms.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jupyter-server-ydoc
3
- Version: 2.1.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
@@ -0,0 +1 @@
1
+ __version__ = "2.2.0"
@@ -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, self.log, self.file_poll_interval
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(
@@ -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 consecutive_error_logs < max_consecutive_logs:
219
- self._log.error(f"Error watching file: {self.path}\n{e!r}", exc_info=e)
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 suppressing further logs.",
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
 
@@ -7,7 +7,11 @@ from traitlets import Int, Unicode
7
7
  from traitlets.config import LoggingConfigurable
8
8
 
9
9
 
10
- class TempFileYStore(_TempFileYStore):
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
 
@@ -43,6 +43,45 @@ async def test_FileLoader_with_watcher():
43
43
  await loader.clean()
44
44
 
45
45
 
46
+ async def test_FileLoader_with_watcher_errors(caplog):
47
+ id = "file-4567"
48
+ path = "myfile.txt"
49
+ paths = {}
50
+ paths[id] = path
51
+
52
+ cm = FakeContentsManager({"last_modified": datetime.now(timezone.utc)})
53
+
54
+ loader = FileLoader(
55
+ id,
56
+ FakeFileIDManager(paths),
57
+ cm,
58
+ poll_interval=0.1,
59
+ max_consecutive_logs=2,
60
+ stop_poll_on_errors_after=1,
61
+ )
62
+ await loader.load_content("text", "file")
63
+
64
+ try:
65
+ cm.model = {}
66
+ await asyncio.sleep(0.5)
67
+ logs = [r.getMessage() for r in caplog.records]
68
+ assert logs == [
69
+ "Error watching file myfile.txt: HTTP 404: Not Found (File not found: myfile.txt)",
70
+ "Error watching file myfile.txt: HTTP 404: Not Found (File not found: myfile.txt)",
71
+ "Too many errors while watching myfile.txt - suppressing further logs.",
72
+ ]
73
+
74
+ await asyncio.sleep(1)
75
+ logs = [r.getMessage() for r in caplog.records]
76
+ assert len(logs) == 4
77
+ assert (
78
+ logs[-1]
79
+ == "Stopping watching file due to consecutive errors over 1 seconds: myfile.txt"
80
+ )
81
+ finally:
82
+ await loader.clean()
83
+
84
+
46
85
  async def test_FileLoader_without_watcher():
47
86
  id = "file-4567"
48
87
  path = "myfile.txt"
@@ -1 +0,0 @@
1
- __version__ = "2.1.2"