fps_file_id 0.1.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.
- fps_file_id-0.1.0/.gitignore +349 -0
- fps_file_id-0.1.0/COPYING.md +59 -0
- fps_file_id-0.1.0/PKG-INFO +19 -0
- fps_file_id-0.1.0/README.md +3 -0
- fps_file_id-0.1.0/fps_file_id/__init__.py +6 -0
- fps_file_id-0.1.0/fps_file_id/file_id.py +229 -0
- fps_file_id-0.1.0/fps_file_id/main.py +16 -0
- fps_file_id-0.1.0/fps_file_id/py.typed +0 -0
- fps_file_id-0.1.0/pyproject.toml +33 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
|
|
2
|
+
# Created by https://www.toptal.com/developers/gitignore/api/git,linux,macos,python,windows,pycharm,jupyternotebook,vscode
|
|
3
|
+
# Edit at https://www.toptal.com/developers/gitignore?templates=git,linux,macos,python,windows,pycharm,jupyternotebook,vscode
|
|
4
|
+
|
|
5
|
+
.fps_cli_args.toml
|
|
6
|
+
|
|
7
|
+
### Git ###
|
|
8
|
+
# Created by git for backups. To disable backups in Git:
|
|
9
|
+
# $ git config --global mergetool.keepBackup false
|
|
10
|
+
*.orig
|
|
11
|
+
|
|
12
|
+
# Created by git when using merge tools for conflicts
|
|
13
|
+
*.BACKUP.*
|
|
14
|
+
*.BASE.*
|
|
15
|
+
*.LOCAL.*
|
|
16
|
+
*.REMOTE.*
|
|
17
|
+
*_BACKUP_*.txt
|
|
18
|
+
*_BASE_*.txt
|
|
19
|
+
*_LOCAL_*.txt
|
|
20
|
+
*_REMOTE_*.txt
|
|
21
|
+
|
|
22
|
+
#!! ERROR: jupyternotebook is undefined. Use list command to see defined gitignore types !!#
|
|
23
|
+
|
|
24
|
+
### Linux ###
|
|
25
|
+
*~
|
|
26
|
+
|
|
27
|
+
# temporary files which can be created if a process still has a handle open of a deleted file
|
|
28
|
+
.fuse_hidden*
|
|
29
|
+
|
|
30
|
+
# KDE directory preferences
|
|
31
|
+
.directory
|
|
32
|
+
|
|
33
|
+
# Linux trash folder which might appear on any partition or disk
|
|
34
|
+
.Trash-*
|
|
35
|
+
|
|
36
|
+
# .nfs files are created when an open file is removed but is still being accessed
|
|
37
|
+
.nfs*
|
|
38
|
+
|
|
39
|
+
### macOS ###
|
|
40
|
+
# General
|
|
41
|
+
.DS_Store
|
|
42
|
+
.AppleDouble
|
|
43
|
+
.LSOverride
|
|
44
|
+
|
|
45
|
+
# Icon must end with two \r
|
|
46
|
+
Icon
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Thumbnails
|
|
50
|
+
._*
|
|
51
|
+
|
|
52
|
+
# Files that might appear in the root of a volume
|
|
53
|
+
.DocumentRevisions-V100
|
|
54
|
+
.fseventsd
|
|
55
|
+
.Spotlight-V100
|
|
56
|
+
.TemporaryItems
|
|
57
|
+
.Trashes
|
|
58
|
+
.VolumeIcon.icns
|
|
59
|
+
.com.apple.timemachine.donotpresent
|
|
60
|
+
|
|
61
|
+
# Directories potentially created on remote AFP share
|
|
62
|
+
.AppleDB
|
|
63
|
+
.AppleDesktop
|
|
64
|
+
Network Trash Folder
|
|
65
|
+
Temporary Items
|
|
66
|
+
.apdisk
|
|
67
|
+
|
|
68
|
+
### PyCharm ###
|
|
69
|
+
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
|
70
|
+
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
|
71
|
+
|
|
72
|
+
# User-specific stuff
|
|
73
|
+
.idea/**/workspace.xml
|
|
74
|
+
.idea/**/tasks.xml
|
|
75
|
+
.idea/**/usage.statistics.xml
|
|
76
|
+
.idea/**/dictionaries
|
|
77
|
+
.idea/**/shelf
|
|
78
|
+
|
|
79
|
+
# AWS User-specific
|
|
80
|
+
.idea/**/aws.xml
|
|
81
|
+
|
|
82
|
+
# Generated files
|
|
83
|
+
.idea/**/contentModel.xml
|
|
84
|
+
|
|
85
|
+
# Sensitive or high-churn files
|
|
86
|
+
.idea/**/dataSources/
|
|
87
|
+
.idea/**/dataSources.ids
|
|
88
|
+
.idea/**/dataSources.local.xml
|
|
89
|
+
.idea/**/sqlDataSources.xml
|
|
90
|
+
.idea/**/dynamic.xml
|
|
91
|
+
.idea/**/uiDesigner.xml
|
|
92
|
+
.idea/**/dbnavigator.xml
|
|
93
|
+
|
|
94
|
+
# Gradle
|
|
95
|
+
.idea/**/gradle.xml
|
|
96
|
+
.idea/**/libraries
|
|
97
|
+
|
|
98
|
+
# Gradle and Maven with auto-import
|
|
99
|
+
# When using Gradle or Maven with auto-import, you should exclude module files,
|
|
100
|
+
# since they will be recreated, and may cause churn. Uncomment if using
|
|
101
|
+
# auto-import.
|
|
102
|
+
# .idea/artifacts
|
|
103
|
+
# .idea/compiler.xml
|
|
104
|
+
# .idea/jarRepositories.xml
|
|
105
|
+
# .idea/modules.xml
|
|
106
|
+
# .idea/*.iml
|
|
107
|
+
# .idea/modules
|
|
108
|
+
# *.iml
|
|
109
|
+
# *.ipr
|
|
110
|
+
|
|
111
|
+
# CMake
|
|
112
|
+
cmake-build-*/
|
|
113
|
+
|
|
114
|
+
# Mongo Explorer plugin
|
|
115
|
+
.idea/**/mongoSettings.xml
|
|
116
|
+
|
|
117
|
+
# File-based project format
|
|
118
|
+
*.iws
|
|
119
|
+
|
|
120
|
+
# IntelliJ
|
|
121
|
+
out/
|
|
122
|
+
|
|
123
|
+
# mpeltonen/sbt-idea plugin
|
|
124
|
+
.idea_modules/
|
|
125
|
+
|
|
126
|
+
# JIRA plugin
|
|
127
|
+
atlassian-ide-plugin.xml
|
|
128
|
+
|
|
129
|
+
# Cursive Clojure plugin
|
|
130
|
+
.idea/replstate.xml
|
|
131
|
+
|
|
132
|
+
# Crashlytics plugin (for Android Studio and IntelliJ)
|
|
133
|
+
com_crashlytics_export_strings.xml
|
|
134
|
+
crashlytics.properties
|
|
135
|
+
crashlytics-build.properties
|
|
136
|
+
fabric.properties
|
|
137
|
+
|
|
138
|
+
# Editor-based Rest Client
|
|
139
|
+
.idea/httpRequests
|
|
140
|
+
|
|
141
|
+
# Android studio 3.1+ serialized cache file
|
|
142
|
+
.idea/caches/build_file_checksums.ser
|
|
143
|
+
|
|
144
|
+
### PyCharm Patch ###
|
|
145
|
+
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
|
146
|
+
|
|
147
|
+
# *.iml
|
|
148
|
+
# modules.xml
|
|
149
|
+
# .idea/misc.xml
|
|
150
|
+
# *.ipr
|
|
151
|
+
|
|
152
|
+
# Sonarlint plugin
|
|
153
|
+
# https://plugins.jetbrains.com/plugin/7973-sonarlint
|
|
154
|
+
.idea/**/sonarlint/
|
|
155
|
+
|
|
156
|
+
# SonarQube Plugin
|
|
157
|
+
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
|
|
158
|
+
.idea/**/sonarIssues.xml
|
|
159
|
+
|
|
160
|
+
# Markdown Navigator plugin
|
|
161
|
+
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
|
|
162
|
+
.idea/**/markdown-navigator.xml
|
|
163
|
+
.idea/**/markdown-navigator-enh.xml
|
|
164
|
+
.idea/**/markdown-navigator/
|
|
165
|
+
|
|
166
|
+
# Cache file creation bug
|
|
167
|
+
# See https://youtrack.jetbrains.com/issue/JBR-2257
|
|
168
|
+
.idea/$CACHE_FILE$
|
|
169
|
+
|
|
170
|
+
# CodeStream plugin
|
|
171
|
+
# https://plugins.jetbrains.com/plugin/12206-codestream
|
|
172
|
+
.idea/codestream.xml
|
|
173
|
+
|
|
174
|
+
### Python ###
|
|
175
|
+
# Byte-compiled / optimized / DLL files
|
|
176
|
+
__pycache__/
|
|
177
|
+
*.py[cod]
|
|
178
|
+
*$py.class
|
|
179
|
+
|
|
180
|
+
# C extensions
|
|
181
|
+
*.so
|
|
182
|
+
|
|
183
|
+
# Distribution / packaging
|
|
184
|
+
.Python
|
|
185
|
+
build/
|
|
186
|
+
develop-eggs/
|
|
187
|
+
dist/
|
|
188
|
+
downloads/
|
|
189
|
+
eggs/
|
|
190
|
+
.eggs/
|
|
191
|
+
lib/
|
|
192
|
+
lib64/
|
|
193
|
+
parts/
|
|
194
|
+
sdist/
|
|
195
|
+
var/
|
|
196
|
+
wheels/
|
|
197
|
+
share/python-wheels/
|
|
198
|
+
*.egg-info/
|
|
199
|
+
.installed.cfg
|
|
200
|
+
*.egg
|
|
201
|
+
MANIFEST
|
|
202
|
+
|
|
203
|
+
# PyInstaller
|
|
204
|
+
# Usually these files are written by a python script from a template
|
|
205
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
206
|
+
*.manifest
|
|
207
|
+
*.spec
|
|
208
|
+
|
|
209
|
+
# Installer logs
|
|
210
|
+
pip-log.txt
|
|
211
|
+
pip-delete-this-directory.txt
|
|
212
|
+
|
|
213
|
+
# Unit test / coverage reports
|
|
214
|
+
htmlcov/
|
|
215
|
+
.tox/
|
|
216
|
+
.nox/
|
|
217
|
+
.coverage
|
|
218
|
+
.coverage.*
|
|
219
|
+
.cache
|
|
220
|
+
nosetests.xml
|
|
221
|
+
coverage.xml
|
|
222
|
+
*.cover
|
|
223
|
+
*.py,cover
|
|
224
|
+
.hypothesis/
|
|
225
|
+
.pytest_cache/
|
|
226
|
+
cover/
|
|
227
|
+
|
|
228
|
+
# Translations
|
|
229
|
+
*.mo
|
|
230
|
+
*.pot
|
|
231
|
+
|
|
232
|
+
# Django stuff:
|
|
233
|
+
*.log
|
|
234
|
+
local_settings.py
|
|
235
|
+
db.sqlite3
|
|
236
|
+
db.sqlite3-journal
|
|
237
|
+
|
|
238
|
+
# Flask stuff:
|
|
239
|
+
instance/
|
|
240
|
+
.webassets-cache
|
|
241
|
+
|
|
242
|
+
# Scrapy stuff:
|
|
243
|
+
.scrapy
|
|
244
|
+
|
|
245
|
+
# Sphinx documentation
|
|
246
|
+
docs/_build/
|
|
247
|
+
|
|
248
|
+
# PyBuilder
|
|
249
|
+
.pybuilder/
|
|
250
|
+
target/
|
|
251
|
+
|
|
252
|
+
# Jupyter Notebook
|
|
253
|
+
.ipynb_checkpoints
|
|
254
|
+
|
|
255
|
+
# IPython
|
|
256
|
+
profile_default/
|
|
257
|
+
ipython_config.py
|
|
258
|
+
|
|
259
|
+
# pyenv
|
|
260
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
261
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
262
|
+
# .python-version
|
|
263
|
+
|
|
264
|
+
# pipenv
|
|
265
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
266
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
267
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
268
|
+
# install all needed dependencies.
|
|
269
|
+
#Pipfile.lock
|
|
270
|
+
|
|
271
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
272
|
+
__pypackages__/
|
|
273
|
+
|
|
274
|
+
# Celery stuff
|
|
275
|
+
celerybeat-schedule
|
|
276
|
+
celerybeat.pid
|
|
277
|
+
|
|
278
|
+
# SageMath parsed files
|
|
279
|
+
*.sage.py
|
|
280
|
+
|
|
281
|
+
# Environments
|
|
282
|
+
.env
|
|
283
|
+
.venv
|
|
284
|
+
env/
|
|
285
|
+
venv/
|
|
286
|
+
ENV/
|
|
287
|
+
env.bak/
|
|
288
|
+
venv.bak/
|
|
289
|
+
|
|
290
|
+
# Spyder project settings
|
|
291
|
+
.spyderproject
|
|
292
|
+
.spyproject
|
|
293
|
+
|
|
294
|
+
# Rope project settings
|
|
295
|
+
.ropeproject
|
|
296
|
+
|
|
297
|
+
# mkdocs documentation
|
|
298
|
+
/site
|
|
299
|
+
|
|
300
|
+
# mypy
|
|
301
|
+
.mypy_cache/
|
|
302
|
+
.dmypy.json
|
|
303
|
+
dmypy.json
|
|
304
|
+
|
|
305
|
+
# Pyre type checker
|
|
306
|
+
.pyre/
|
|
307
|
+
|
|
308
|
+
# pytype static type analyzer
|
|
309
|
+
.pytype/
|
|
310
|
+
|
|
311
|
+
# Cython debug symbols
|
|
312
|
+
cython_debug/
|
|
313
|
+
|
|
314
|
+
#!! ERROR: vscode is undefined. Use list command to see defined gitignore types !!#
|
|
315
|
+
|
|
316
|
+
### Windows ###
|
|
317
|
+
# Windows thumbnail cache files
|
|
318
|
+
Thumbs.db
|
|
319
|
+
Thumbs.db:encryptable
|
|
320
|
+
ehthumbs.db
|
|
321
|
+
ehthumbs_vista.db
|
|
322
|
+
|
|
323
|
+
# Dump file
|
|
324
|
+
*.stackdump
|
|
325
|
+
|
|
326
|
+
# Folder config file
|
|
327
|
+
[Dd]esktop.ini
|
|
328
|
+
|
|
329
|
+
# Recycle Bin used on file shares
|
|
330
|
+
$RECYCLE.BIN/
|
|
331
|
+
|
|
332
|
+
# Windows Installer files
|
|
333
|
+
*.cab
|
|
334
|
+
*.msi
|
|
335
|
+
*.msix
|
|
336
|
+
*.msm
|
|
337
|
+
*.msp
|
|
338
|
+
|
|
339
|
+
# Windows shortcuts
|
|
340
|
+
*.lnk
|
|
341
|
+
|
|
342
|
+
# End of https://www.toptal.com/developers/gitignore/api/git,linux,macos,python,windows,pycharm,jupyternotebook,vscode
|
|
343
|
+
|
|
344
|
+
.jupyter_ystore.db
|
|
345
|
+
.jupyter_ystore.db-journal
|
|
346
|
+
fps_cli_args.toml
|
|
347
|
+
|
|
348
|
+
# pixi environments
|
|
349
|
+
.pixi
|
|
@@ -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.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fps_file_id
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An FPS plugin for the file ID API
|
|
5
|
+
Project-URL: Homepage, https://jupyter.org
|
|
6
|
+
Author-email: Jupyter Development Team <jupyter@googlegroups.com>
|
|
7
|
+
License: BSD 3-Clause License
|
|
8
|
+
License-File: COPYING.md
|
|
9
|
+
Keywords: fastapi,jupyter,plugins,server
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: anyio<5,>=3.6.2
|
|
12
|
+
Requires-Dist: jupyverse-api<0.10.0,>=0.9.0
|
|
13
|
+
Requires-Dist: sqlite-anyio<0.3.0,>=0.2.0
|
|
14
|
+
Requires-Dist: watchfiles<2,>=1.0.4
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# fps-contents
|
|
18
|
+
|
|
19
|
+
An FPS plugin for the file ID API.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sqlite3
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
from anyio import Event, Lock, Path
|
|
9
|
+
from sqlite_anyio import connect
|
|
10
|
+
from watchfiles import Change, awatch
|
|
11
|
+
|
|
12
|
+
from jupyverse_api.file_id import FileId
|
|
13
|
+
|
|
14
|
+
logger = structlog.get_logger()
|
|
15
|
+
watchfiles_logger = logging.getLogger("watchfiles")
|
|
16
|
+
watchfiles_logger.setLevel(logging.WARNING)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Watcher:
|
|
20
|
+
def __init__(self, path: str) -> None:
|
|
21
|
+
self.path = path
|
|
22
|
+
self._event = Event()
|
|
23
|
+
|
|
24
|
+
def __aiter__(self):
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
async def __anext__(self):
|
|
28
|
+
await self._event.wait()
|
|
29
|
+
self._event = Event()
|
|
30
|
+
return self._change
|
|
31
|
+
|
|
32
|
+
def notify(self, change):
|
|
33
|
+
self._change = change
|
|
34
|
+
self._event.set()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _FileId(FileId):
|
|
38
|
+
db_path: str
|
|
39
|
+
initialized: Event
|
|
40
|
+
watchers: dict[str, list[Watcher]]
|
|
41
|
+
lock: Lock
|
|
42
|
+
|
|
43
|
+
def __init__(self, db_path: str = ".fileid.db"):
|
|
44
|
+
self.db_path = db_path
|
|
45
|
+
self.initialized = Event()
|
|
46
|
+
self.watchers = {}
|
|
47
|
+
self.stop_event = Event()
|
|
48
|
+
self.lock = Lock()
|
|
49
|
+
|
|
50
|
+
async def start(self) -> None:
|
|
51
|
+
self._db = await connect(self.db_path)
|
|
52
|
+
try:
|
|
53
|
+
await self.watch_files()
|
|
54
|
+
except sqlite3.ProgrammingError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
async def stop(self) -> None:
|
|
58
|
+
await self._db.close()
|
|
59
|
+
self.stop_event.set()
|
|
60
|
+
|
|
61
|
+
async def get_id(self, path: str) -> str | None:
|
|
62
|
+
await self.initialized.wait()
|
|
63
|
+
async with self.lock:
|
|
64
|
+
cursor = await self._db.cursor()
|
|
65
|
+
await cursor.execute("SELECT id FROM fileids WHERE path = ?", (path,))
|
|
66
|
+
for (idx,) in await cursor.fetchall():
|
|
67
|
+
return idx
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
async def get_path(self, idx: str) -> str | None:
|
|
71
|
+
await self.initialized.wait()
|
|
72
|
+
async with self.lock:
|
|
73
|
+
cursor = await self._db.cursor()
|
|
74
|
+
await cursor.execute("SELECT path FROM fileids WHERE id = ?", (idx,))
|
|
75
|
+
for (path,) in await cursor.fetchall():
|
|
76
|
+
return path
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
async def index(self, path: str) -> str | None:
|
|
80
|
+
await self.initialized.wait()
|
|
81
|
+
async with self.lock:
|
|
82
|
+
apath = Path(path)
|
|
83
|
+
if not await apath.exists():
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
idx = uuid4().hex
|
|
87
|
+
mtime = (await apath.stat()).st_mtime
|
|
88
|
+
cursor = await self._db.cursor()
|
|
89
|
+
await cursor.execute("INSERT INTO fileids VALUES (?, ?, ?)", (idx, path, mtime))
|
|
90
|
+
await self._db.commit()
|
|
91
|
+
return idx
|
|
92
|
+
|
|
93
|
+
async def watch_files(self):
|
|
94
|
+
async with self.lock:
|
|
95
|
+
cursor = await self._db.cursor()
|
|
96
|
+
await cursor.execute("DROP TABLE IF EXISTS fileids")
|
|
97
|
+
await cursor.execute(
|
|
98
|
+
"CREATE TABLE fileids "
|
|
99
|
+
"(id TEXT PRIMARY KEY, path TEXT NOT NULL UNIQUE, mtime REAL NOT NULL)"
|
|
100
|
+
)
|
|
101
|
+
await self._db.commit()
|
|
102
|
+
|
|
103
|
+
# index files
|
|
104
|
+
async with self.lock:
|
|
105
|
+
cursor = await self._db.cursor()
|
|
106
|
+
async for path in Path().rglob("*"):
|
|
107
|
+
idx = uuid4().hex
|
|
108
|
+
try:
|
|
109
|
+
mtime = (await path.stat()).st_mtime
|
|
110
|
+
except FileNotFoundError:
|
|
111
|
+
pass
|
|
112
|
+
else:
|
|
113
|
+
await cursor.execute(
|
|
114
|
+
"INSERT INTO fileids VALUES (?, ?, ?)", (idx, str(path), mtime)
|
|
115
|
+
)
|
|
116
|
+
await self._db.commit()
|
|
117
|
+
self.initialized.set()
|
|
118
|
+
|
|
119
|
+
async for changes in awatch(".", stop_event=self.stop_event):
|
|
120
|
+
async with self.lock:
|
|
121
|
+
deleted_paths = set()
|
|
122
|
+
added_paths = set()
|
|
123
|
+
cursor = await self._db.cursor()
|
|
124
|
+
for change, changed_path in changes:
|
|
125
|
+
# get relative path
|
|
126
|
+
changed_path = Path(changed_path).relative_to(await Path().absolute())
|
|
127
|
+
changed_path_str = str(changed_path)
|
|
128
|
+
|
|
129
|
+
if change == Change.deleted:
|
|
130
|
+
logger.debug("File was deleted", path=changed_path_str)
|
|
131
|
+
await cursor.execute(
|
|
132
|
+
"SELECT COUNT(*) FROM fileids WHERE path = ?", (changed_path_str,)
|
|
133
|
+
)
|
|
134
|
+
if not (await cursor.fetchone())[0]:
|
|
135
|
+
# path is not indexed, ignore
|
|
136
|
+
logger.debug(
|
|
137
|
+
"File is not indexed, ignoring",
|
|
138
|
+
path=changed_path_str,
|
|
139
|
+
)
|
|
140
|
+
continue
|
|
141
|
+
# path is indexed
|
|
142
|
+
await maybe_rename(
|
|
143
|
+
self._db, changed_path_str, deleted_paths, added_paths, False
|
|
144
|
+
)
|
|
145
|
+
elif change == Change.added:
|
|
146
|
+
logger.debug("File was added", path=changed_path_str)
|
|
147
|
+
await maybe_rename(
|
|
148
|
+
self._db, changed_path_str, added_paths, deleted_paths, True
|
|
149
|
+
)
|
|
150
|
+
elif change == Change.modified:
|
|
151
|
+
logger.debug("File was modified", path=changed_path_str)
|
|
152
|
+
if changed_path_str == self.db_path:
|
|
153
|
+
continue
|
|
154
|
+
await cursor.execute(
|
|
155
|
+
"SELECT COUNT(*) FROM fileids WHERE path = ?", (changed_path_str,)
|
|
156
|
+
)
|
|
157
|
+
if not (await cursor.fetchone())[0]:
|
|
158
|
+
# path is not indexed, ignore
|
|
159
|
+
logger.debug(
|
|
160
|
+
"File is not indexed, ignoring",
|
|
161
|
+
path=changed_path_str,
|
|
162
|
+
)
|
|
163
|
+
continue
|
|
164
|
+
mtime = (await changed_path.stat()).st_mtime
|
|
165
|
+
await cursor.execute(
|
|
166
|
+
"UPDATE fileids SET mtime = ? WHERE path = ?",
|
|
167
|
+
(mtime, changed_path_str),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
for path in deleted_paths - added_paths:
|
|
171
|
+
logger.debug("Unindexing file", path=path)
|
|
172
|
+
await cursor.execute("DELETE FROM fileids WHERE path = ?", (path,))
|
|
173
|
+
await self._db.commit()
|
|
174
|
+
|
|
175
|
+
for change in changes:
|
|
176
|
+
changed_path = change[1]
|
|
177
|
+
# get relative path
|
|
178
|
+
relative_changed_path = str(Path(changed_path).relative_to(await Path().absolute()))
|
|
179
|
+
relative_change = (change[0], relative_changed_path)
|
|
180
|
+
for watcher in self.watchers.get(relative_changed_path, []):
|
|
181
|
+
watcher.notify(relative_change)
|
|
182
|
+
|
|
183
|
+
def watch(self, path: str) -> Watcher:
|
|
184
|
+
watcher = Watcher(path)
|
|
185
|
+
self.watchers.setdefault(path, []).append(watcher)
|
|
186
|
+
return watcher
|
|
187
|
+
|
|
188
|
+
def unwatch(self, path: str, watcher: Watcher):
|
|
189
|
+
self.watchers[path].remove(watcher)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def get_mtime(path, db) -> float | None:
|
|
193
|
+
if db:
|
|
194
|
+
cursor = await db.cursor()
|
|
195
|
+
await cursor.execute("SELECT mtime FROM fileids WHERE path = ?", (path,))
|
|
196
|
+
for (mtime,) in await cursor.fetchall():
|
|
197
|
+
return mtime
|
|
198
|
+
# deleted file is not in database, shouldn't happen
|
|
199
|
+
return None
|
|
200
|
+
try:
|
|
201
|
+
mtime = (await Path(path).stat()).st_mtime
|
|
202
|
+
except FileNotFoundError:
|
|
203
|
+
return None
|
|
204
|
+
return mtime
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def maybe_rename(
|
|
208
|
+
db, changed_path: str, changed_paths: set[str], other_paths: set[str], is_added_path: bool
|
|
209
|
+
) -> None:
|
|
210
|
+
# check if the same file was added/deleted, this would be a rename
|
|
211
|
+
db_or_fs1, db_or_fs2 = db, None
|
|
212
|
+
if is_added_path:
|
|
213
|
+
db_or_fs1, db_or_fs2 = db_or_fs2, db_or_fs1
|
|
214
|
+
mtime1 = await get_mtime(changed_path, db_or_fs1)
|
|
215
|
+
if mtime1 is None:
|
|
216
|
+
return
|
|
217
|
+
for other_path in other_paths:
|
|
218
|
+
mtime2 = await get_mtime(other_path, db_or_fs2)
|
|
219
|
+
if mtime1 == mtime2:
|
|
220
|
+
# same files, according to modification times
|
|
221
|
+
path1, path2 = changed_path, other_path
|
|
222
|
+
if is_added_path:
|
|
223
|
+
path1, path2 = path2, path1
|
|
224
|
+
logger.debug("File was renamed", from_path=path1, to_path=path2)
|
|
225
|
+
cursor = await db.cursor()
|
|
226
|
+
await cursor.execute("UPDATE fileids SET path = ? WHERE path = ?", (path2, path1))
|
|
227
|
+
other_paths.remove(other_path)
|
|
228
|
+
return
|
|
229
|
+
changed_paths.add(changed_path)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from anyio import create_task_group
|
|
2
|
+
from fps import Module
|
|
3
|
+
|
|
4
|
+
from jupyverse_api.file_id import FileId
|
|
5
|
+
|
|
6
|
+
from .file_id import _FileId
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileIdModule(Module):
|
|
10
|
+
async def prepare(self) -> None:
|
|
11
|
+
self.file_id = _FileId()
|
|
12
|
+
|
|
13
|
+
async with create_task_group() as tg:
|
|
14
|
+
tg.start_soon(self.file_id.start)
|
|
15
|
+
self.put(self.file_id, FileId, teardown_callback=self.file_id.stop)
|
|
16
|
+
self.done()
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [ "hatchling",]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fps_file_id"
|
|
7
|
+
description = "An FPS plugin for the file ID API"
|
|
8
|
+
keywords = ["jupyter", "server", "fastapi", "plugins"]
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"watchfiles >=1.0.4,<2",
|
|
12
|
+
"sqlite-anyio >=0.2.0,<0.3.0",
|
|
13
|
+
"anyio>=3.6.2,<5",
|
|
14
|
+
"jupyverse-api >=0.9.0,<0.10.0",
|
|
15
|
+
]
|
|
16
|
+
version = "0.1.0"
|
|
17
|
+
[[project.authors]]
|
|
18
|
+
name = "Jupyter Development Team"
|
|
19
|
+
email = "jupyter@googlegroups.com"
|
|
20
|
+
|
|
21
|
+
[project.readme]
|
|
22
|
+
file = "README.md"
|
|
23
|
+
content-type = "text/markdown"
|
|
24
|
+
|
|
25
|
+
[project.license]
|
|
26
|
+
text = "BSD 3-Clause License"
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://jupyter.org"
|
|
30
|
+
|
|
31
|
+
[project.entry-points]
|
|
32
|
+
"fps.modules" = {file_id = "fps_file_id.main:FileIdModule"}
|
|
33
|
+
"jupyverse.modules" = {file_id = "fps_file_id.main:FileIdModule"}
|