Skip to content

Commit 068c71d

Browse files
Add FileWatcher API (#529)
1 parent af6b1f3 commit 068c71d

File tree

30 files changed

+335
-27
lines changed

30 files changed

+335
-27
lines changed

docs/plugins/contents.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
`fps-contents` implements the `contents` API, i.e. everything related to reading/writing files, for the local file system. It also provides notifications for file changes using [watchfiles](https://github.com/samuelcolvin/watchfiles).
1+
`fps-contents` implements the `contents` API, i.e. everything related to reading/writing files, for the local file system.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from __future__ import annotations
2+
3+
from abc import ABC, abstractmethod
4+
from collections.abc import AsyncGenerator
5+
from enum import IntEnum
6+
from pathlib import Path
7+
8+
from anyio import Event
9+
10+
11+
class Change(IntEnum):
12+
added = 1
13+
modified = 2
14+
deleted = 3
15+
16+
17+
FileChange = tuple[Change, str]
18+
19+
20+
class FileWatcher(ABC):
21+
@abstractmethod
22+
async def watch(
23+
self,
24+
path: Path | str,
25+
stop_event: Event | None = None,
26+
) -> AsyncGenerator[set[FileChange], None]: ...

plugins/file_id/fps_file_id/file_id.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
from __future__ import annotations
22

3-
import logging
43
import sqlite3
54
from uuid import uuid4
65

76
import structlog
87
from anyio import Event, Lock, Path
98
from sqlite_anyio import connect
10-
from watchfiles import Change, awatch
119

1210
from jupyverse_api.file_id import FileId
11+
from jupyverse_api.file_watcher import Change, FileWatcher
1312

1413
logger = structlog.get_logger()
15-
watchfiles_logger = logging.getLogger("watchfiles")
16-
watchfiles_logger.setLevel(logging.WARNING)
1714

1815

1916
class Watcher:
@@ -40,7 +37,8 @@ class _FileId(FileId):
4037
watchers: dict[str, list[Watcher]]
4138
lock: Lock
4239

43-
def __init__(self, db_path: str = ".fileid.db"):
40+
def __init__(self, file_watcher: FileWatcher, db_path: str = ".fileid.db"):
41+
self.file_watcher = file_watcher
4442
self.db_path = db_path
4543
self.initialized = Event()
4644
self.watchers = {}
@@ -116,14 +114,15 @@ async def watch_files(self):
116114
await self._db.commit()
117115
self.initialized.set()
118116

119-
async for changes in awatch(".", stop_event=self.stop_event):
117+
here = await Path().absolute()
118+
async for changes in self.file_watcher.watch(here, stop_event=self.stop_event):
120119
async with self.lock:
121120
deleted_paths = set()
122121
added_paths = set()
123122
cursor = await self._db.cursor()
124123
for change, changed_path in changes:
125124
# get relative path
126-
changed_path = Path(changed_path).relative_to(await Path().absolute())
125+
changed_path = Path(changed_path).relative_to(here)
127126
changed_path_str = str(changed_path)
128127

129128
if change == Change.deleted:

plugins/file_id/fps_file_id/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
from fps import Module
33

44
from jupyverse_api.file_id import FileId
5+
from jupyverse_api.file_watcher import FileWatcher
56

67
from .file_id import _FileId
78

89

910
class FileIdModule(Module):
1011
async def prepare(self) -> None:
11-
self.file_id = _FileId()
12+
file_watcher = await self.get(FileWatcher) # type: ignore[type-abstract]
13+
self.file_id = _FileId(file_watcher)
1214

1315
async with create_task_group() as tg:
1416
tg.start_soon(self.file_id.start)

plugins/file_id/pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ description = "An FPS plugin for the file ID API"
99
keywords = ["jupyter", "server", "fastapi", "plugins"]
1010
requires-python = ">=3.9"
1111
dependencies = [
12-
"watchfiles >=1.0.4,<2",
1312
"sqlite-anyio >=0.2.0,<0.3.0",
1413
"anyio>=3.6.2,<5",
1514
"jupyverse-api >=0.11.0,<0.12.0",

plugins/file_watcher/COPYING.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Licensing terms
2+
3+
This project is licensed under the terms of the Modified BSD License
4+
(also known as New or Revised or 3-Clause BSD), as follows:
5+
6+
- Copyright (c) 2025-, Jupyter Development Team
7+
8+
All rights reserved.
9+
10+
Redistribution and use in source and binary forms, with or without
11+
modification, are permitted provided that the following conditions are met:
12+
13+
Redistributions of source code must retain the above copyright notice, this
14+
list of conditions and the following disclaimer.
15+
16+
Redistributions in binary form must reproduce the above copyright notice, this
17+
list of conditions and the following disclaimer in the documentation and/or
18+
other materials provided with the distribution.
19+
20+
Neither the name of the Jupyter Development Team nor the names of its
21+
contributors may be used to endorse or promote products derived from this
22+
software without specific prior written permission.
23+
24+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
28+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34+
35+
## About the Jupyter Development Team
36+
37+
The Jupyter Development Team is the set of all contributors to the Jupyter project.
38+
This includes all of the Jupyter subprojects.
39+
40+
The core team that coordinates development on GitHub can be found here:
41+
https://github.com/jupyter/.
42+
43+
## Our Copyright Policy
44+
45+
Jupyter uses a shared copyright model. Each contributor maintains copyright
46+
over their contributions to Jupyter. But, it is important to note that these
47+
contributions are typically only changes to the repositories. Thus, the Jupyter
48+
source code, in its entirety is not the copyright of any single person or
49+
institution. Instead, it is the collective copyright of the entire Jupyter
50+
Development Team. If individual contributors want to maintain a record of what
51+
changes/contributions they have specific copyright on, they should indicate
52+
their copyright in the commit message of the change, when they commit the
53+
change to one of the Jupyter repositories.
54+
55+
With this in mind, the following banner should be used in any source code file
56+
to indicate the copyright and license terms:
57+
58+
# Copyright (c) Jupyter Development Team.
59+
# Distributed under the terms of the Modified BSD License.

plugins/file_watcher/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# fps-file-watcher
2+
3+
An FPS plugin for the file watcher API.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import importlib.metadata
2+
3+
try:
4+
__version__ = importlib.metadata.version("fps_file_watcher")
5+
except importlib.metadata.PackageNotFoundError:
6+
__version__ = "unknown"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from collections.abc import AsyncGenerator
5+
from pathlib import Path
6+
7+
import structlog
8+
from anyio import Event
9+
from watchfiles import awatch
10+
11+
from jupyverse_api.file_watcher import FileChange, FileWatcher
12+
13+
logger = structlog.get_logger()
14+
watchfiles_logger = logging.getLogger("watchfiles")
15+
watchfiles_logger.setLevel(logging.WARNING)
16+
17+
18+
class _FileWatcher(FileWatcher):
19+
async def watch( # type: ignore[override]
20+
self,
21+
path: Path | str,
22+
stop_event: Event | None = None,
23+
) -> AsyncGenerator[set[FileChange], None]:
24+
async for changes in awatch(path, stop_event=stop_event):
25+
yield changes # type: ignore[misc]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from fps import Module
2+
3+
from jupyverse_api.file_watcher import FileWatcher
4+
5+
from .file_watcher import _FileWatcher
6+
7+
8+
class FileWatcherModule(Module):
9+
async def prepare(self) -> None:
10+
file_watcher = _FileWatcher()
11+
self.put(file_watcher, FileWatcher)

0 commit comments

Comments
 (0)