contextlake 2.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.
Files changed (53) hide show
  1. contextlake/__init__.py +3 -0
  2. contextlake/__main__.py +6 -0
  3. contextlake/cli.py +333 -0
  4. contextlake/config.py +120 -0
  5. contextlake/core.py +773 -0
  6. contextlake/kb/__init__.py +15 -0
  7. contextlake/kb/commands.py +698 -0
  8. contextlake/kb/config.py +138 -0
  9. contextlake/kb/connectors/__init__.py +7 -0
  10. contextlake/kb/connectors/atlassian.py +175 -0
  11. contextlake/kb/connectors/common.py +42 -0
  12. contextlake/kb/connectors/figma.py +124 -0
  13. contextlake/kb/connectors/gitlab.py +86 -0
  14. contextlake/kb/connectors/orchestrate.py +143 -0
  15. contextlake/kb/embeddings/__init__.py +9 -0
  16. contextlake/kb/embeddings/base.py +48 -0
  17. contextlake/kb/embeddings/hybrid.py +79 -0
  18. contextlake/kb/embeddings/index.py +46 -0
  19. contextlake/kb/embeddings/ollama.py +42 -0
  20. contextlake/kb/embeddings/openai.py +54 -0
  21. contextlake/kb/embeddings/store.py +200 -0
  22. contextlake/kb/ids.py +40 -0
  23. contextlake/kb/llm/__init__.py +9 -0
  24. contextlake/kb/llm/base.py +44 -0
  25. contextlake/kb/llm/ollama.py +39 -0
  26. contextlake/kb/llm/openai.py +53 -0
  27. contextlake/kb/manifest.py +101 -0
  28. contextlake/kb/mcp_client.py +63 -0
  29. contextlake/kb/model.py +71 -0
  30. contextlake/kb/parse.py +329 -0
  31. contextlake/kb/references.py +66 -0
  32. contextlake/kb/security.py +34 -0
  33. contextlake/kb/server.py +231 -0
  34. contextlake/kb/state.py +40 -0
  35. contextlake/kb/steer/__init__.py +7 -0
  36. contextlake/kb/steer/generate.py +161 -0
  37. contextlake/kb/steer/skills.py +113 -0
  38. contextlake/kb/store/__init__.py +5 -0
  39. contextlake/kb/store/base.py +81 -0
  40. contextlake/kb/store/shards.py +97 -0
  41. contextlake/kb/store/sqlite_store.py +265 -0
  42. contextlake/kb/wiki/__init__.py +4 -0
  43. contextlake/kb/wiki/council.py +60 -0
  44. contextlake/kb/wiki/generate.py +86 -0
  45. contextlake/logging_setup.py +119 -0
  46. contextlake/safety.py +82 -0
  47. contextlake/style.py +147 -0
  48. contextlake-2.1.0.dist-info/METADATA +240 -0
  49. contextlake-2.1.0.dist-info/RECORD +53 -0
  50. contextlake-2.1.0.dist-info/WHEEL +5 -0
  51. contextlake-2.1.0.dist-info/entry_points.txt +3 -0
  52. contextlake-2.1.0.dist-info/licenses/LICENSE +21 -0
  53. contextlake-2.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ """contextlake — keep a local workspace mirrored with GitLab repositories."""
2
+
3
+ __version__ = "2.1.0"
@@ -0,0 +1,6 @@
1
+ """Enable `python -m contextlake`."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
contextlake/cli.py ADDED
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env python3
2
+ """GitLab Workspace Synchronization CLI Tool.
3
+
4
+ Keeps a local workspace mirrored with the GitLab repositories you can access:
5
+ clones what is missing, updates existing clones, and moves each repo onto its
6
+ most active development branch -- while protecting any local working branches.
7
+
8
+ Entry points (all equivalent):
9
+ contextlake <command> # installed console script
10
+ python -m contextlake <command>
11
+ python3 contextlake.py <command> # bare script, no install
12
+ """
13
+
14
+ import argparse
15
+ import sys
16
+
17
+ from . import __version__
18
+ from .config import DEFAULT_CONFIG, expand_path, get_cache_paths, load_config
19
+ from .core import (
20
+ clone_missing_repos,
21
+ fetch_gitlab_projects,
22
+ show_status,
23
+ switch_repository_branches,
24
+ update_repositories,
25
+ verify_structure,
26
+ )
27
+ from .logging_setup import log, setup_logging
28
+
29
+ # Boolean flags backed by paired --x / --no-x switches. They must default to
30
+ # None so we can tell "user passed a flag" from "user said nothing" -- otherwise
31
+ # the store_true default (False) silently overrides the config file every run.
32
+ _TRISTATE_FLAGS = (
33
+ "clean_corrupted",
34
+ "adaptive_workers",
35
+ "protect_working_branches",
36
+ "require_clean_workspace",
37
+ "auto_stash",
38
+ "dry_run",
39
+ )
40
+
41
+ # Scalar CLI options that map 1:1 onto config keys.
42
+ _SCALAR_FLAGS = (
43
+ "max_retries",
44
+ "backoff_initial",
45
+ "backoff_max",
46
+ "min_workers",
47
+ "error_threshold",
48
+ "safe_branches",
49
+ )
50
+
51
+
52
+ def build_parser():
53
+ """Build the argument parser. Kept separate from main() so it is testable."""
54
+ parser = argparse.ArgumentParser(
55
+ prog="contextlake",
56
+ description="GitLab Workspace Synchronization CLI Tool",
57
+ formatter_class=argparse.RawDescriptionHelpFormatter,
58
+ epilog="""
59
+ Examples:
60
+ contextlake sync # Run full synchronization
61
+ contextlake status # Show status (read-only)
62
+ contextlake --dry-run sync # Show what sync would do, change nothing
63
+ """,
64
+ )
65
+
66
+ parser.add_argument(
67
+ "command",
68
+ choices=[
69
+ "fetch", "clone", "update", "branches", "verify", "sync", "status",
70
+ # one-command turnkey setup (sync + knowledge layer + steering)
71
+ "bootstrap",
72
+ # knowledge layer (optional [kb] extra)
73
+ "index", "connect", "embed", "lint", "wiki", "steer",
74
+ "serve", "query", "doctor",
75
+ ],
76
+ help="Command to execute",
77
+ )
78
+ parser.add_argument("args", nargs="*", help="Positional arguments (e.g. query text)")
79
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
80
+ parser.add_argument("--work-dir", help="Working directory (overrides config file)")
81
+ parser.add_argument("--group", help="GitLab group (overrides config file)")
82
+ parser.add_argument("--config", help="Path to config file (overrides default search paths)")
83
+
84
+ # Knowledge-layer options (used by index/serve/query/doctor)
85
+ kb = parser.add_argument_group("knowledge layer")
86
+ kb.add_argument("--source", help="index: a repo directory or a graph shard JSON")
87
+ kb.add_argument("--workspace", help="index: index every git repo under this directory")
88
+ kb.add_argument("--force", action="store_true",
89
+ help="index: re-index every repo; steer: overwrite non-managed files")
90
+ kb.add_argument("--out", help="steer: directory to write steering files into (default: cwd)")
91
+ kb.add_argument("--kb-config", dest="kb_config",
92
+ help="bootstrap: knowledge-layer config (kb.toml), separate from the sync INI")
93
+ kb.add_argument("--no-sync", dest="no_sync", action="store_true",
94
+ help="bootstrap: skip the GitLab mirror step (index the workspace as-is)")
95
+ kb.add_argument("--no-connect", dest="no_connect", action="store_true",
96
+ help="bootstrap: skip the connectors step")
97
+ kb.add_argument("--no-embed", dest="no_embed", action="store_true",
98
+ help="bootstrap: skip the embeddings step")
99
+ kb.add_argument("--no-wiki", dest="no_wiki", action="store_true",
100
+ help="bootstrap: skip the wiki-generation step")
101
+ kb.add_argument("--watch", action="store_true",
102
+ help="index --workspace: keep re-indexing on an interval (Ctrl-C to stop)")
103
+ kb.add_argument("--interval", type=int,
104
+ help="index --watch: seconds between passes (default 60)")
105
+ kb.add_argument("--transport", choices=["stdio", "http"], help="serve: MCP transport")
106
+ kb.add_argument("--host", help="serve: bind host (http transport)")
107
+ kb.add_argument("--port", type=int, help="serve: bind port (http transport)")
108
+ kb.add_argument("--kind", help="query: filter by node kind")
109
+ kb.add_argument("--repo", help="query: filter by repo")
110
+ kb.add_argument("--limit", type=int, help="query: max results")
111
+ kb.add_argument("--as-of", dest="as_of",
112
+ help="query: search a repo's snapshot at this indexed commit (needs --repo)")
113
+
114
+ parser.add_argument(
115
+ "--dry-run", action="store_true", dest="dry_run",
116
+ help="Show what would happen without cloning, updating, or switching branches",
117
+ )
118
+
119
+ # Logging / verbosity
120
+ parser.add_argument("-v", "--verbose", action="store_true", help="Verbose (debug) output")
121
+ parser.add_argument("-q", "--quiet", action="store_true", help="Only warnings and errors")
122
+ parser.add_argument("--log-file", help="Append a full timestamped log to this file")
123
+
124
+ # Clone / corruption handling
125
+ parser.add_argument(
126
+ "--clean-corrupted", action="store_true", dest="clean_corrupted",
127
+ help="Remove corrupted/incomplete directories before cloning (default: true)",
128
+ )
129
+ parser.add_argument(
130
+ "--no-clean-corrupted", action="store_false", dest="clean_corrupted",
131
+ help="Do not remove corrupted/incomplete directories (fail instead)",
132
+ )
133
+
134
+ # Retry / backoff
135
+ parser.add_argument("--max-retries", type=int, help="Max retry attempts for failed operations")
136
+ parser.add_argument("--backoff-initial", type=float, help="Initial backoff time in seconds")
137
+ parser.add_argument("--backoff-max", type=float, help="Maximum backoff time in seconds")
138
+
139
+ # Adaptive parallelism
140
+ parser.add_argument(
141
+ "--adaptive-workers", action="store_true", dest="adaptive_workers",
142
+ help="Enable adaptive worker pool (default: true)",
143
+ )
144
+ parser.add_argument(
145
+ "--no-adaptive-workers", action="store_false", dest="adaptive_workers",
146
+ help="Disable adaptive worker pool (use static max_workers)",
147
+ )
148
+ parser.add_argument("--min-workers", type=int, help="Minimum workers for the adaptive pool")
149
+ parser.add_argument("--error-threshold", type=float, help="Error rate threshold (0.0-1.0)")
150
+
151
+ # Branch safety
152
+ parser.add_argument(
153
+ "--protect-working-branches", action="store_true", dest="protect_working_branches",
154
+ help="Enable branch protection (default: true)",
155
+ )
156
+ parser.add_argument(
157
+ "--no-protect-working-branches", action="store_false", dest="protect_working_branches",
158
+ help="Disable branch protection (allow operations on any branch)",
159
+ )
160
+ parser.add_argument(
161
+ "--safe-branches",
162
+ help="Comma-separated safe branches (default: main,master,develop,development)",
163
+ )
164
+ parser.add_argument(
165
+ "--require-clean-workspace", action="store_true", dest="require_clean_workspace",
166
+ help="Require clean workspace before operations (default: true)",
167
+ )
168
+ parser.add_argument(
169
+ "--no-require-clean-workspace", action="store_false", dest="require_clean_workspace",
170
+ help="Allow operations with uncommitted changes",
171
+ )
172
+ parser.add_argument(
173
+ "--auto-stash", action="store_true", dest="auto_stash",
174
+ help="Automatically stash changes before operations (default: false)",
175
+ )
176
+ parser.add_argument(
177
+ "--no-auto-stash", action="store_false", dest="auto_stash",
178
+ help="Disable automatic stashing",
179
+ )
180
+
181
+ # Tri-state booleans: unset on the command line -> None -> config wins.
182
+ parser.set_defaults(**{name: None for name in _TRISTATE_FLAGS})
183
+ return parser
184
+
185
+
186
+ def apply_cli_overrides(args, config):
187
+ """Overlay CLI arguments onto a loaded config dict. Returns the same dict.
188
+
189
+ Only values the user actually supplied override the config file; everything
190
+ else is left untouched so config-file (and built-in default) values survive.
191
+ """
192
+ for name in _TRISTATE_FLAGS:
193
+ value = getattr(args, name, None)
194
+ if value is not None:
195
+ config[name] = "true" if value else "false"
196
+
197
+ for name in _SCALAR_FLAGS:
198
+ value = getattr(args, name, None)
199
+ if value is not None:
200
+ config[name] = str(value)
201
+
202
+ return config
203
+
204
+
205
+ def _bootstrap(args, config, work_dir, gitlab_group):
206
+ """One-command turnkey setup: mirror repos, build the knowledge layer, and write
207
+ editor steering. Optional/unconfigured stages are skipped; a failing stage warns
208
+ but never aborts the rest."""
209
+ import copy
210
+
211
+ from . import style
212
+
213
+ def _stage(title):
214
+ log("")
215
+ log(style.bold(style.cyan(f"▶ {title}")))
216
+
217
+ if not getattr(args, "no_sync", False):
218
+ _stage("Mirror repositories from GitLab")
219
+ fetch_gitlab_projects(gitlab_group, config)
220
+ clone_missing_repos(work_dir, config, gitlab_group)
221
+ update_repositories(work_dir, config)
222
+ switch_repository_branches(work_dir, config, gitlab_group)
223
+ verify_structure(work_dir, config, gitlab_group)
224
+ else:
225
+ log("Skipping the GitLab mirror step (--no-sync)")
226
+
227
+ try:
228
+ from .kb import commands as kb
229
+ except ImportError as e:
230
+ log(f"Knowledge layer not installed — skipping index/connect/embed/wiki/steer. "
231
+ f"Install it with: pip install 'contextlake[kb]' ({e})")
232
+ return
233
+
234
+ # kb stages run against the workspace and the *kb* config (kb.toml), which is
235
+ # distinct from the core sync INI passed as --config.
236
+ kb_args = copy.copy(args)
237
+ kb_args.config = getattr(args, "kb_config", None)
238
+ kb_args.workspace = work_dir
239
+ kb_args.source = None
240
+ kb_args.out = work_dir
241
+
242
+ stages = [("Index the code graph", kb.cmd_index)]
243
+ if not getattr(args, "no_connect", False):
244
+ stages.append(("Connect knowledge sources", kb.cmd_connect))
245
+ if not getattr(args, "no_embed", False):
246
+ stages.append(("Build semantic vectors", kb.cmd_embed))
247
+ if not getattr(args, "no_wiki", False):
248
+ stages.append(("Generate the curated wiki", kb.cmd_wiki))
249
+ stages.append(("Write editor steering (.mcp.json, AGENTS.md, …)", kb.cmd_steer))
250
+
251
+ for title, fn in stages:
252
+ _stage(title)
253
+ try:
254
+ fn(kb_args)
255
+ except Exception as e: # noqa: BLE001 - one stage must not abort bootstrap
256
+ log(f" {style.warn(title + ' failed')} — {e}")
257
+
258
+ log("")
259
+ serve = "contextlake serve" + (f" --config {kb_args.config}" if kb_args.config else "")
260
+ log(style.ok(f"Bootstrap complete — workspace ready at {work_dir}."))
261
+ log(f" Editors are wired (.mcp.json + steering). Start the knowledge server: {serve}")
262
+
263
+
264
+ def main(argv=None):
265
+ parser = build_parser()
266
+ args = parser.parse_args(argv)
267
+
268
+ setup_logging(verbose=args.verbose, quiet=args.quiet, log_file=args.log_file)
269
+
270
+ # Knowledge-layer verbs are handled by the optional kb subsystem and don't
271
+ # need the sync config/preamble. Imported lazily so the core tool runs
272
+ # without the [kb] extra.
273
+ if args.command in ("index", "connect", "embed", "lint", "wiki", "steer",
274
+ "serve", "query", "doctor"):
275
+ try:
276
+ from .kb import commands as kb_commands
277
+ except ImportError as e:
278
+ log(f"The '{args.command}' command needs the knowledge-layer extra: "
279
+ f"pip install 'contextlake[kb]' ({e})")
280
+ sys.exit(1)
281
+ sys.exit(kb_commands.dispatch(args.command, args))
282
+
283
+ # Load configuration (honouring an explicit --config path if given), then
284
+ # overlay any CLI overrides on top.
285
+ config = load_config(args.config)
286
+ config = apply_cli_overrides(args, config)
287
+
288
+ work_dir = expand_path(args.work_dir) if args.work_dir else config.get(
289
+ "work_dir", DEFAULT_CONFIG["work_dir"]
290
+ )
291
+ gitlab_group = args.group or config.get("gitlab_group", DEFAULT_CONFIG["gitlab_group"])
292
+
293
+ log(f"Working directory: {work_dir}")
294
+ log(f"GitLab group: {gitlab_group}")
295
+ cache_file, _ = get_cache_paths(config)
296
+ log(f"Cache file: {cache_file}")
297
+ if config.get("dry_run", "false").lower() == "true":
298
+ log("DRY RUN: no repositories will be cloned, updated, or switched")
299
+ log("")
300
+
301
+ try:
302
+ if args.command == "fetch":
303
+ fetch_gitlab_projects(gitlab_group, config)
304
+ elif args.command == "clone":
305
+ clone_missing_repos(work_dir, config, gitlab_group)
306
+ elif args.command == "update":
307
+ update_repositories(work_dir, config)
308
+ elif args.command == "branches":
309
+ switch_repository_branches(work_dir, config, gitlab_group)
310
+ elif args.command == "verify":
311
+ verify_structure(work_dir, config, gitlab_group)
312
+ elif args.command == "sync":
313
+ log("Starting full synchronization...")
314
+ fetch_gitlab_projects(gitlab_group, config)
315
+ clone_missing_repos(work_dir, config, gitlab_group)
316
+ update_repositories(work_dir, config)
317
+ switch_repository_branches(work_dir, config, gitlab_group)
318
+ verify_structure(work_dir, config, gitlab_group)
319
+ log("Full synchronization complete!")
320
+ elif args.command == "status":
321
+ show_status(work_dir, config, gitlab_group)
322
+ elif args.command == "bootstrap":
323
+ _bootstrap(args, config, work_dir, gitlab_group)
324
+ except KeyboardInterrupt:
325
+ log("Operation cancelled by user")
326
+ sys.exit(130)
327
+ except Exception as e: # noqa: BLE001 - top-level guard reports and exits
328
+ log(f"Error: {e}")
329
+ sys.exit(1)
330
+
331
+
332
+ if __name__ == "__main__":
333
+ main()
contextlake/config.py ADDED
@@ -0,0 +1,120 @@
1
+ """
2
+ Configuration loading for contextlake
3
+ """
4
+
5
+ import configparser
6
+ import os
7
+
8
+ from .logging_setup import log
9
+
10
+ # Configuration file paths. The current (contextlake) files take precedence, but
11
+ # the former gitlab-sync files are still read so existing setups keep working
12
+ # without any change after the rename.
13
+ CONFIG_FILE = os.path.expanduser('~/.contextlake.ini')
14
+ LOCAL_CONFIG_FILE = '.contextlake.ini'
15
+ LEGACY_CONFIG_FILE = os.path.expanduser('~/.gitlab_sync.ini')
16
+ LEGACY_LOCAL_CONFIG_FILE = '.gitlab_sync.ini'
17
+
18
+ # INI section names, low-to-high precedence: the current section wins if a file
19
+ # happens to carry both.
20
+ SECTIONS = ('gitlab_sync', 'contextlake')
21
+
22
+ # Config values that name a filesystem location and so must have ~ and $VARS
23
+ # expanded (the INI/CLI layers store them verbatim, unlike DEFAULT_CONFIG).
24
+ PATH_KEYS = ('work_dir', 'cache_dir')
25
+
26
+
27
+ def expand_path(value):
28
+ """Expand ~ and environment variables in a path-like config value."""
29
+ return os.path.expanduser(os.path.expandvars(value)) if value else value
30
+
31
+ # Default Configuration
32
+ DEFAULT_CONFIG = {
33
+ 'work_dir': os.path.expanduser('~/work'),
34
+ 'gitlab_group': 'your-gitlab-group',
35
+ 'cache_dir': '/tmp',
36
+ 'cache_file': 'gitlab_projects.txt',
37
+ 'cache_json': 'gitlab_projects.json',
38
+ 'clone_timeout': '300',
39
+ 'fetch_timeout': '60',
40
+ 'branch_timeout': '30',
41
+ 'pull_timeout': '60',
42
+ 'max_workers': '8',
43
+ 'clone_method': 'auto', # auto -> prefer glab (uses its auth), else git over HTTPS
44
+ 'branch_strategy': 'hybrid', # most-active selection: commits | recency | hybrid
45
+ 'clean_corrupted': 'true',
46
+ 'max_retries': '3',
47
+ 'backoff_initial': '1',
48
+ 'backoff_max': '30',
49
+ 'adaptive_workers': 'true',
50
+ 'min_workers': '2',
51
+ 'error_threshold': '0.5',
52
+ 'protect_working_branches': 'true',
53
+ 'safe_branches': 'main,master,develop,development',
54
+ 'require_clean_workspace': 'true',
55
+ 'auto_stash': 'false'
56
+ }
57
+
58
+
59
+ def _merge(config, path):
60
+ """Merge an INI file's config section into config, if present.
61
+
62
+ Accepts either the current ``[contextlake]`` section or the legacy
63
+ ``[gitlab_sync]`` one (current wins if both are present in one file).
64
+ """
65
+ if not path or not os.path.exists(path):
66
+ return
67
+ parser = configparser.ConfigParser()
68
+ parser.read(path)
69
+ for section in SECTIONS:
70
+ if section in parser:
71
+ config.update(parser[section])
72
+
73
+
74
+ def load_config(config_path=None):
75
+ """Load configuration with precedence: explicit --config > local > global > defaults.
76
+
77
+ Sources are merged from lowest to highest precedence so the later (more
78
+ specific) source wins on conflicting keys. The legacy gitlab-sync files are
79
+ read just below their contextlake counterparts, so an existing setup keeps
80
+ working while a new contextlake file (if present) takes precedence.
81
+ """
82
+ config = DEFAULT_CONFIG.copy()
83
+ _merge(config, LEGACY_CONFIG_FILE) # legacy global (~/.gitlab_sync.ini)
84
+ _merge(config, CONFIG_FILE) # global (~/.contextlake.ini)
85
+ _merge(config, LEGACY_LOCAL_CONFIG_FILE) # legacy local workspace config
86
+ _merge(config, LOCAL_CONFIG_FILE) # local workspace config
87
+ _merge(config, config_path) # explicit --config path
88
+
89
+ # INI/CLI values are stored verbatim, so a `work_dir = ~/repos` would
90
+ # otherwise be treated as a literal "~" directory. Expand here.
91
+ for key in PATH_KEYS:
92
+ if key in config:
93
+ config[key] = expand_path(config[key])
94
+
95
+ if config.get('gitlab_group') == DEFAULT_CONFIG['gitlab_group']:
96
+ # No usable config was found. The local files are resolved against the
97
+ # CURRENT directory, which trips people up when the config lives next to
98
+ # the example in the repo but the command is run from elsewhere — so show
99
+ # the exact paths searched (absolute) and whether each exists.
100
+ log("WARNING: gitlab_group is still the placeholder 'your-gitlab-group' — "
101
+ "no config with your group was found. Searched (low to high precedence):")
102
+ for path in (LEGACY_CONFIG_FILE, CONFIG_FILE, LEGACY_LOCAL_CONFIG_FILE,
103
+ LOCAL_CONFIG_FILE, config_path):
104
+ if not path:
105
+ continue
106
+ mark = "found" if os.path.exists(path) else "absent"
107
+ log(f" [{mark}] {os.path.abspath(path)}")
108
+ log(" Local files (.contextlake.ini) are read from the CURRENT directory. "
109
+ "Copy .contextlake.ini.example to one of the paths above (or pass "
110
+ "--config PATH) and set gitlab_group.")
111
+
112
+ return config
113
+
114
+
115
+ def get_cache_paths(config):
116
+ """Get cache file paths from config."""
117
+ cache_dir = config.get('cache_dir', '/tmp')
118
+ cache_file = config.get('cache_file', 'gitlab_projects.txt')
119
+ cache_json = config.get('cache_json', 'gitlab_projects.json')
120
+ return os.path.join(cache_dir, cache_file), os.path.join(cache_dir, cache_json)