caffeinated-whale-cli 3.2.0__tar.gz → 3.3.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.
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/PKG-INFO +27 -2
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/README.md +26 -1
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/pyproject.toml +1 -1
- caffeinated_whale_cli-3.3.0/src/caffeinated_whale_cli/__init__.py +1 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/config.py +49 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/auto_inspect.py +8 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/config.py +35 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/inspect.py +12 -1
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/supervision.py +110 -9
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/where.py +18 -1
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/cache.py +6 -1
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/completion_utils.py +13 -1
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/config_utils.py +60 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/db_utils.py +75 -14
- caffeinated_whale_cli-3.2.0/src/caffeinated_whale_cli/__init__.py +0 -1
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/LICENSE +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/setup.cfg +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/__main__.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/__init__.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/apps.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/axi.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/backup.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/console.html +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/doctor.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/init.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/inspect.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/label.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/list.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/logs.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/open.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/restart.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/restore.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/rm.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/rm_bench.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/rm_site.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/run.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/scale.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/self_update.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/serve.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/setup.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/start.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/status.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/stop.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/unlock.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/update.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/utils.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/where.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/__init__.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/apps.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/backup.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/bench_ops.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/cred_bridge.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/credbridge.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/docker.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/doctor.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/envelope.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/errors.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/exec_stream.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/fleet.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/init.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/label.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/list.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/logs.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/open.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/resolvers.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/restart.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/restore.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/rm.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/rm_bench.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/rm_site.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/run.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/scale.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/setup.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/start.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/status.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/stop.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/unlock.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/update.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/url.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/version.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/main.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/update_notice.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/__init__.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/agent_hooks.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/auto_inspect.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/bench_labels.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/bench_sites.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/console.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/cred_daemon.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/daemon_identity.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/docker_utils.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/port_utils.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/sendme_utils.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/shared_home.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/signals.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/startup.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/tips.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/toon.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/vscode_utils.py +0 -0
- {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli.egg-info/SOURCES.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: caffeinated-whale-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: A CLI tool to help manage Frappe Docker instances.
|
|
5
5
|
Author-email: Christopher McKay <mckay.christopher73@outlook.com>
|
|
6
6
|
License: MIT
|
|
@@ -2027,7 +2027,7 @@ cwcli config [SUBCOMMAND]
|
|
|
2027
2027
|
|
|
2028
2028
|
##### `config show` - Show the Effective Configuration
|
|
2029
2029
|
|
|
2030
|
-
The one read for everything: the custom search paths, the auto-inspect settings together with live daemon state and boot-hook state, the credential-bridge state (enabled, daemon, transport, wired instances), the tips setting, and the config-file and cache-DB locations.
|
|
2030
|
+
The one read for everything: the cache on/off state (and whether `CWCLI_NO_CACHE` is overriding it), the custom search paths, the auto-inspect settings together with live daemon state and boot-hook state, the credential-bridge state (enabled, daemon, transport, wired instances), the tips setting, and the config-file and cache-DB locations.
|
|
2031
2031
|
|
|
2032
2032
|
```bash
|
|
2033
2033
|
cwcli config show
|
|
@@ -2095,6 +2095,31 @@ cwcli config cache [SUBCOMMAND]
|
|
|
2095
2095
|
- **`list [--json]`** - List all projects currently in the cache
|
|
2096
2096
|
- Example: `cwcli config cache list --json`
|
|
2097
2097
|
|
|
2098
|
+
- **`disable`** / **`enable`** - Turn the on-disk cache off entirely, or back on (the default)
|
|
2099
|
+
- Example: `cwcli config cache disable`
|
|
2100
|
+
- Example: `cwcli config cache enable`
|
|
2101
|
+
- See ["Disabling caching entirely"](#disabling-caching-entirely) below.
|
|
2102
|
+
|
|
2103
|
+
###### Disabling caching entirely
|
|
2104
|
+
|
|
2105
|
+
cwcli keeps an on-disk SQLite cache (under `~/.cwcli/cache`, or `$CWCLI_HOME/cache`) that read commands - `inspect`, `status`, `where`, `open`, the `apps` group, tab completion, and every `axi` read verb - work from, so they do not re-inspect a running instance every time.
|
|
2106
|
+
|
|
2107
|
+
You can turn it off entirely, in two equivalent ways:
|
|
2108
|
+
|
|
2109
|
+
- **Config key:** `cwcli config cache disable` sets `[cache] enabled = false` in the config file. `cwcli config cache enable` reverts it.
|
|
2110
|
+
- **Environment variable:** `CWCLI_NO_CACHE=1` disables caching for a single shell or CI job; `CWCLI_NO_CACHE=0` re-enables it. The environment variable **overrides** the config key, so you can force caching on or off for one invocation regardless of the persisted setting.
|
|
2111
|
+
|
|
2112
|
+
With caching disabled:
|
|
2113
|
+
|
|
2114
|
+
- Every read resolves **live**, as if a fresh `inspect` had just run - a stale cached row can never be served.
|
|
2115
|
+
- **Nothing** is written to or read from the cache DB, and a pre-existing cache file is left in place untouched, so re-enabling restores the previous behaviour.
|
|
2116
|
+
- Tab completion degrades gracefully to a live/empty lookup rather than serving stale names.
|
|
2117
|
+
- `auto-inspect` refuses to start (it exists only to keep the cache warm, which does nothing when caching is off).
|
|
2118
|
+
- Bench labels still work, because they are also stored in each bench's marker file.
|
|
2119
|
+
- Safety-critical commands (`rm`/`rm-site`'s backup step, `apps checkout`'s wrong-copy detection) get correct live data.
|
|
2120
|
+
|
|
2121
|
+
Because every read resolves live, commands are slower than with the cache warm - this is a correctness/isolation mode, not the everyday default.
|
|
2122
|
+
|
|
2098
2123
|
##### `config auto-inspect` - Automatic Project Inspection
|
|
2099
2124
|
|
|
2100
2125
|
Manages automatic background inspection of running Frappe projects to keep cached data fresh (tab completion, project status queries, and other commands that rely on cached data).
|
|
@@ -1987,7 +1987,7 @@ cwcli config [SUBCOMMAND]
|
|
|
1987
1987
|
|
|
1988
1988
|
##### `config show` - Show the Effective Configuration
|
|
1989
1989
|
|
|
1990
|
-
The one read for everything: the custom search paths, the auto-inspect settings together with live daemon state and boot-hook state, the credential-bridge state (enabled, daemon, transport, wired instances), the tips setting, and the config-file and cache-DB locations.
|
|
1990
|
+
The one read for everything: the cache on/off state (and whether `CWCLI_NO_CACHE` is overriding it), the custom search paths, the auto-inspect settings together with live daemon state and boot-hook state, the credential-bridge state (enabled, daemon, transport, wired instances), the tips setting, and the config-file and cache-DB locations.
|
|
1991
1991
|
|
|
1992
1992
|
```bash
|
|
1993
1993
|
cwcli config show
|
|
@@ -2055,6 +2055,31 @@ cwcli config cache [SUBCOMMAND]
|
|
|
2055
2055
|
- **`list [--json]`** - List all projects currently in the cache
|
|
2056
2056
|
- Example: `cwcli config cache list --json`
|
|
2057
2057
|
|
|
2058
|
+
- **`disable`** / **`enable`** - Turn the on-disk cache off entirely, or back on (the default)
|
|
2059
|
+
- Example: `cwcli config cache disable`
|
|
2060
|
+
- Example: `cwcli config cache enable`
|
|
2061
|
+
- See ["Disabling caching entirely"](#disabling-caching-entirely) below.
|
|
2062
|
+
|
|
2063
|
+
###### Disabling caching entirely
|
|
2064
|
+
|
|
2065
|
+
cwcli keeps an on-disk SQLite cache (under `~/.cwcli/cache`, or `$CWCLI_HOME/cache`) that read commands - `inspect`, `status`, `where`, `open`, the `apps` group, tab completion, and every `axi` read verb - work from, so they do not re-inspect a running instance every time.
|
|
2066
|
+
|
|
2067
|
+
You can turn it off entirely, in two equivalent ways:
|
|
2068
|
+
|
|
2069
|
+
- **Config key:** `cwcli config cache disable` sets `[cache] enabled = false` in the config file. `cwcli config cache enable` reverts it.
|
|
2070
|
+
- **Environment variable:** `CWCLI_NO_CACHE=1` disables caching for a single shell or CI job; `CWCLI_NO_CACHE=0` re-enables it. The environment variable **overrides** the config key, so you can force caching on or off for one invocation regardless of the persisted setting.
|
|
2071
|
+
|
|
2072
|
+
With caching disabled:
|
|
2073
|
+
|
|
2074
|
+
- Every read resolves **live**, as if a fresh `inspect` had just run - a stale cached row can never be served.
|
|
2075
|
+
- **Nothing** is written to or read from the cache DB, and a pre-existing cache file is left in place untouched, so re-enabling restores the previous behaviour.
|
|
2076
|
+
- Tab completion degrades gracefully to a live/empty lookup rather than serving stale names.
|
|
2077
|
+
- `auto-inspect` refuses to start (it exists only to keep the cache warm, which does nothing when caching is off).
|
|
2078
|
+
- Bench labels still work, because they are also stored in each bench's marker file.
|
|
2079
|
+
- Safety-critical commands (`rm`/`rm-site`'s backup step, `apps checkout`'s wrong-copy detection) get correct live data.
|
|
2080
|
+
|
|
2081
|
+
Because every read resolves live, commands are slower than with the cache warm - this is a correctness/isolation mode, not the everyday default.
|
|
2082
|
+
|
|
2058
2083
|
##### `config auto-inspect` - Automatic Project Inspection
|
|
2059
2084
|
|
|
2060
2085
|
Manages automatic background inspection of running Frappe projects to keep cached data fresh (tab completion, project status queries, and other commands that rely on cached data).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.3.0"
|
|
@@ -101,6 +101,17 @@ def show(
|
|
|
101
101
|
console.print("[bold]Locations[/bold]")
|
|
102
102
|
console.print(f" Config file: [green]{report.config_file}[/green]")
|
|
103
103
|
console.print(f" Cache DB: [green]{report.cache_db}[/green]")
|
|
104
|
+
|
|
105
|
+
console.print("\n[bold]Cache[/bold]")
|
|
106
|
+
if report.cache_enabled:
|
|
107
|
+
console.print(" Enabled: [green]Yes[/green]")
|
|
108
|
+
else:
|
|
109
|
+
source = "CWCLI_NO_CACHE" if report.cache_env_override else "config"
|
|
110
|
+
console.print(f" Enabled: [red]No[/red] [dim](disabled via {source})[/dim]")
|
|
111
|
+
console.print(" [dim]Reads resolve live; nothing is written to the cache DB.[/dim]")
|
|
112
|
+
if report.cache_env_override:
|
|
113
|
+
console.print(" [dim]CWCLI_NO_CACHE is set and overrides the config key.[/dim]")
|
|
114
|
+
|
|
104
115
|
console.print("\n[bold]Search paths[/bold]")
|
|
105
116
|
if report.search_paths:
|
|
106
117
|
for path in report.search_paths:
|
|
@@ -351,6 +362,44 @@ def clear_cache(
|
|
|
351
362
|
)
|
|
352
363
|
|
|
353
364
|
|
|
365
|
+
@cache_app.command("enable")
|
|
366
|
+
def cache_enable():
|
|
367
|
+
"""
|
|
368
|
+
Turn the on-disk cache back on (the default).
|
|
369
|
+
"""
|
|
370
|
+
_apply_cache_setting(True)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@cache_app.command("disable")
|
|
374
|
+
def cache_disable():
|
|
375
|
+
"""
|
|
376
|
+
Turn the on-disk cache off entirely.
|
|
377
|
+
|
|
378
|
+
Every read command then resolves live (as if a fresh inspect ran) and nothing
|
|
379
|
+
is written to or read from the cache DB. The existing cache file is left in
|
|
380
|
+
place, so re-enabling restores it. Set CWCLI_NO_CACHE=1 for the same effect
|
|
381
|
+
per shell / in CI.
|
|
382
|
+
"""
|
|
383
|
+
_apply_cache_setting(False)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _apply_cache_setting(enabled: bool):
|
|
387
|
+
state = core_config.set_cache(enabled).data
|
|
388
|
+
assert state is not None
|
|
389
|
+
if enabled:
|
|
390
|
+
console.print("[green]Caching enabled.[/green] Reads use the on-disk cache.")
|
|
391
|
+
else:
|
|
392
|
+
console.print(
|
|
393
|
+
"[yellow]Caching disabled.[/yellow] Reads resolve live; nothing is written "
|
|
394
|
+
"to the cache DB (the existing cache is left in place)."
|
|
395
|
+
)
|
|
396
|
+
if state.env_override and state.enabled != enabled:
|
|
397
|
+
console.print(
|
|
398
|
+
"[yellow]Note:[/yellow] CWCLI_NO_CACHE is set and overrides this setting; "
|
|
399
|
+
f"caching is currently {'enabled' if state.enabled else 'disabled'} for this shell."
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
354
403
|
@cache_app.command("path")
|
|
355
404
|
def cache_path():
|
|
356
405
|
"""
|
|
@@ -160,6 +160,14 @@ def enable(interval: int | None = None, at_boot: bool | None = None) -> Result[A
|
|
|
160
160
|
it when a changed interval must take effect), then syncs the boot hook when
|
|
161
161
|
``at_boot`` was requested (``None`` leaves the hook untouched).
|
|
162
162
|
"""
|
|
163
|
+
if config_utils.cache_disabled():
|
|
164
|
+
raise CwcliError(
|
|
165
|
+
ErrorKind.USAGE,
|
|
166
|
+
"auto_inspect.cache_disabled",
|
|
167
|
+
"Caching is disabled, so auto-inspect has nothing to keep warm.",
|
|
168
|
+
hint="Re-enable caching first: `cwcli config cache enable` (or unset CWCLI_NO_CACHE).",
|
|
169
|
+
)
|
|
170
|
+
|
|
163
171
|
if interval is not None and interval < MIN_INTERVAL:
|
|
164
172
|
raise CwcliError(
|
|
165
173
|
ErrorKind.USAGE,
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/config.py
RENAMED
|
@@ -18,6 +18,7 @@ cannot be checked, nothing is pruned.
|
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
+
import os
|
|
21
22
|
import posixpath
|
|
22
23
|
from dataclasses import dataclass, field
|
|
23
24
|
|
|
@@ -38,12 +39,22 @@ class ConfigReport:
|
|
|
38
39
|
|
|
39
40
|
config_file: str
|
|
40
41
|
cache_db: str
|
|
42
|
+
cache_enabled: bool
|
|
43
|
+
cache_env_override: bool
|
|
41
44
|
search_paths: list[str]
|
|
42
45
|
auto_inspect: AutoInspectState
|
|
43
46
|
cred_bridge: CredBridgeState
|
|
44
47
|
show_tips: bool
|
|
45
48
|
|
|
46
49
|
|
|
50
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
51
|
+
class CacheState:
|
|
52
|
+
"""Whether the on-disk cache is on, and whether an env var is forcing it."""
|
|
53
|
+
|
|
54
|
+
enabled: bool
|
|
55
|
+
env_override: bool # True when CWCLI_NO_CACHE is set (config key is inert)
|
|
56
|
+
|
|
57
|
+
|
|
47
58
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
48
59
|
class SearchPathChange:
|
|
49
60
|
"""A search-path add/remove: the normalized path and whether anything changed."""
|
|
@@ -79,6 +90,12 @@ def _normalize(path: str) -> str:
|
|
|
79
90
|
return posixpath.normpath(posixpath.expanduser(path))
|
|
80
91
|
|
|
81
92
|
|
|
93
|
+
def _cache_env_override() -> bool:
|
|
94
|
+
"""True when CWCLI_NO_CACHE is set to a non-empty value (it wins over config)."""
|
|
95
|
+
env = os.environ.get("CWCLI_NO_CACHE")
|
|
96
|
+
return env is not None and env.strip() != ""
|
|
97
|
+
|
|
98
|
+
|
|
82
99
|
def show_config() -> Result[ConfigReport]:
|
|
83
100
|
"""The effective config as one report: paths, auto-inspect, tips, locations."""
|
|
84
101
|
state = core_auto_inspect.status().data
|
|
@@ -91,6 +108,8 @@ def show_config() -> Result[ConfigReport]:
|
|
|
91
108
|
data=ConfigReport(
|
|
92
109
|
config_file=str(config_utils.CONFIG_FILE),
|
|
93
110
|
cache_db=str(db_utils.DB_PATH),
|
|
111
|
+
cache_enabled=not config_utils.cache_disabled(),
|
|
112
|
+
cache_env_override=_cache_env_override(),
|
|
94
113
|
search_paths=list(config["search_paths"]["custom_bench_paths"]),
|
|
95
114
|
auto_inspect=state,
|
|
96
115
|
cred_bridge=bridge_state,
|
|
@@ -99,6 +118,22 @@ def show_config() -> Result[ConfigReport]:
|
|
|
99
118
|
)
|
|
100
119
|
|
|
101
120
|
|
|
121
|
+
def set_cache(enabled: bool) -> Result[CacheState]:
|
|
122
|
+
"""Turn the on-disk cache on or off via the ``[cache] enabled`` config key.
|
|
123
|
+
|
|
124
|
+
The cache DB is never deleted or altered here - it is left in place so
|
|
125
|
+
re-enabling restores the previous behaviour. When ``CWCLI_NO_CACHE`` is set it
|
|
126
|
+
overrides this key, so the returned :class:`CacheState` reports that override.
|
|
127
|
+
"""
|
|
128
|
+
config_utils.set_cache_enabled(enabled)
|
|
129
|
+
return Result(
|
|
130
|
+
status=Status.OK,
|
|
131
|
+
data=CacheState(
|
|
132
|
+
enabled=not config_utils.cache_disabled(), env_override=_cache_env_override()
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
102
137
|
def add_search_path(path: str) -> Result[SearchPathChange]:
|
|
103
138
|
"""Add a bench search path: refuse non-absolute, normalize before dedup.
|
|
104
139
|
|
|
@@ -581,6 +581,14 @@ def inspect_raw(
|
|
|
581
581
|
f"Unknown refresh mode '{refresh}'. Use one of: {', '.join(_REFRESH_MODES)}.",
|
|
582
582
|
)
|
|
583
583
|
|
|
584
|
+
# Caching off: always resolve live. Force the full tier so no cached row (T1)
|
|
585
|
+
# or partial refresh (T2) can be served, and the write below lands in the
|
|
586
|
+
# ephemeral in-memory store db_utils redirects to (the on-disk cache is never
|
|
587
|
+
# touched). This is also the recursion base for db_utils._live_populate: a
|
|
588
|
+
# forced-full run with no selector never reads the cache back.
|
|
589
|
+
if config_utils.cache_disabled():
|
|
590
|
+
refresh = "full"
|
|
591
|
+
|
|
584
592
|
emit = on_event or _noop
|
|
585
593
|
warnings: list[Message] = []
|
|
586
594
|
|
|
@@ -592,7 +600,10 @@ def inspect_raw(
|
|
|
592
600
|
served_from = "cache"
|
|
593
601
|
|
|
594
602
|
cached_data = None
|
|
595
|
-
|
|
603
|
+
# With caching disabled there is nothing to read and the read would only
|
|
604
|
+
# trigger a redundant nested live populate; the forced-full path below does a
|
|
605
|
+
# fresh discovery and narrows the selector from that.
|
|
606
|
+
if (refresh != "full" or bench is not None) and not config_utils.cache_disabled():
|
|
596
607
|
cached_data = db_utils.get_cached_project_data(project_name)
|
|
597
608
|
|
|
598
609
|
if refresh != "full":
|
|
@@ -51,6 +51,7 @@ never returned across a boundary; every return is plain serializable data.
|
|
|
51
51
|
from __future__ import annotations
|
|
52
52
|
|
|
53
53
|
import json
|
|
54
|
+
import re
|
|
54
55
|
import shlex
|
|
55
56
|
import time
|
|
56
57
|
from collections.abc import Callable
|
|
@@ -1230,6 +1231,89 @@ def _resync_failed(text: str, restarted: list[str]) -> ResyncOutcome:
|
|
|
1230
1231
|
return ResyncOutcome(attempted=True, restarted=list(restarted), unserved_sites=[], error=text)
|
|
1231
1232
|
|
|
1232
1233
|
|
|
1234
|
+
# A bench's ``common_site_config.json`` MAY legitimately omit ``webserver_port`` -
|
|
1235
|
+
# a DNS-multitenant bench (many domain-named sites) removes it deliberately so
|
|
1236
|
+
# Frappe routes purely by Host header, not by a port cwcli would otherwise read as
|
|
1237
|
+
# a fixed default. `resolve_assigned_ports(fill_defaults=False)` reports that the
|
|
1238
|
+
# same way it reports a genuinely unreadable config: absent. Below, "the config has
|
|
1239
|
+
# no port" is answered with EVIDENCE instead - the live web process's own bound
|
|
1240
|
+
# port, from its argv - never a bare guess.
|
|
1241
|
+
_PORT_FLAG_RE = re.compile(r"--port[=\s]+(\d+)")
|
|
1242
|
+
_BIND_FLAG_RE = re.compile(r"(?:--bind|-b)[=\s]+\S*:(\d+)")
|
|
1243
|
+
_LISTENER_OWNERSHIP_SCRIPT = r"""
|
|
1244
|
+
import os
|
|
1245
|
+
import sys
|
|
1246
|
+
|
|
1247
|
+
pid = int(sys.argv[1])
|
|
1248
|
+
port = int(sys.argv[2])
|
|
1249
|
+
bench = os.path.realpath(sys.argv[3])
|
|
1250
|
+
try:
|
|
1251
|
+
cwd = os.path.realpath(f"/proc/{pid}/cwd")
|
|
1252
|
+
if os.path.commonpath((cwd, bench)) != bench:
|
|
1253
|
+
raise OSError
|
|
1254
|
+
inodes = set()
|
|
1255
|
+
for fd in os.listdir(f"/proc/{pid}/fd"):
|
|
1256
|
+
try:
|
|
1257
|
+
target = os.readlink(f"/proc/{pid}/fd/{fd}")
|
|
1258
|
+
except OSError:
|
|
1259
|
+
continue
|
|
1260
|
+
if target.startswith("socket:[") and target.endswith("]"):
|
|
1261
|
+
inodes.add(target[8:-1])
|
|
1262
|
+
for table in ("/proc/net/tcp", "/proc/net/tcp6"):
|
|
1263
|
+
with open(table, encoding="ascii") as rows:
|
|
1264
|
+
next(rows)
|
|
1265
|
+
for row in rows:
|
|
1266
|
+
fields = row.split()
|
|
1267
|
+
local_port = int(fields[1].rsplit(":", 1)[1], 16)
|
|
1268
|
+
if fields[3] == "0A" and local_port == port and fields[9] in inodes:
|
|
1269
|
+
raise SystemExit(0)
|
|
1270
|
+
except (OSError, ValueError):
|
|
1271
|
+
raise SystemExit(2)
|
|
1272
|
+
raise SystemExit(1)
|
|
1273
|
+
""".strip()
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def _port_from_process(container, pid: int) -> int | None:
|
|
1277
|
+
"""The bound port of ``pid``, read from its own argv - evidence, not a guess.
|
|
1278
|
+
|
|
1279
|
+
bench bakes the bench's real assigned port into the Procfile's ``web:`` line at
|
|
1280
|
+
init (``bench serve --port <N>``), so once that line is running its cmdline IS
|
|
1281
|
+
the truth; a gunicorn-fronted web instead carries ``--bind``/``-b <host>:<N>``.
|
|
1282
|
+
Returns None when the process is gone or its argv names no port at all (a bare
|
|
1283
|
+
``bench serve`` with no ``--port``, which defers to Frappe's own default).
|
|
1284
|
+
"""
|
|
1285
|
+
for row in _ps_rows(container):
|
|
1286
|
+
if row.pid != pid:
|
|
1287
|
+
continue
|
|
1288
|
+
match = _PORT_FLAG_RE.search(row.args) or _BIND_FLAG_RE.search(row.args)
|
|
1289
|
+
return int(match.group(1)) if match else None
|
|
1290
|
+
return None
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
def _process_owns_listener(container, pid: int, port: int, bench_path: str) -> bool:
|
|
1294
|
+
"""Return whether this bench's process owns the listener on ``port``."""
|
|
1295
|
+
exit_code, _output = container.exec_run(
|
|
1296
|
+
["python3", "-c", _LISTENER_OWNERSHIP_SCRIPT, str(pid), str(port), bench_path]
|
|
1297
|
+
)
|
|
1298
|
+
return bool(exit_code == 0)
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
def _resync_web_port(container, bench_path: str, web_pid: int | None) -> int | None:
|
|
1302
|
+
ports = resolvers.resolve_assigned_ports(container, [bench_path], fill_defaults=False).get(
|
|
1303
|
+
bench_path
|
|
1304
|
+
)
|
|
1305
|
+
if ports is not None:
|
|
1306
|
+
return ports[0]
|
|
1307
|
+
process_port = _port_from_process(container, web_pid) if web_pid is not None else None
|
|
1308
|
+
if process_port is not None:
|
|
1309
|
+
return process_port
|
|
1310
|
+
if web_pid is not None and _process_owns_listener(
|
|
1311
|
+
container, web_pid, resolvers.WEB_CONTAINER_BASE, bench_path
|
|
1312
|
+
):
|
|
1313
|
+
return resolvers.WEB_CONTAINER_BASE
|
|
1314
|
+
return None
|
|
1315
|
+
|
|
1316
|
+
|
|
1233
1317
|
def resync_after_code_change(
|
|
1234
1318
|
container,
|
|
1235
1319
|
bench_path: str,
|
|
@@ -1280,6 +1364,18 @@ def resync_after_code_change(
|
|
|
1280
1364
|
bound-port-shaped claim, and serving stale code is exactly the class of defect a
|
|
1281
1365
|
claim like that misses. Every site the caller names must answer a real
|
|
1282
1366
|
site-routed request before the outcome is ``ok``.
|
|
1367
|
+
- **An absent ``webserver_port`` is not a read failure.** A DNS-multitenant
|
|
1368
|
+
bench (many domain-named sites) deliberately removes it from
|
|
1369
|
+
``common_site_config.json`` so Frappe routes purely by Host header; a bench
|
|
1370
|
+
like that used to fail here with "the bench's assigned port could not be
|
|
1371
|
+
read", even though the update itself had already succeeded. The probe port
|
|
1372
|
+
now falls back to EVIDENCE - the live web process's own bound port (its argv
|
|
1373
|
+
carries bench's real assignment, baked into the Procfile's ``web:`` line at
|
|
1374
|
+
init), then Frappe's own unconfigured default only when that listener is
|
|
1375
|
+
proven to belong to this bench's web process - before the sites are probed
|
|
1376
|
+
for real. Missing evidence fails honestly without probing a sibling bench or
|
|
1377
|
+
blaming a config key that was never supposed to exist.
|
|
1378
|
+
See :func:`_port_from_process`.
|
|
1283
1379
|
"""
|
|
1284
1380
|
announce = on_restart or (lambda _program: None)
|
|
1285
1381
|
unique_sites = list(dict.fromkeys(sites))
|
|
@@ -1290,6 +1386,7 @@ def resync_after_code_change(
|
|
|
1290
1386
|
|
|
1291
1387
|
restarted: list[str] = []
|
|
1292
1388
|
supervised = False
|
|
1389
|
+
web_port: int | None
|
|
1293
1390
|
try:
|
|
1294
1391
|
supervised = discover_stack(container, bench_path, required=True).supervisor_up
|
|
1295
1392
|
if supervised:
|
|
@@ -1304,6 +1401,7 @@ def resync_after_code_change(
|
|
|
1304
1401
|
# is no site to verify against, and starting one is not this verb's job.
|
|
1305
1402
|
if states[web][0] not in _LIVE_STATES:
|
|
1306
1403
|
return quiet
|
|
1404
|
+
web_port = _resync_web_port(container, bench_path, states[web][1])
|
|
1307
1405
|
|
|
1308
1406
|
for program in code_bearing_programs(list(states)):
|
|
1309
1407
|
if states[program][0] not in _LIVE_STATES:
|
|
@@ -1320,21 +1418,24 @@ def resync_after_code_change(
|
|
|
1320
1418
|
restarted,
|
|
1321
1419
|
)
|
|
1322
1420
|
restarted.append(program)
|
|
1323
|
-
|
|
1324
|
-
|
|
1421
|
+
else:
|
|
1422
|
+
unsupervised = discover_unsupervised_stack(container, bench_path, required=True)
|
|
1423
|
+
if not unsupervised.manager_up:
|
|
1424
|
+
return quiet
|
|
1425
|
+
web_process = next((p for p in unsupervised.processes if p.label == "web"), None)
|
|
1426
|
+
web_port = _resync_web_port(
|
|
1427
|
+
container, bench_path, web_process.pid if web_process is not None else None
|
|
1428
|
+
)
|
|
1325
1429
|
|
|
1326
|
-
|
|
1327
|
-
bench_path
|
|
1328
|
-
)
|
|
1329
|
-
if ports is None:
|
|
1430
|
+
if web_port is None:
|
|
1330
1431
|
return _resync_failed(
|
|
1331
|
-
"the bench's
|
|
1332
|
-
"the affected sites serve",
|
|
1432
|
+
"the bench's serving port could not be determined safely, so cwcli could not "
|
|
1433
|
+
"confirm the affected sites serve",
|
|
1333
1434
|
restarted,
|
|
1334
1435
|
)
|
|
1335
1436
|
|
|
1336
1437
|
pending, codes = _wait_for_serving_sites(
|
|
1337
|
-
container, port=
|
|
1438
|
+
container, port=web_port, sites=unique_sites, timeout=timeout
|
|
1338
1439
|
)
|
|
1339
1440
|
if pending:
|
|
1340
1441
|
observed = ", ".join(f"{site}={codes[site] or 'unreachable'}" for site in pending)
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/where.py
RENAMED
|
@@ -31,7 +31,7 @@ from dataclasses import dataclass, field, replace
|
|
|
31
31
|
|
|
32
32
|
import peewee
|
|
33
33
|
|
|
34
|
-
from ..utils import db_utils
|
|
34
|
+
from ..utils import config_utils, db_utils
|
|
35
35
|
from .envelope import Message, Result, Status
|
|
36
36
|
from .errors import CwcliError, ErrorKind
|
|
37
37
|
from .list import list_instances
|
|
@@ -177,6 +177,21 @@ def _live_project_names() -> set[str] | None:
|
|
|
177
177
|
return {instance.project_name for instance in (result.data or [])}
|
|
178
178
|
|
|
179
179
|
|
|
180
|
+
def _populate_live_if_disabled() -> None:
|
|
181
|
+
"""With caching off, ``where`` has no cache to search, so inspect every live
|
|
182
|
+
instance into the ephemeral in-memory store first, then search that.
|
|
183
|
+
|
|
184
|
+
Reuses the same live-populate ``db_utils`` runs for a single project (one full
|
|
185
|
+
inspect per running instance); the on-disk cache is never touched. A daemon we
|
|
186
|
+
cannot reach leaves the store empty, which ``where`` reports as no matches.
|
|
187
|
+
"""
|
|
188
|
+
if not config_utils.cache_disabled():
|
|
189
|
+
return
|
|
190
|
+
live = _live_project_names()
|
|
191
|
+
for project_name in live or set():
|
|
192
|
+
db_utils.get_cached_project_data(project_name)
|
|
193
|
+
|
|
194
|
+
|
|
180
195
|
def where(
|
|
181
196
|
search: str,
|
|
182
197
|
*,
|
|
@@ -193,6 +208,8 @@ def where(
|
|
|
193
208
|
"Cannot use --apps and --sites together.",
|
|
194
209
|
)
|
|
195
210
|
|
|
211
|
+
_populate_live_if_disabled()
|
|
212
|
+
|
|
196
213
|
matches: list[WhereMatch] = []
|
|
197
214
|
|
|
198
215
|
# A corrupt/locked cache or a schema mismatch surfaces here as a raw peewee
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/cache.py
RENAMED
|
@@ -5,7 +5,7 @@ This module provides utilities for managing the project inspection cache,
|
|
|
5
5
|
including re-caching operations built on the core inspect slice.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from . import db_utils
|
|
8
|
+
from . import config_utils, db_utils
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def recache_project(project_name: str, verbose: bool = False) -> bool:
|
|
@@ -31,6 +31,11 @@ def recache_project(project_name: str, verbose: bool = False) -> bool:
|
|
|
31
31
|
True if recache succeeded, False otherwise (including when the project's
|
|
32
32
|
containers are not running, since a stopped bench cannot be inspected).
|
|
33
33
|
"""
|
|
34
|
+
# Caching off: there is no on-disk cache to refresh, so a post-mutation
|
|
35
|
+
# recache would only run a full inspect into a store that is discarded.
|
|
36
|
+
if config_utils.cache_disabled():
|
|
37
|
+
return True
|
|
38
|
+
|
|
34
39
|
try:
|
|
35
40
|
# Clear the cache for this project
|
|
36
41
|
db_utils.clear_cache_for_project(project_name)
|
|
@@ -11,7 +11,7 @@ import time
|
|
|
11
11
|
import docker
|
|
12
12
|
import typer
|
|
13
13
|
|
|
14
|
-
from . import db_utils
|
|
14
|
+
from . import config_utils, db_utils
|
|
15
15
|
|
|
16
16
|
# Cache configuration
|
|
17
17
|
_CACHE_TTL = 2.0 # Cache results for 2 seconds (enough for tab completion session)
|
|
@@ -138,6 +138,12 @@ def complete_app_names(
|
|
|
138
138
|
if not project_name:
|
|
139
139
|
return []
|
|
140
140
|
|
|
141
|
+
# Caching off: degrade to an empty completion rather than blocking the shell
|
|
142
|
+
# on a live inspect. Empty beats stale, and completion must never do container
|
|
143
|
+
# I/O.
|
|
144
|
+
if config_utils.cache_disabled():
|
|
145
|
+
return []
|
|
146
|
+
|
|
141
147
|
# Check cache first
|
|
142
148
|
cache_key = f"apps:{project_name}"
|
|
143
149
|
cached = _get_cached(cache_key)
|
|
@@ -186,6 +192,12 @@ def complete_site_names(
|
|
|
186
192
|
if not project_name:
|
|
187
193
|
return []
|
|
188
194
|
|
|
195
|
+
# Caching off: degrade to an empty completion rather than blocking the shell
|
|
196
|
+
# on a live inspect. Empty beats stale, and completion must never do container
|
|
197
|
+
# I/O.
|
|
198
|
+
if config_utils.cache_disabled():
|
|
199
|
+
return []
|
|
200
|
+
|
|
189
201
|
# Check cache first
|
|
190
202
|
cache_key = f"sites:{project_name}"
|
|
191
203
|
cached = _get_cached(cache_key)
|
|
@@ -99,6 +99,18 @@ startup_enabled = false
|
|
|
99
99
|
# Applies immediately (no daemon restart needed); cwcli's own per-operation
|
|
100
100
|
# bridge (apps install/update, init) is not restricted by this list.
|
|
101
101
|
allowed_hosts = ["github.com", "gitlab.com"]
|
|
102
|
+
|
|
103
|
+
[cache]
|
|
104
|
+
# The on-disk SQLite cache under <cwcli home>/cache speeds up read commands
|
|
105
|
+
# (inspect, status, where, tab completion, ...) by remembering each project's
|
|
106
|
+
# benches, sites, and apps.
|
|
107
|
+
#
|
|
108
|
+
# Set enabled = false to turn caching off entirely: every read resolves live
|
|
109
|
+
# (as if a fresh inspect ran), nothing is written to or read from the cache DB,
|
|
110
|
+
# and a pre-existing cache is left in place untouched so turning caching back on
|
|
111
|
+
# restores it. The CWCLI_NO_CACHE environment variable overrides this key per
|
|
112
|
+
# shell / in CI (CWCLI_NO_CACHE=1 disables, CWCLI_NO_CACHE=0 re-enables).
|
|
113
|
+
enabled = true
|
|
102
114
|
"""
|
|
103
115
|
|
|
104
116
|
DEFAULT_ALLOWED_HOSTS = ("github.com", "gitlab.com")
|
|
@@ -150,6 +162,10 @@ def load_config() -> dict:
|
|
|
150
162
|
else:
|
|
151
163
|
for key, value in _cred_bridge_defaults().items():
|
|
152
164
|
config_data["cred_bridge"].setdefault(key, value)
|
|
165
|
+
if "cache" not in config_data:
|
|
166
|
+
config_data["cache"] = {"enabled": True}
|
|
167
|
+
elif "enabled" not in config_data["cache"]:
|
|
168
|
+
config_data["cache"]["enabled"] = True
|
|
153
169
|
return config_data
|
|
154
170
|
except toml.TomlDecodeError:
|
|
155
171
|
return {
|
|
@@ -157,6 +173,7 @@ def load_config() -> dict:
|
|
|
157
173
|
"auto_inspect": {"enabled": False, "interval": 3600, "startup_enabled": False},
|
|
158
174
|
"ui": {"show_tips": True},
|
|
159
175
|
"cred_bridge": _cred_bridge_defaults(),
|
|
176
|
+
"cache": {"enabled": True},
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
|
|
@@ -286,6 +303,49 @@ def set_cred_bridge_startup(enabled: bool):
|
|
|
286
303
|
save_config(config)
|
|
287
304
|
|
|
288
305
|
|
|
306
|
+
def read_cache_config() -> dict:
|
|
307
|
+
"""Read the [cache] config WITHOUT creating config state.
|
|
308
|
+
|
|
309
|
+
Used by :func:`cache_disabled`, which runs on the hot path of every cache
|
|
310
|
+
read; it must never write a default config file into the user's home as a
|
|
311
|
+
side effect (mirrors :func:`read_cred_bridge_config`).
|
|
312
|
+
"""
|
|
313
|
+
default = {"enabled": True}
|
|
314
|
+
if not CONFIG_FILE.is_file():
|
|
315
|
+
return default
|
|
316
|
+
try:
|
|
317
|
+
with open(CONFIG_FILE) as f:
|
|
318
|
+
config = toml.load(f)
|
|
319
|
+
except (OSError, toml.TomlDecodeError):
|
|
320
|
+
return default
|
|
321
|
+
cache = config.get("cache", default)
|
|
322
|
+
return cache if isinstance(cache, dict) else default
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def cache_disabled() -> bool:
|
|
326
|
+
"""Whether cwcli's on-disk SQLite cache is turned off.
|
|
327
|
+
|
|
328
|
+
The ``CWCLI_NO_CACHE`` environment variable overrides the ``[cache] enabled``
|
|
329
|
+
config key, so caching can be toggled per shell / in CI - and can even be
|
|
330
|
+
re-enabled (``CWCLI_NO_CACHE=0``) when the config file disables it. An unset
|
|
331
|
+
or empty env var falls through to the config key (default: enabled).
|
|
332
|
+
"""
|
|
333
|
+
env = os.environ.get("CWCLI_NO_CACHE")
|
|
334
|
+
if env is not None and env.strip() != "":
|
|
335
|
+
return env.strip().lower() in ("1", "true", "yes", "on")
|
|
336
|
+
return not bool(read_cache_config().get("enabled", True))
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def set_cache_enabled(enabled: bool):
|
|
340
|
+
"""Enable or disable the on-disk cache via the ``[cache] enabled`` config key."""
|
|
341
|
+
config = load_config()
|
|
342
|
+
if "cache" not in config:
|
|
343
|
+
config["cache"] = {"enabled": enabled}
|
|
344
|
+
else:
|
|
345
|
+
config["cache"]["enabled"] = enabled
|
|
346
|
+
save_config(config)
|
|
347
|
+
|
|
348
|
+
|
|
289
349
|
def get_show_tips() -> bool:
|
|
290
350
|
"""
|
|
291
351
|
Get whether tips should be shown during long-running operations.
|
|
@@ -12,7 +12,7 @@ from peewee import (
|
|
|
12
12
|
TextField,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
from . import shared_home
|
|
15
|
+
from . import config_utils, shared_home
|
|
16
16
|
from .config_utils import cwcli_home
|
|
17
17
|
|
|
18
18
|
# Resolve the cache location through the shared cwcli_home() helper so the
|
|
@@ -21,6 +21,12 @@ from .config_utils import cwcli_home
|
|
|
21
21
|
CACHE_DIR = cwcli_home() / "cache"
|
|
22
22
|
DB_PATH = CACHE_DIR / "cwc-cache.db"
|
|
23
23
|
|
|
24
|
+
# The sentinel path an ephemeral in-memory SQLite database uses. When caching is
|
|
25
|
+
# disabled (config_utils.cache_disabled()) the whole cache is redirected here, so
|
|
26
|
+
# the on-disk DB is never opened, read, or written and a stale row can never leak
|
|
27
|
+
# through - see initialize_database() and _live_populate().
|
|
28
|
+
_MEMORY_DB = ":memory:"
|
|
29
|
+
|
|
24
30
|
# SECURITY: config_json rows are whitelist-filtered by _redact_config_for_cache
|
|
25
31
|
# before write, so the cache never stores DB credentials, encryption keys, or
|
|
26
32
|
# redis URLs. Restrictive filesystem permissions (0700 dir / 0600 file) remain
|
|
@@ -30,20 +36,23 @@ DB_PATH = CACHE_DIR / "cwc-cache.db"
|
|
|
30
36
|
# shared_home).
|
|
31
37
|
|
|
32
38
|
# Create cache directory with restricted permissions (0700 = owner-only access)
|
|
33
|
-
# This prevents other users on the system from reading cached credentials
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
# This prevents other users on the system from reading cached credentials. Skip
|
|
40
|
+
# it entirely when caching is disabled, so a disabled install leaves no on-disk
|
|
41
|
+
# footprint at all.
|
|
42
|
+
if not config_utils.cache_disabled():
|
|
43
|
+
CACHE_DIR.mkdir(parents=True, mode=shared_home.dir_mode(0o700), exist_ok=True)
|
|
44
|
+
|
|
45
|
+
# Ensure existing directory has correct permissions
|
|
46
|
+
if CACHE_DIR.exists():
|
|
47
|
+
try:
|
|
48
|
+
CACHE_DIR.chmod(shared_home.dir_mode(0o700))
|
|
49
|
+
shared_home.apply_group(CACHE_DIR)
|
|
50
|
+
except (OSError, PermissionError):
|
|
51
|
+
# On Windows or restricted filesystems, chmod may fail
|
|
52
|
+
# Still proceed but permissions may not be as strict
|
|
53
|
+
pass
|
|
45
54
|
|
|
46
|
-
db = SqliteDatabase(DB_PATH)
|
|
55
|
+
db = SqliteDatabase(_MEMORY_DB if config_utils.cache_disabled() else DB_PATH)
|
|
47
56
|
|
|
48
57
|
|
|
49
58
|
class BaseModel(Model):
|
|
@@ -371,7 +380,54 @@ def _scrub_cached_config_secrets():
|
|
|
371
380
|
print(f"Warning: could not scrub cached config secrets: {e}", file=sys.stderr)
|
|
372
381
|
|
|
373
382
|
|
|
383
|
+
# Projects live-populated into the ephemeral in-memory cache this process (see
|
|
384
|
+
# _live_populate). A reentrancy flag stops the populating inspect - which writes
|
|
385
|
+
# the same in-memory DB - from recursing back through a cache read.
|
|
386
|
+
_live_populated: set[str] = set()
|
|
387
|
+
_populating = False
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _db_target() -> str:
|
|
391
|
+
"""Where the cache DB should point right now: in-memory when disabled."""
|
|
392
|
+
return _MEMORY_DB if config_utils.cache_disabled() else str(DB_PATH)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _live_populate(project_name: str) -> None:
|
|
396
|
+
"""In cache-disabled mode, populate the ephemeral in-memory DB with one live
|
|
397
|
+
inspect so downstream cache reads resolve fresh data instead of nothing.
|
|
398
|
+
|
|
399
|
+
The on-disk cache is never touched: the disabled DB points at ``:memory:``,
|
|
400
|
+
so the inspect's write lands in the ephemeral store this process discards on
|
|
401
|
+
exit. Memoized and reentrancy-guarded (the inspect writes the same DB), and
|
|
402
|
+
best-effort: a stopped/absent project simply yields no in-memory rows, which
|
|
403
|
+
every caller already handles as a cache miss.
|
|
404
|
+
"""
|
|
405
|
+
global _populating
|
|
406
|
+
if not config_utils.cache_disabled() or _populating or project_name in _live_populated:
|
|
407
|
+
return
|
|
408
|
+
_populating = True
|
|
409
|
+
try:
|
|
410
|
+
from ..core import inspect as core_inspect
|
|
411
|
+
|
|
412
|
+
core_inspect.inspect(project_name, refresh="full", offer_choice=False)
|
|
413
|
+
except Exception: # noqa: BLE001 - a failed live read just leaves the store empty
|
|
414
|
+
pass
|
|
415
|
+
finally:
|
|
416
|
+
_live_populated.add(project_name) # never retry within one process
|
|
417
|
+
_populating = False
|
|
418
|
+
|
|
419
|
+
|
|
374
420
|
def initialize_database():
|
|
421
|
+
target = _db_target()
|
|
422
|
+
if str(db.database) != target:
|
|
423
|
+
# The disabled state flipped (e.g. a test toggled CWCLI_NO_CACHE, or the
|
|
424
|
+
# first read after enable/disable). Re-point the shared database so every
|
|
425
|
+
# model follows it. Switching TO :memory: gives a fresh empty store;
|
|
426
|
+
# switching back opens the untouched on-disk cache.
|
|
427
|
+
if not db.is_closed():
|
|
428
|
+
db.close()
|
|
429
|
+
db.init(target)
|
|
430
|
+
_live_populated.clear()
|
|
375
431
|
if db.is_closed():
|
|
376
432
|
db.connect()
|
|
377
433
|
# Create tables if missing
|
|
@@ -528,6 +584,7 @@ def _cache_project_data(project_name: str, bench_instances_data: list[dict]) ->
|
|
|
528
584
|
|
|
529
585
|
def get_cached_project_data(project_name):
|
|
530
586
|
initialize_database()
|
|
587
|
+
_live_populate(project_name)
|
|
531
588
|
try:
|
|
532
589
|
project = Project.get(Project.name == project_name)
|
|
533
590
|
|
|
@@ -643,6 +700,7 @@ def get_common_site_config(project_name: str, bench_path: str | None = None) ->
|
|
|
643
700
|
Dictionary with common site config or None if not found
|
|
644
701
|
"""
|
|
645
702
|
initialize_database()
|
|
703
|
+
_live_populate(project_name)
|
|
646
704
|
try:
|
|
647
705
|
project = Project.get(Project.name == project_name)
|
|
648
706
|
|
|
@@ -685,6 +743,7 @@ def get_site_config(
|
|
|
685
743
|
Dictionary with site config or None if not found
|
|
686
744
|
"""
|
|
687
745
|
initialize_database()
|
|
746
|
+
_live_populate(project_name)
|
|
688
747
|
try:
|
|
689
748
|
project = Project.get(Project.name == project_name)
|
|
690
749
|
|
|
@@ -729,6 +788,7 @@ def get_all_site_configs(project_name: str, bench_path: str | None = None) -> di
|
|
|
729
788
|
Dictionary mapping site names to their configs
|
|
730
789
|
"""
|
|
731
790
|
initialize_database()
|
|
791
|
+
_live_populate(project_name)
|
|
732
792
|
result = {}
|
|
733
793
|
|
|
734
794
|
try:
|
|
@@ -763,6 +823,7 @@ def get_current_site(project_name: str, bench_path: str | None = None) -> str |
|
|
|
763
823
|
non-empty ``current_site`` found (scoped to ``bench_path`` when given).
|
|
764
824
|
"""
|
|
765
825
|
initialize_database()
|
|
826
|
+
_live_populate(project_name)
|
|
766
827
|
try:
|
|
767
828
|
project = Project.get(Project.name == project_name)
|
|
768
829
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "3.2.0"
|
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/__main__.py
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
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/commands/rm.py
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
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/apps.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/backup.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/docker.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/doctor.py
RENAMED
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/errors.py
RENAMED
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/fleet.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/init.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/label.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/list.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/logs.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/open.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/rm.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/run.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/scale.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/setup.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/start.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/status.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/stop.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/unlock.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/update.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/core/url.py
RENAMED
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/main.py
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
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/tips.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.3.0}/src/caffeinated_whale_cli/utils/toon.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|