gwdatafind-server 2.1.0__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.
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/CHANGELOG.md +9 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/PKG-INFO +1 -1
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/docs/changelog.md +9 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/_version.py +3 -3
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/source/pelican.py +125 -42
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server.egg-info/PKG-INFO +1 -1
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/pyproject.toml +5 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/test_source_pelican.py +145 -13
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/.gitignore +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/.gitlab-ci.yml +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/.markdownlint.yml +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/Dockerfile +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/LICENSE +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/MANIFEST.in +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/README.md +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/config/docker-compose.yml +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/config/gunicorn.conf.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/config/gwdatafind-server.service +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/config/gwdatafind-server.toml +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/diskcache/Makefile +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/diskcache/create_testdata.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/diskcache/diskcache.rsc +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/docker-compose.yml +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/docs/api/index.md +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/docs/api/ldr.md +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/docs/api/v1.md +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/docs/configuration.md +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/docs/deploy/examples/auth.md +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/docs/deploy/examples/diskcache.md +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/docs/deploy/examples/gwosc.md +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/docs/deploy/examples/index.md +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/docs/deploy/index.md +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/docs/index.md +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/docs/overview.md +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/docs/requirements.txt +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/examples/auth/docker-compose.yml +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/examples/auth/gwdatafind-server.toml +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/examples/diskcache/docker-compose.yml +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/examples/diskcache/gwdatafind-server.toml +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/__init__.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/api/__init__.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/api/base.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/api/ldr.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/api/utils.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/api/v1.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/app.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/auth/__init__.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/auth/core.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/auth/scitoken.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/cache.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/config.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/credential/__init__.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/credential/credential.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/credential/scitoken.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/gwdatafind_server.wsgi +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/source/__init__.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/source/diskcache.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/source/source.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/urls.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server.egg-info/SOURCES.txt +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server.egg-info/dependency_links.txt +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server.egg-info/requires.txt +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server.egg-info/top_level.txt +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/mkdocs.yml +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/setup.cfg +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/__init__.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/cache-0x00ff.dat +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/cache-0x0101.dat +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/conftest.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/grid-mapfile +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/gwdatafind-server.ini +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/gwdatafind-server.toml +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/test_api_v1.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/test_app.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/test_auth_override.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/test_auth_scitoken.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/test_cache.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/test_config.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/test_credential.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/test_credential_scitoken.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/test_source.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/test_source_diskcache.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/test_urls.py +0 -0
- {gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/tests/utils.py +0 -0
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0 - 2025-04-09 { #2.2.0 }
|
|
4
|
+
|
|
5
|
+
- Fix directory recursion with Pelican memory cache
|
|
6
|
+
([!129](https://git.ligo.org/computing/gwdatafind/server/-/merge_requests/129))
|
|
7
|
+
- Add Pelican cache versioning and compatibility checks
|
|
8
|
+
([!130](https://git.ligo.org/computing/gwdatafind/server/-/merge_requests/130))
|
|
9
|
+
|
|
10
|
+
[Full details](https://git.ligo.org/computing/gwdatafind/server/-/releases/2.2.0)
|
|
11
|
+
|
|
3
12
|
## 2.1.0 - 2025-03-26 { #2.1.0 }
|
|
4
13
|
|
|
5
14
|
- Add Pelican database cache file
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0 - 2025-04-09 { #2.2.0 }
|
|
4
|
+
|
|
5
|
+
- Fix directory recursion with Pelican memory cache
|
|
6
|
+
([!129](https://git.ligo.org/computing/gwdatafind/server/-/merge_requests/129))
|
|
7
|
+
- Add Pelican cache versioning and compatibility checks
|
|
8
|
+
([!130](https://git.ligo.org/computing/gwdatafind/server/-/merge_requests/130))
|
|
9
|
+
|
|
10
|
+
[Full details](https://git.ligo.org/computing/gwdatafind/server/-/releases/2.2.0)
|
|
11
|
+
|
|
3
12
|
## 2.1.0 - 2025-03-26 { #2.1.0 }
|
|
4
13
|
|
|
5
14
|
- Add Pelican database cache file
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 2, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gc287e7f56'
|
|
@@ -22,6 +22,7 @@ from typing import (
|
|
|
22
22
|
cast,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
+
from .. import __version__ as GWDATAFIND_SERVER_VERSION
|
|
25
26
|
from ..cache import CacheEntry
|
|
26
27
|
from .source import (
|
|
27
28
|
Source,
|
|
@@ -67,8 +68,17 @@ class PelicanCache:
|
|
|
67
68
|
back to SQLite at the end of each update cycle.
|
|
68
69
|
"""
|
|
69
70
|
|
|
71
|
+
#: Version of the Pelican cache DB schema
|
|
70
72
|
SCHEMA_VERSION = 1
|
|
71
73
|
|
|
74
|
+
#: Token to identify compatible cache files
|
|
75
|
+
#:
|
|
76
|
+
#: This should be incremented if changes are made to the way the cache is
|
|
77
|
+
#: constructed or used that would make old cache files incompatible,
|
|
78
|
+
#: even if the schema version is unchanged.
|
|
79
|
+
#: This allows the server to force a refresh of the cache if needed.
|
|
80
|
+
COMPATIBILITY_TOKEN = "pelican-cache-v1" # noqa: S105
|
|
81
|
+
|
|
72
82
|
def __init__(self, cache_file: str | Path):
|
|
73
83
|
"""Initialize the Pelican cache.
|
|
74
84
|
|
|
@@ -86,9 +96,19 @@ class PelicanCache:
|
|
|
86
96
|
# Create directory if it doesn't exist
|
|
87
97
|
self.cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
88
98
|
|
|
99
|
+
if self.cache_file.exists():
|
|
100
|
+
compatible, reason = self._is_existing_cache_compatible()
|
|
101
|
+
if not compatible:
|
|
102
|
+
self._invalidate_cache(reason)
|
|
103
|
+
|
|
89
104
|
with self._get_connection() as conn:
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
self._create_schema(conn)
|
|
106
|
+
self._write_metadata(conn)
|
|
107
|
+
conn.commit()
|
|
108
|
+
|
|
109
|
+
def _create_schema(self, conn: sqlite3.Connection) -> None:
|
|
110
|
+
"""Create cache tables and indexes if they don't already exist."""
|
|
111
|
+
conn.executescript("""
|
|
92
112
|
CREATE TABLE IF NOT EXISTS directories (
|
|
93
113
|
path TEXT PRIMARY KEY,
|
|
94
114
|
last_modified TEXT,
|
|
@@ -113,28 +133,72 @@ class PelicanCache:
|
|
|
113
133
|
ON urls(directory_path);
|
|
114
134
|
CREATE INDEX IF NOT EXISTS idx_directories_path
|
|
115
135
|
ON directories(path);
|
|
116
|
-
|
|
136
|
+
""")
|
|
137
|
+
|
|
138
|
+
def _read_metadata(self, conn: sqlite3.Connection) -> dict[str, str]:
|
|
139
|
+
"""Read all metadata keys from the database."""
|
|
140
|
+
cursor = conn.execute("SELECT key, value FROM cache_metadata")
|
|
141
|
+
return dict(cursor.fetchall())
|
|
142
|
+
|
|
143
|
+
def _write_metadata(self, conn: sqlite3.Connection) -> None:
|
|
144
|
+
"""Write metadata required to validate cache compatibility."""
|
|
145
|
+
conn.executemany(
|
|
146
|
+
"""
|
|
147
|
+
INSERT INTO cache_metadata (key, value)
|
|
148
|
+
VALUES (?, ?)
|
|
149
|
+
ON CONFLICT(key) DO UPDATE SET value=excluded.value
|
|
150
|
+
""",
|
|
151
|
+
[
|
|
152
|
+
("schema_version", str(self.SCHEMA_VERSION)),
|
|
153
|
+
("compatibility_token", self.COMPATIBILITY_TOKEN),
|
|
154
|
+
],
|
|
155
|
+
)
|
|
156
|
+
conn.execute(
|
|
157
|
+
"INSERT OR IGNORE INTO cache_metadata (key, value) VALUES (?, ?)",
|
|
158
|
+
("created_by", GWDATAFIND_SERVER_VERSION),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _is_existing_cache_compatible(self) -> tuple[bool, str]:
|
|
162
|
+
"""Check whether an existing cache file can be used safely."""
|
|
163
|
+
try:
|
|
164
|
+
with self._get_connection() as conn:
|
|
165
|
+
metadata = self._read_metadata(conn)
|
|
166
|
+
except sqlite3.Error as exc:
|
|
167
|
+
return False, f"failed to read metadata: {exc}"
|
|
168
|
+
|
|
169
|
+
raw_schema_version = metadata.get("schema_version")
|
|
170
|
+
if raw_schema_version is None:
|
|
171
|
+
return False, "missing schema_version metadata"
|
|
117
172
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
173
|
+
try:
|
|
174
|
+
schema_version = int(raw_schema_version)
|
|
175
|
+
except ValueError:
|
|
176
|
+
return False, f"invalid schema_version value: {raw_schema_version}"
|
|
177
|
+
|
|
178
|
+
if schema_version != self.SCHEMA_VERSION:
|
|
179
|
+
return False, (
|
|
180
|
+
"schema_version mismatch "
|
|
181
|
+
f"(expected={self.SCHEMA_VERSION}, found={schema_version})"
|
|
122
182
|
)
|
|
123
|
-
result = cursor.fetchone()
|
|
124
183
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
184
|
+
compatibility_token = metadata.get("compatibility_token")
|
|
185
|
+
if compatibility_token != self.COMPATIBILITY_TOKEN:
|
|
186
|
+
return False, (
|
|
187
|
+
"compatibility_token mismatch "
|
|
188
|
+
f"(expected={self.COMPATIBILITY_TOKEN}, "
|
|
189
|
+
f"found={compatibility_token})"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return True, ""
|
|
193
|
+
|
|
194
|
+
def _invalidate_cache(self, reason: str) -> None:
|
|
195
|
+
"""Invalidate an incompatible cache database by deleting it."""
|
|
196
|
+
self.cache_file.unlink()
|
|
197
|
+
self.logger.warning(
|
|
198
|
+
"Invalidated incompatible Pelican cache %s (%s)",
|
|
199
|
+
self.cache_file,
|
|
200
|
+
reason,
|
|
201
|
+
)
|
|
138
202
|
|
|
139
203
|
@contextmanager
|
|
140
204
|
def _get_connection(self) -> Generator[sqlite3.Connection, None, None]:
|
|
@@ -400,6 +464,7 @@ class PelicanSource(Source):
|
|
|
400
464
|
)
|
|
401
465
|
|
|
402
466
|
cache: dict[str, list[str]] = defaultdict(list)
|
|
467
|
+
unchanged_dirs: set[str] = set()
|
|
403
468
|
|
|
404
469
|
# Walk the tree for URLs
|
|
405
470
|
try:
|
|
@@ -416,27 +481,36 @@ class PelicanSource(Source):
|
|
|
416
481
|
subdirs = cast("dict[str, PelicanInfo]", subdirs)
|
|
417
482
|
files = cast("dict[str, PelicanInfo]", files)
|
|
418
483
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
484
|
+
if name in unchanged_dirs:
|
|
485
|
+
cached_urls = self._cache[name]
|
|
486
|
+
cache[name] = cached_urls
|
|
487
|
+
yield from cached_urls
|
|
488
|
+
else:
|
|
489
|
+
# Parse files for this directory when not marked unchanged
|
|
490
|
+
for info in files.values():
|
|
491
|
+
filename = info["name"]
|
|
492
|
+
url = f"{pelican_url}{filename}"
|
|
493
|
+
cache[name].append(url)
|
|
494
|
+
yield url
|
|
425
495
|
|
|
426
|
-
|
|
496
|
+
self._check_subdirs(subdirs, exclude, unchanged_dirs)
|
|
427
497
|
|
|
428
498
|
# Update memory cache
|
|
429
499
|
self._cache = cache
|
|
430
500
|
|
|
431
|
-
def
|
|
501
|
+
def _check_subdirs(
|
|
432
502
|
self,
|
|
433
503
|
dirnames: dict[str, PelicanInfo],
|
|
434
504
|
exclude: Iterable[re.Pattern],
|
|
435
|
-
|
|
436
|
-
) ->
|
|
437
|
-
"""
|
|
505
|
+
unchanged_dirs: set[str],
|
|
506
|
+
) -> None:
|
|
507
|
+
"""Check subdirectories in a Pelican directory walk.
|
|
508
|
+
|
|
509
|
+
This method attemps to optimise the outer walk() by checking
|
|
510
|
+
each subdirectory for exclusion patterns, emptiness,
|
|
511
|
+
and whether it is unchanged based on modification time.
|
|
438
512
|
|
|
439
|
-
Note that ``dirnames`` and ``
|
|
513
|
+
Note that ``dirnames`` and ``unchanged_dirs`` are modified in place.
|
|
440
514
|
"""
|
|
441
515
|
# NOTE: need list() here to get a static copy of the subdirs, so we can
|
|
442
516
|
# modify the original dict while iterating
|
|
@@ -456,25 +530,34 @@ class PelicanSource(Source):
|
|
|
456
530
|
# if subdir is empty, remove it
|
|
457
531
|
if info["size"] == 0:
|
|
458
532
|
del dirnames[path]
|
|
533
|
+
self.logger.debug(
|
|
534
|
+
"[%s] Skipping empty subdirectory: %s",
|
|
535
|
+
self.name,
|
|
536
|
+
name,
|
|
537
|
+
)
|
|
459
538
|
continue
|
|
460
539
|
|
|
461
540
|
mod = info.get("modified")
|
|
462
541
|
# Check if directory is current in memory cache
|
|
463
|
-
if (
|
|
464
|
-
name
|
|
465
|
-
and name in self._cache_modified
|
|
466
|
-
and mod == self._cache_modified[name] # unchanged
|
|
467
|
-
):
|
|
468
|
-
# Directory is current - yield from cache
|
|
469
|
-
yield from self._cache[name]
|
|
470
|
-
cache[name] = self._cache[name]
|
|
471
|
-
del dirnames[path]
|
|
542
|
+
if self._is_unchanged(name, mod):
|
|
543
|
+
unchanged_dirs.add(name)
|
|
472
544
|
continue
|
|
473
545
|
|
|
474
546
|
# Store the current modification time for this directory
|
|
475
547
|
if mod is not None:
|
|
476
548
|
self._cache_modified[name] = mod
|
|
477
549
|
|
|
550
|
+
def _is_unchanged(self, name: str, mod: datetime | None) -> bool:
|
|
551
|
+
"""Check if a directory is unchanged based on modification time."""
|
|
552
|
+
return (
|
|
553
|
+
# has been cached before
|
|
554
|
+
name in self._cache
|
|
555
|
+
# reports its own modification time
|
|
556
|
+
and mod is not None
|
|
557
|
+
# and the modification time matches the cached one
|
|
558
|
+
and self._cache_modified.get(name) == mod
|
|
559
|
+
)
|
|
560
|
+
|
|
478
561
|
def _iter_cache(self) -> Iterator[str]:
|
|
479
562
|
for dirurls in self._cache.values():
|
|
480
563
|
yield from dirurls
|
|
@@ -166,6 +166,11 @@ force-wrap-aliases = true
|
|
|
166
166
|
"INP001", # implicit namespace package
|
|
167
167
|
]
|
|
168
168
|
|
|
169
|
+
[tool.ruff.lint.pep8-naming]
|
|
170
|
+
extend-ignore-names = [
|
|
171
|
+
"__version__", # allow __version__ to be renamed on import
|
|
172
|
+
]
|
|
173
|
+
|
|
169
174
|
# -- setuptools
|
|
170
175
|
|
|
171
176
|
[tool.setuptools]
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import re
|
|
9
9
|
import sqlite3
|
|
10
|
+
from contextlib import closing
|
|
10
11
|
from datetime import (
|
|
11
12
|
UTC,
|
|
12
13
|
datetime,
|
|
@@ -16,6 +17,7 @@ from unittest.mock import patch
|
|
|
16
17
|
|
|
17
18
|
import pytest
|
|
18
19
|
|
|
20
|
+
from gwdatafind_server import __version__ as GWDATAFIND_SERVER_VERSION
|
|
19
21
|
from gwdatafind_server.cache import (
|
|
20
22
|
T010150SourceCache,
|
|
21
23
|
)
|
|
@@ -72,6 +74,12 @@ def test_parse_federation_url(input_url: str, expected_output: str) -> None:
|
|
|
72
74
|
class TestPelicanCache:
|
|
73
75
|
"""Test suite for PelicanCache class."""
|
|
74
76
|
|
|
77
|
+
@staticmethod
|
|
78
|
+
def _read_metadata(cache_file: Path) -> dict[str, str]:
|
|
79
|
+
with closing(sqlite3.connect(cache_file)) as conn:
|
|
80
|
+
cursor = conn.execute("SELECT key, value FROM cache_metadata")
|
|
81
|
+
return dict(cursor.fetchall())
|
|
82
|
+
|
|
75
83
|
@pytest.fixture
|
|
76
84
|
def cache_file(self, tmp_path: Path) -> Path:
|
|
77
85
|
"""Temporary cache file path."""
|
|
@@ -104,6 +112,17 @@ class TestPelicanCache:
|
|
|
104
112
|
assert url_cache == {}
|
|
105
113
|
assert modification_times == {}
|
|
106
114
|
|
|
115
|
+
def test_init_writes_metadata(self, cache_file: Path):
|
|
116
|
+
"""Test cache initialization writes required metadata keys."""
|
|
117
|
+
PelicanCache(cache_file)
|
|
118
|
+
metadata = self._read_metadata(cache_file)
|
|
119
|
+
|
|
120
|
+
assert metadata["schema_version"] == str(PelicanCache.SCHEMA_VERSION)
|
|
121
|
+
assert (
|
|
122
|
+
metadata["compatibility_token"] == PelicanCache.COMPATIBILITY_TOKEN
|
|
123
|
+
)
|
|
124
|
+
assert metadata["created_by"] == GWDATAFIND_SERVER_VERSION
|
|
125
|
+
|
|
107
126
|
def test_save_and_load_cache(self, pelican_cache: PelicanCache):
|
|
108
127
|
"""Test saving and loading cache data."""
|
|
109
128
|
# Prepare test data
|
|
@@ -149,6 +168,51 @@ class TestPelicanCache:
|
|
|
149
168
|
assert loaded_cache == url_cache
|
|
150
169
|
assert loaded_times == {}
|
|
151
170
|
|
|
171
|
+
def test_init_invalidates_mismatched_schema_version(self, cache_file: Path):
|
|
172
|
+
"""Test incompatible schema version causes automatic cache invalidation."""
|
|
173
|
+
cache = PelicanCache(cache_file)
|
|
174
|
+
cache.save(
|
|
175
|
+
{"/test/dir": ["pelican://osg-htc.org/gwdata/file.gwf"]},
|
|
176
|
+
{"/test/dir": datetime.now(UTC)},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
with closing(sqlite3.connect(cache_file)) as conn:
|
|
180
|
+
conn.execute(
|
|
181
|
+
"UPDATE cache_metadata SET value = ? WHERE key = ?",
|
|
182
|
+
("0", "schema_version"),
|
|
183
|
+
)
|
|
184
|
+
conn.commit()
|
|
185
|
+
|
|
186
|
+
reset_cache = PelicanCache(cache_file)
|
|
187
|
+
loaded_cache, loaded_times = reset_cache.load()
|
|
188
|
+
metadata = self._read_metadata(cache_file)
|
|
189
|
+
|
|
190
|
+
assert loaded_cache == {}
|
|
191
|
+
assert loaded_times == {}
|
|
192
|
+
assert metadata["schema_version"] == str(PelicanCache.SCHEMA_VERSION)
|
|
193
|
+
assert (
|
|
194
|
+
metadata["compatibility_token"] == PelicanCache.COMPATIBILITY_TOKEN
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def test_init_invalidates_missing_compatibility_token(self, cache_file: Path):
|
|
198
|
+
"""Test missing compatibility token causes automatic cache invalidation."""
|
|
199
|
+
PelicanCache(cache_file)
|
|
200
|
+
|
|
201
|
+
with closing(sqlite3.connect(cache_file)) as conn:
|
|
202
|
+
conn.execute(
|
|
203
|
+
"DELETE FROM cache_metadata WHERE key = ?",
|
|
204
|
+
("compatibility_token",),
|
|
205
|
+
)
|
|
206
|
+
conn.commit()
|
|
207
|
+
|
|
208
|
+
PelicanCache(cache_file)
|
|
209
|
+
metadata = self._read_metadata(cache_file)
|
|
210
|
+
|
|
211
|
+
assert metadata["schema_version"] == str(PelicanCache.SCHEMA_VERSION)
|
|
212
|
+
assert (
|
|
213
|
+
metadata["compatibility_token"] == PelicanCache.COMPATIBILITY_TOKEN
|
|
214
|
+
)
|
|
215
|
+
|
|
152
216
|
@patch(
|
|
153
217
|
"sqlite3.connect",
|
|
154
218
|
side_effect=sqlite3.OperationalError("mocked failure"),
|
|
@@ -272,8 +336,8 @@ class TestPelicanSource(BaseTestSource):
|
|
|
272
336
|
"""Test _exclude_dir with empty patterns list."""
|
|
273
337
|
assert not PelicanSource._exclude_dir("any_file", []) # noqa: SLF001
|
|
274
338
|
|
|
275
|
-
def
|
|
276
|
-
"""Test
|
|
339
|
+
def test_check_subdirs(self, basic_config: PelicanSourceConfig):
|
|
340
|
+
"""Test _check_subdirs method."""
|
|
277
341
|
source = PelicanSource(basic_config)
|
|
278
342
|
|
|
279
343
|
# Set up test data
|
|
@@ -299,21 +363,21 @@ class TestPelicanSource(BaseTestSource):
|
|
|
299
363
|
}
|
|
300
364
|
|
|
301
365
|
exclude_patterns = [re.compile(r".*debug.*")]
|
|
302
|
-
|
|
366
|
+
unchanged_dirs: set[str] = set()
|
|
303
367
|
|
|
304
368
|
# Test the method - don't need to keep return value
|
|
305
|
-
|
|
369
|
+
source._check_subdirs(dirnames, exclude_patterns, unchanged_dirs) # noqa: SLF001
|
|
306
370
|
|
|
307
371
|
# Verify empty and excluded directories are removed
|
|
308
372
|
assert "/gwdata/O4/tmp" not in dirnames # Empty directory removed
|
|
309
373
|
assert "/gwdata/O4/debug" not in dirnames # Excluded directory removed
|
|
310
374
|
assert "/gwdata/O4/strain" in dirnames # Valid directory remains
|
|
311
375
|
|
|
312
|
-
def
|
|
376
|
+
def test_check_subdirs_with_cache_hit(
|
|
313
377
|
self,
|
|
314
378
|
basic_config: PelicanSourceConfig,
|
|
315
379
|
):
|
|
316
|
-
"""Test
|
|
380
|
+
"""Test _check_subdirs marks unchanged directories for cache use."""
|
|
317
381
|
source = PelicanSource(basic_config)
|
|
318
382
|
|
|
319
383
|
# Set up existing cache
|
|
@@ -335,16 +399,84 @@ class TestPelicanSource(BaseTestSource):
|
|
|
335
399
|
},
|
|
336
400
|
}
|
|
337
401
|
|
|
338
|
-
|
|
402
|
+
unchanged_dirs: set[str] = set()
|
|
339
403
|
|
|
340
404
|
# Test the method
|
|
341
|
-
|
|
405
|
+
source._check_subdirs(dirnames, [], unchanged_dirs) # noqa: SLF001
|
|
406
|
+
|
|
407
|
+
# Verify directory is marked unchanged and retained for traversal
|
|
408
|
+
assert "/gwdata/O4/strain" in unchanged_dirs
|
|
409
|
+
assert "/gwdata/O4/strain" in dirnames
|
|
410
|
+
|
|
411
|
+
def test_query_pelican_uses_cache_without_skipping_descendants(
|
|
412
|
+
self,
|
|
413
|
+
basic_config: PelicanSourceConfig,
|
|
414
|
+
):
|
|
415
|
+
"""Test unchanged directories use cache while descendants are still walked."""
|
|
416
|
+
source = PelicanSource(basic_config)
|
|
417
|
+
|
|
418
|
+
mod_time = datetime(2024, 1, 1, tzinfo=UTC)
|
|
419
|
+
new_mod = datetime(2024, 1, 2, tzinfo=UTC)
|
|
420
|
+
parent_cached_url = "pelican://osg-htc.org/gwdata/O4/parent.gwf"
|
|
421
|
+
source._cache = { # noqa: SLF001
|
|
422
|
+
"/gwdata/O4": [parent_cached_url],
|
|
423
|
+
}
|
|
424
|
+
source._cache_modified = { # noqa: SLF001
|
|
425
|
+
"/gwdata/O4": mod_time,
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
parent_subdirs: dict[str, PelicanInfo] = {
|
|
429
|
+
"/gwdata/O4/new": {
|
|
430
|
+
"name": "/gwdata/O4/new/",
|
|
431
|
+
"size": 1024,
|
|
432
|
+
"type": "directory",
|
|
433
|
+
"modified": new_mod,
|
|
434
|
+
},
|
|
435
|
+
}
|
|
436
|
+
parent_files: dict[str, PelicanInfo] = {
|
|
437
|
+
"parent.gwf": {
|
|
438
|
+
"name": "/gwdata/O4/parent.gwf",
|
|
439
|
+
"size": 128,
|
|
440
|
+
"type": "file",
|
|
441
|
+
"modified": mod_time,
|
|
442
|
+
},
|
|
443
|
+
}
|
|
444
|
+
child_files: dict[str, PelicanInfo] = {
|
|
445
|
+
"child.gwf": {
|
|
446
|
+
"name": "/gwdata/O4/new/child.gwf",
|
|
447
|
+
"size": 256,
|
|
448
|
+
"type": "file",
|
|
449
|
+
"modified": new_mod,
|
|
450
|
+
},
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
walk_entries = [
|
|
454
|
+
("/gwdata/O4", parent_subdirs, parent_files),
|
|
455
|
+
("/gwdata/O4/new", {}, child_files),
|
|
456
|
+
]
|
|
457
|
+
|
|
458
|
+
with patch("pelicanfs.core.PelicanFileSystem") as mock_pelfs_init:
|
|
459
|
+
mock_pelfs = mock_pelfs_init.return_value
|
|
460
|
+
mock_pelfs.discovery_url = "pelican://osg-htc.org"
|
|
461
|
+
mock_pelfs.dircache.use_listings_cache = True
|
|
462
|
+
mock_pelfs.http_file_system.dircache.use_listings_cache = True
|
|
463
|
+
mock_pelfs.fastwalk.return_value = iter(walk_entries)
|
|
464
|
+
|
|
465
|
+
urls = list(
|
|
466
|
+
source.query_pelican(
|
|
467
|
+
source.federation_url,
|
|
468
|
+
source.path,
|
|
469
|
+
),
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
child_url = "pelican://osg-htc.org/gwdata/O4/new/child.gwf"
|
|
473
|
+
|
|
474
|
+
# Parent comes from cache exactly once; child is discovered by traversal
|
|
475
|
+
assert urls == [parent_cached_url, child_url]
|
|
342
476
|
|
|
343
|
-
#
|
|
344
|
-
assert
|
|
345
|
-
|
|
346
|
-
assert cache["/gwdata/O4/strain"] == ["cached_url1", "cached_url2"]
|
|
347
|
-
assert "/gwdata/O4/strain" not in dirnames # Directory removed after cache hit
|
|
477
|
+
# Cache is refreshed with both parent and child directories
|
|
478
|
+
assert source._cache["/gwdata/O4"] == [parent_cached_url] # noqa: SLF001
|
|
479
|
+
assert source._cache["/gwdata/O4/new"] == [child_url] # noqa: SLF001
|
|
348
480
|
|
|
349
481
|
def test_run_method_basic(
|
|
350
482
|
self,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/examples/diskcache/gwdatafind-server.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/credential/__init__.py
RENAMED
|
File without changes
|
{gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/credential/credential.py
RENAMED
|
File without changes
|
{gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/credential/scitoken.py
RENAMED
|
File without changes
|
{gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server/gwdatafind_server.wsgi
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{gwdatafind_server-2.1.0 → gwdatafind_server-2.2.0}/gwdatafind_server.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|