bxc 1.1.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.
bxc/__init__.py ADDED
@@ -0,0 +1,76 @@
1
+ """
2
+ bxc - Bibliographic Reference Formatter
3
+ """
4
+
5
+ import os
6
+ from bxc.parser import parse_bibtex
7
+ from bxc.exceptions import BxcError, BibTeXParseError, StyleNotFoundError
8
+ from bxc.cache import (
9
+ get_cache_dir,
10
+ resolve_style,
11
+ update_cache,
12
+ clear_cache,
13
+ rebuild_cache,
14
+ get_cache_status,
15
+ )
16
+ from bxc.registry import search_styles
17
+
18
+ __version__ = "1.1.0"
19
+
20
+ def format_bibtex(
21
+ bibtex_source: str,
22
+ style: str,
23
+ output_format: str = "plain",
24
+ mode: str = "bibliography",
25
+ cite: str | None = None,
26
+ is_path: bool = False
27
+ ) -> str:
28
+ """
29
+ Formats BibTeX references using a CSL style.
30
+ """
31
+ if bibtex_source is None or style is None:
32
+ raise TypeError("bibtex_source and style cannot be None")
33
+ if not isinstance(bibtex_source, str) or not isinstance(style, str):
34
+ raise TypeError("bibtex_source and style must be strings")
35
+
36
+ valid_formats = {"plain", "markdown", "html"}
37
+ if output_format not in valid_formats:
38
+ raise BxcError(f"Unsupported output format: '{output_format}'")
39
+
40
+ valid_modes = {"bibliography", "citation"}
41
+ if mode not in valid_modes:
42
+ raise BxcError(f"Unsupported selection mode: '{mode}'")
43
+
44
+ if bibtex_source == "-":
45
+ is_path = True
46
+ elif not is_path and bibtex_source and not bibtex_source.strip().startswith("@"):
47
+ if bibtex_source.endswith(".bib") or os.path.exists(bibtex_source):
48
+ is_path = True
49
+
50
+ entries = parse_bibtex(bibtex_source, is_path=is_path)
51
+ style_path = resolve_style(style)
52
+
53
+ from bxc.formatter import format_references
54
+ return format_references(
55
+ entries=entries,
56
+ style_path=style_path,
57
+ format=output_format,
58
+ mode=mode,
59
+ cite=cite
60
+ )
61
+
62
+
63
+ __all__ = [
64
+ "parse_bibtex",
65
+ "format_bibtex",
66
+ "BxcError",
67
+ "BibTeXParseError",
68
+ "StyleNotFoundError",
69
+ "get_cache_dir",
70
+ "resolve_style",
71
+ "update_cache",
72
+ "clear_cache",
73
+ "rebuild_cache",
74
+ "get_cache_status",
75
+ "search_styles",
76
+ ]
bxc/author.py ADDED
@@ -0,0 +1,62 @@
1
+ """
2
+ Author and editor parsing for BibTeX files.
3
+ """
4
+
5
+ import re
6
+
7
+
8
+ class AuthorParser:
9
+ """
10
+ Parses BibTeX author and editor name strings into structured CSL formats.
11
+ """
12
+
13
+ def parse_authors(self, author_str: str) -> list[dict[str, str]]:
14
+ """
15
+ Parses a BibTeX author/editor string (separated by 'and') into CSL-JSON family/given components.
16
+ """
17
+ if not author_str:
18
+ return []
19
+ # Split by 'and' case-insensitively; a single \s (not \s+) avoids any
20
+ # quantifier ambiguity, since leftover whitespace is stripped below anyway.
21
+ parts = re.split(r'\sand\s', author_str, flags=re.IGNORECASE)
22
+ authors = []
23
+ for part in parts:
24
+ part = part.strip()
25
+ if part:
26
+ authors.append(self._parse_name(part))
27
+ return authors
28
+
29
+ @staticmethod
30
+ def _is_braced_literal(part: str) -> bool:
31
+ return part.startswith('{') and part.endswith('}') and '{' not in part[1:-1] and '}' not in part[1:-1]
32
+
33
+ @staticmethod
34
+ def _split_name(part: str) -> tuple[str, str, str]:
35
+ """Returns (family, given, suffix) for a single BibTeX name."""
36
+ if ',' in part:
37
+ pieces = [x.strip() for x in part.split(',')]
38
+ if len(pieces) >= 3:
39
+ # "von Last, Jr, First"
40
+ return pieces[0], ", ".join(pieces[2:]), pieces[1]
41
+ return pieces[0], pieces[1], ""
42
+ words = part.rsplit(None, 1)
43
+ if len(words) == 2:
44
+ return words[1].strip(), words[0].strip(), ""
45
+ return part.strip(), "", ""
46
+
47
+ def _parse_name(self, part: str) -> dict[str, str]:
48
+ if part.lower() == "others":
49
+ return {"literal": "et al."}
50
+ if self._is_braced_literal(part):
51
+ # Fully braced name is an institution / single literal name
52
+ return {"literal": part[1:-1].strip()}
53
+ family, given, suffix = self._split_name(part)
54
+ # Clean curly braces that protect case
55
+ author = {
56
+ "family": family.replace('{', '').replace('}', ''),
57
+ "given": given.replace('{', '').replace('}', ''),
58
+ }
59
+ suffix = suffix.replace('{', '').replace('}', '')
60
+ if suffix:
61
+ author["suffix"] = suffix
62
+ return author
bxc/cache.py ADDED
@@ -0,0 +1,450 @@
1
+ """
2
+ Cache management module for bxc.
3
+ """
4
+
5
+ import os
6
+ import shutil
7
+ import platformdirs
8
+ import tarfile
9
+ import tempfile
10
+ import urllib.request
11
+ import urllib.error
12
+ import xml.etree.ElementTree as ET
13
+ import json
14
+ import re
15
+ import shutil as _shutil
16
+ from typing import Any
17
+ from bxc.exceptions import StyleNotFoundError, BxcError
18
+
19
+ CSL_REMOTE_URL_PREFIX = "https://raw.githubusercontent.com/citation-style-language/styles/master/"
20
+
21
+ # Local fallback for when the CDN above is unreachable or rate-limited: a
22
+ # bundled snapshot of the full CSL styles repository (top-level + dependent/),
23
+ # compressed with LZMA2 (xz) at max settings so it stays readable via the
24
+ # standard library's tarfile/lzma modules without adding a runtime dependency.
25
+ BUNDLED_STYLES_ARCHIVE = os.path.join(os.path.dirname(__file__), "styles_bundle.tar.xz")
26
+ STYLES_INDEX_FILENAME = "styles_index.json"
27
+ BUNDLED_STYLES_INDEX = os.path.join(os.path.dirname(__file__), STYLES_INDEX_FILENAME)
28
+
29
+ # A CSL style id is a short lowercase slug; anything else (path separators, "..",
30
+ # absolute paths, drive letters) must never reach the filesystem or the remote URL.
31
+ STYLE_NAME_PATTERN = re.compile(r"[a-z0-9][a-z0-9._-]{0,127}")
32
+
33
+
34
+ def validate_style_name(style_filename: str) -> str:
35
+ """Returns the style file name if it is a safe slug ending in .csl, else raises StyleNotFoundError."""
36
+ stem = style_filename[:-4] if style_filename.endswith(".csl") else style_filename
37
+ if not STYLE_NAME_PATTERN.fullmatch(stem) or ".." in stem:
38
+ raise StyleNotFoundError(
39
+ f"Invalid CSL style name '{style_filename}': expected a lowercase name such as 'ieee' "
40
+ "or the path to an existing .csl file."
41
+ )
42
+ return style_filename
43
+
44
+
45
+ def _user_agent() -> str:
46
+ from bxc import __version__
47
+
48
+ return f"bxc-bibliographic-formatter/{__version__}"
49
+
50
+
51
+ class CacheManager:
52
+ """
53
+ Manages the local OS-native CSL cache directory and remote updates.
54
+ """
55
+
56
+ def __init__(self, remote_url: str | None = None) -> None:
57
+ self.remote_url_prefix = remote_url or CSL_REMOTE_URL_PREFIX
58
+
59
+ def get_cache_dir(self) -> str:
60
+ """
61
+ Returns the absolute path to the local OS-native CSL cache directory.
62
+ Uses the BXC_CACHE_DIR environment variable if set to allow test isolation.
63
+ """
64
+ base_dir = os.environ.get("BXC_CACHE_DIR")
65
+ if not base_dir:
66
+ base_dir = platformdirs.user_cache_dir(appname="bxc")
67
+
68
+ styles_dir = os.path.join(base_dir, "styles")
69
+ return styles_dir
70
+
71
+ def _fetch_url(self, url: str) -> bytes:
72
+ req = urllib.request.Request(
73
+ url,
74
+ headers={"User-Agent": _user_agent()}
75
+ )
76
+ with urllib.request.urlopen(req, timeout=10) as response:
77
+ return response.read()
78
+
79
+ def _write_cache_file_atomic(self, cache_dir: str, cache_path: str, content: bytes) -> None:
80
+ # Use delete=False for cross-platform (Windows) compatibility
81
+ tmp_file = tempfile.NamedTemporaryFile(dir=cache_dir, suffix=".tmp", delete=False)
82
+ temp_path = tmp_file.name
83
+ try:
84
+ tmp_file.write(content)
85
+ tmp_file.close() # Must close before os.replace
86
+ os.replace(temp_path, cache_path)
87
+ except Exception:
88
+ tmp_file.close()
89
+ if os.path.exists(temp_path):
90
+ try:
91
+ os.remove(temp_path)
92
+ except OSError:
93
+ pass
94
+ raise
95
+
96
+ def _extract_from_bundle(self, style_name: str) -> bytes | None:
97
+ """
98
+ Looks up style_name in the bundled local CSL archive shipped with bxc.
99
+ Returns the raw CSL bytes if found, or None if the archive is missing
100
+ or doesn't contain this style.
101
+ """
102
+ if not os.path.exists(BUNDLED_STYLES_ARCHIVE):
103
+ return None
104
+ try:
105
+ with tarfile.open(BUNDLED_STYLES_ARCHIVE, mode="r:xz") as archive:
106
+ for member_name in (f"./{style_name}", f"./dependent/{style_name}"):
107
+ try:
108
+ member = archive.getmember(member_name)
109
+ except KeyError:
110
+ continue
111
+ extracted = archive.extractfile(member)
112
+ if extracted is not None:
113
+ return extracted.read()
114
+ except (tarfile.TarError, OSError):
115
+ return None
116
+ return None
117
+
118
+ def _download_from_cdn(self, style_name: str, cache_dir: str, cache_path: str) -> Exception | None:
119
+ """
120
+ Tries every candidate CDN URL for style_name. On success, writes the
121
+ result to cache_path and returns None. On total failure, returns the
122
+ last error encountered instead of raising, so callers can decide what
123
+ to try next (e.g. the bundled archive) before giving up.
124
+ """
125
+ remote_base = os.environ.get("BXC_REMOTE_URL", self.remote_url_prefix)
126
+ if not remote_base.endswith("/"):
127
+ remote_base += "/"
128
+
129
+ # Dependent styles (e.g. journal-specific variants) live under a
130
+ # dependent/ prefix on the real CSL repository, not at the top level.
131
+ candidate_urls = [f"{remote_base}{style_name}", f"{remote_base}dependent/{style_name}"]
132
+
133
+ last_error: Exception | None = None
134
+ for url in candidate_urls:
135
+ try:
136
+ content = self._fetch_url(url)
137
+ self._write_cache_file_atomic(cache_dir, cache_path, content)
138
+ return None
139
+ except urllib.error.HTTPError as e:
140
+ last_error = e
141
+ if e.code == 404:
142
+ continue # try the next candidate path
143
+ break # a non-404 HTTP error means the CDN itself is misbehaving
144
+ except Exception as e:
145
+ last_error = e
146
+ break # network-level failure (DNS/timeout/connection) - no point retrying paths
147
+ return last_error
148
+
149
+ def download_style(self, style_name: str, cache_path: str, prefer_bundle: bool = True) -> None:
150
+ """
151
+ Resolves a CSL style and saves it to cache_path.
152
+
153
+ By default (prefer_bundle=True), checks the local bundled archive
154
+ shipped with bxc first - instant, offline, and avoids hammering the
155
+ CDN for the 10,000+ styles already snapshotted locally - and only
156
+ talks to the network for styles the bundle doesn't have. This is
157
+ what everyday style resolution (`bxc format`) uses.
158
+
159
+ Sync-oriented callers that specifically want fresh content from the
160
+ CDN (`bxc cache update` / `bxc cache rebuild`) pass prefer_bundle=False
161
+ to check the CDN first instead, still falling back to the bundle if
162
+ the CDN is unreachable. Ensures thread-safe and process-safe atomic
163
+ writes either way.
164
+
165
+ An explicit BXC_REMOTE_URL override always wins over prefer_bundle:
166
+ pointing bxc at a specific remote (a private mirror, a test mock
167
+ server) is a deliberate signal to talk to that remote, not to get
168
+ silently intercepted by the bundled snapshot.
169
+ """
170
+ cache_dir = os.path.dirname(cache_path)
171
+ os.makedirs(cache_dir, exist_ok=True)
172
+
173
+ effective_prefer_bundle = prefer_bundle and "BXC_REMOTE_URL" not in os.environ
174
+
175
+ if effective_prefer_bundle:
176
+ bundled_content = self._extract_from_bundle(style_name)
177
+ if bundled_content is not None:
178
+ self._write_cache_file_atomic(cache_dir, cache_path, bundled_content)
179
+ return
180
+ last_error = self._download_from_cdn(style_name, cache_dir, cache_path)
181
+ if last_error is None:
182
+ return
183
+ else:
184
+ last_error = self._download_from_cdn(style_name, cache_dir, cache_path)
185
+ if last_error is None:
186
+ return
187
+ bundled_content = self._extract_from_bundle(style_name)
188
+ if bundled_content is not None:
189
+ self._write_cache_file_atomic(cache_dir, cache_path, bundled_content)
190
+ return
191
+
192
+ if isinstance(last_error, urllib.error.HTTPError):
193
+ if last_error.code == 404:
194
+ raise StyleNotFoundError(
195
+ f"Failed to download CSL style '{style_name}': not found on remote server (404)."
196
+ ) from last_error
197
+ raise StyleNotFoundError(
198
+ f"Failed to download CSL style '{style_name}' (HTTP {last_error.code})."
199
+ ) from last_error
200
+ raise StyleNotFoundError(
201
+ f"Failed to download CSL style '{style_name}': not found locally and "
202
+ f"could not be fetched from the remote repository: {last_error}"
203
+ ) from last_error
204
+
205
+ def extract_independent_parent(self, csl_content: bytes) -> str | None:
206
+ """
207
+ Parses CSL XML content and extracts the style name of the independent parent if present.
208
+ """
209
+ if not csl_content:
210
+ raise BxcError("CSL style file is empty.")
211
+ try:
212
+ root = ET.fromstring(csl_content)
213
+ for elem in root.iter():
214
+ tag = elem.tag
215
+ if tag.endswith("link") or tag == "link":
216
+ rel = elem.attrib.get("rel")
217
+ href = elem.attrib.get("href")
218
+ if rel == "independent-parent" and href:
219
+ parent_name = href.rstrip("/").split("/")[-1]
220
+ return parent_name
221
+ except ET.ParseError as e:
222
+ raise BxcError(f"XML parse error: {e}") from e
223
+ return None
224
+
225
+ def _resolve_parent_or_path(self, content: bytes, path: str, visited: set[str]) -> str:
226
+ parent_name = self.extract_independent_parent(content)
227
+ if parent_name:
228
+ return self.resolve_style(parent_name, visited)
229
+ return os.path.abspath(path)
230
+
231
+ def _read_and_resolve(self, path: str, visited: set[str], error_context: str) -> str:
232
+ try:
233
+ with open(path, "rb") as f:
234
+ content = f.read()
235
+ except Exception as e:
236
+ raise BxcError(f"{error_context}: {e}") from e
237
+ return self._resolve_parent_or_path(content, path, visited)
238
+
239
+ def _resolve_local_style(self, style_identifier: str, visited: set[str]) -> str:
240
+ if not os.path.isfile(style_identifier):
241
+ raise StyleNotFoundError(
242
+ f"Local CSL style file '{style_identifier}' could not be resolved. "
243
+ "The file does not exist or is not a valid file."
244
+ )
245
+ abs_path = os.path.abspath(style_identifier)
246
+ return self._read_and_resolve(
247
+ abs_path, visited, f"Failed to read local CSL style '{style_identifier}'"
248
+ )
249
+
250
+ def _resolve_cached_style(self, style_identifier: str, visited: set[str]) -> str:
251
+ style_name_lower = style_identifier.lower()
252
+ style_filename = style_name_lower if style_name_lower.endswith(".csl") else f"{style_name_lower}.csl"
253
+ validate_style_name(style_filename)
254
+
255
+ cache_dir = self.get_cache_dir()
256
+ cache_path = os.path.join(cache_dir, style_filename)
257
+ cache_exists = os.path.exists(cache_path) and os.path.isfile(cache_path)
258
+
259
+ if cache_exists and os.path.getsize(cache_path) == 0:
260
+ self.download_style(style_filename, cache_path)
261
+ return self._read_and_resolve(
262
+ cache_path, visited, f"Failed to read cached CSL style '{cache_path}' after download"
263
+ )
264
+
265
+ if cache_exists:
266
+ try:
267
+ return self._read_and_resolve(cache_path, visited, "")
268
+ except Exception:
269
+ pass # corrupted/unreadable cache entry - fall through and redownload
270
+
271
+ self.download_style(style_filename, cache_path)
272
+ return self._read_and_resolve(
273
+ cache_path, visited, f"Failed to read cached CSL style '{cache_path}'"
274
+ )
275
+
276
+ @staticmethod
277
+ def _normalize_style_identifier(style_identifier: str) -> tuple[str, bool]:
278
+ is_local_path = style_identifier.lower().endswith(".csl") or os.path.exists(style_identifier)
279
+ if is_local_path:
280
+ norm_id = os.path.abspath(style_identifier)
281
+ else:
282
+ norm_id = style_identifier.lower()
283
+ if not norm_id.endswith(".csl"):
284
+ norm_id = f"{norm_id}.csl"
285
+ return norm_id, is_local_path
286
+
287
+ def resolve_style(self, style_identifier: str, visited: set[str] | None = None) -> str:
288
+ """
289
+ Resolves a style name or a local path to the absolute path of a CSL style file.
290
+ """
291
+ if visited is None:
292
+ visited = set()
293
+
294
+ norm_id, is_local_path = self._normalize_style_identifier(style_identifier)
295
+ if norm_id in visited:
296
+ raise BxcError(f"Circular dependency detected in style resolution for '{style_identifier}'")
297
+ visited.add(norm_id)
298
+
299
+ if is_local_path:
300
+ return self._resolve_local_style(style_identifier, visited)
301
+ return self._resolve_cached_style(style_identifier, visited)
302
+
303
+ def update_cache(self) -> bool:
304
+ """
305
+ Refreshes the style index. Returns True when a fresh index was downloaded from the
306
+ remote, False when the default remote has none (HTTP 404) and the bundled index
307
+ was installed instead.
308
+ """
309
+ cache_dir = self.get_cache_dir()
310
+ base_dir = os.path.dirname(cache_dir)
311
+ os.makedirs(base_dir, exist_ok=True)
312
+
313
+ index_path = os.path.join(base_dir, STYLES_INDEX_FILENAME)
314
+
315
+ remote_base = os.environ.get("BXC_REMOTE_URL", self.remote_url_prefix)
316
+ if not remote_base.endswith("/"):
317
+ remote_base += "/"
318
+ url = f"{remote_base}{STYLES_INDEX_FILENAME}"
319
+
320
+ tmp_file = tempfile.NamedTemporaryFile(dir=base_dir, suffix=".tmp", delete=False)
321
+ temp_path = tmp_file.name
322
+
323
+ try:
324
+ req = urllib.request.Request(
325
+ url,
326
+ headers={"User-Agent": _user_agent()}
327
+ )
328
+ with urllib.request.urlopen(req, timeout=10) as response:
329
+ content = response.read()
330
+
331
+ json.loads(content.decode("utf-8"))
332
+
333
+ tmp_file.write(content)
334
+ tmp_file.close()
335
+
336
+ os.replace(temp_path, index_path)
337
+ return True
338
+ except Exception as e:
339
+ tmp_file.close()
340
+ if os.path.exists(temp_path):
341
+ try:
342
+ os.remove(temp_path)
343
+ except OSError:
344
+ pass
345
+ # The upstream CSL repository does not publish styles_index.json; when using the
346
+ # default remote (no explicit override), refresh from the bundled index instead.
347
+ using_default_remote = "BXC_REMOTE_URL" not in os.environ and self.remote_url_prefix == CSL_REMOTE_URL_PREFIX
348
+ if (
349
+ using_default_remote
350
+ and isinstance(e, urllib.error.HTTPError)
351
+ and e.code == 404
352
+ and os.path.isfile(BUNDLED_STYLES_INDEX)
353
+ ):
354
+ _shutil.copyfile(BUNDLED_STYLES_INDEX, index_path)
355
+ return False
356
+ raise BxcError(f"Failed to update cache: {e}") from e
357
+
358
+ def clear_cache(self) -> None:
359
+ """
360
+ Clears all cached CSL styles and deletes the index file.
361
+ """
362
+ cache_dir = self.get_cache_dir()
363
+ if os.path.exists(cache_dir):
364
+ for filename in os.listdir(cache_dir):
365
+ file_path = os.path.join(cache_dir, filename)
366
+ try:
367
+ if os.path.isfile(file_path) or os.path.islink(file_path):
368
+ os.unlink(file_path)
369
+ elif os.path.isdir(file_path):
370
+ shutil.rmtree(file_path)
371
+ except Exception as e:
372
+ raise BxcError(f"Failed to delete cached style file '{file_path}': {e}")
373
+
374
+ base_dir = os.path.dirname(cache_dir)
375
+ index_path = os.path.join(base_dir, STYLES_INDEX_FILENAME)
376
+ if os.path.exists(index_path):
377
+ try:
378
+ os.remove(index_path)
379
+ except Exception as e:
380
+ raise BxcError(f"Failed to delete style index file '{index_path}': {e}")
381
+
382
+ def get_cache_status(self) -> dict[str, Any]:
383
+ """
384
+ Returns status information about the cache.
385
+ """
386
+ cache_dir = self.get_cache_dir()
387
+ base_dir = os.path.dirname(cache_dir)
388
+ index_path = os.path.join(base_dir, STYLES_INDEX_FILENAME)
389
+
390
+ styles_count = 0
391
+ corrupted_count = 0
392
+ if os.path.exists(cache_dir):
393
+ for filename in os.listdir(cache_dir):
394
+ file_path = os.path.join(cache_dir, filename)
395
+ if os.path.isfile(file_path) and filename.endswith(".csl"):
396
+ styles_count += 1
397
+ if os.path.getsize(file_path) == 0:
398
+ corrupted_count += 1
399
+
400
+ return {
401
+ "cache_dir": cache_dir,
402
+ "index_exists": os.path.exists(index_path),
403
+ "styles_count": styles_count,
404
+ "corrupted_count": corrupted_count
405
+ }
406
+
407
+ def rebuild_cache(self) -> None:
408
+ """
409
+ Rebuilds the cache by updating the index and re-downloading all cached CSL styles.
410
+ This is a "sync with CDN" operation, so it checks the CDN first for
411
+ fresh content rather than the (static) bundled archive - it only
412
+ falls back to the bundle if the CDN itself is unreachable.
413
+ """
414
+ self.update_cache()
415
+
416
+ cache_dir = self.get_cache_dir()
417
+ if os.path.exists(cache_dir):
418
+ for filename in os.listdir(cache_dir):
419
+ file_path = os.path.join(cache_dir, filename)
420
+ if os.path.isfile(file_path) and filename.endswith(".csl"):
421
+ self.download_style(filename, file_path, prefer_bundle=False)
422
+
423
+
424
+ # --- Global Backward Compatible API Functions ---
425
+
426
+ _default_cache_manager = CacheManager()
427
+
428
+
429
+ def get_cache_dir() -> str:
430
+ return _default_cache_manager.get_cache_dir()
431
+
432
+
433
+ def resolve_style(style_identifier: str, visited: set[str] | None = None) -> str:
434
+ return _default_cache_manager.resolve_style(style_identifier, visited)
435
+
436
+
437
+ def update_cache() -> bool:
438
+ return _default_cache_manager.update_cache()
439
+
440
+
441
+ def clear_cache() -> None:
442
+ return _default_cache_manager.clear_cache()
443
+
444
+
445
+ def get_cache_status() -> dict[str, Any]:
446
+ return _default_cache_manager.get_cache_status()
447
+
448
+
449
+ def rebuild_cache() -> None:
450
+ return _default_cache_manager.rebuild_cache()