Skip to content

Commit 0c8e13f

Browse files
committed
2.9.7
1 parent 55f0e2d commit 0c8e13f

File tree

5 files changed

+178
-15
lines changed

5 files changed

+178
-15
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1111
Use one changelog file, but separate entries by track in each release window.
1212

1313

14+
## [2.9.7] 2026-02-25
15+
16+
### Library (v2.9.7)
17+
18+
#### changed
19+
- CLI config loading now treats `web`, `tagpack-tool`, and `tagstore` as optional-config command groups, allowing these commands to run without a valid `.graphsense.yaml`.
20+
- Top-level command detection in CLI config loading now skips global options (including `--config-file`) before resolving command-specific loading behavior.
21+
22+
#### added
23+
- Integration tests for `graphsense-cli web openapi`, `graphsense-cli tagpack-tool --version`, and `graphsense-cli tagstore version` to verify behavior without a loaded GraphSense config file.
24+
25+
### Web API + Python client (webapi-2.9.5)
26+
no changes
27+
1428
## [2.9.6] 2026-02-23
1529

1630
### Library (v2.9.6)

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
SHELL := /bin/bash
22
PROJECT := graphsense-lib
33
VENV := venv
4-
RELEASESEM := 'v2.9.7rc1'
4+
RELEASESEM := 'v2.9.7'
55
WEBAPISEM := 'v2.9.5'
66

77
-include .env

src/graphsenselib/cli/common.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,36 +78,45 @@ def inner(function):
7878

7979

8080
def try_load_config(filename: str):
81-
if (
82-
len(sys.argv) > 1
83-
and sys.argv[1][0] == "-"
84-
and sys.argv[1][1:] == "v" * len(sys.argv[1][1:])
85-
):
86-
remaining_args = sys.argv[2:]
87-
else:
88-
remaining_args = sys.argv[1:]
89-
90-
is_tagpack_tool = len(remaining_args) > 0 and remaining_args[0] in [
81+
remaining_args = sys.argv[1:]
82+
83+
global_options_with_values = {"--config-file"}
84+
top_level_command = None
85+
skip_next = False
86+
for arg in remaining_args:
87+
if skip_next:
88+
skip_next = False
89+
continue
90+
if arg in global_options_with_values:
91+
skip_next = True
92+
continue
93+
if arg.startswith("-"):
94+
continue
95+
top_level_command = arg
96+
break
97+
98+
is_optional_config_command = top_level_command in [
9199
"tagpack-tool",
92100
"tagstore",
101+
"web",
93102
]
94103

95104
app_config = get_config()
96105
f = filename or app_config.underlying_file
97106

98107
try:
99-
if is_tagpack_tool:
108+
if is_optional_config_command:
100109
success, errors = app_config.load_partial(filename=filename)
101110

102111
if not success:
103112
logger.debug(
104-
f"Partial config loading for {remaining_args[0]} with {len(errors)} issues:"
113+
f"Partial config loading for {top_level_command} with {len(errors)} issues:"
105114
)
106115
for error in errors:
107116
logger.debug(f" - {error}")
108117
logger.debug("Continuing with partial/default configuration...")
109118
else:
110-
logger.debug(f"Config created successfully for {remaining_args[0]}")
119+
logger.debug(f"Config created successfully for {top_level_command}")
111120
else:
112121
# Use strict loading for other tools
113122
app_config.load(filename=filename)
@@ -117,7 +126,7 @@ def try_load_config(filename: str):
117126
md5hash = hashlib.md5(file.read()).hexdigest()
118127
else:
119128
md5hash = "no-config-file"
120-
if not is_tagpack_tool:
129+
if not is_optional_config_command:
121130
raise Exception("No config file loaded")
122131

123132
return app_config, md5hash

tests/cli/test_common.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import pytest
2+
3+
from graphsenselib.cli.common import try_load_config
4+
5+
6+
class FakeConfig:
7+
def __init__(self, underlying_file=None):
8+
self.underlying_file = underlying_file
9+
self.model_config = {
10+
"default_files": ["~/.graphsense.yaml"],
11+
"file_env_var": "GRAPHSENSE_CONFIG",
12+
}
13+
self.load_partial_called = False
14+
self.load_called = False
15+
16+
def load_partial(self, filename=None):
17+
self.load_partial_called = True
18+
return True, []
19+
20+
def load(self, filename=None):
21+
self.load_called = True
22+
23+
def generate_yaml(self, DEBUG=False):
24+
return ""
25+
26+
27+
def test_try_load_config_web_is_optional(monkeypatch):
28+
cfg = FakeConfig(underlying_file=None)
29+
30+
monkeypatch.setattr("graphsenselib.cli.common.get_config", lambda: cfg)
31+
monkeypatch.setattr(
32+
"graphsenselib.cli.common.sys.argv",
33+
["graphsense-cli", "web", "openapi"],
34+
)
35+
36+
loaded_cfg, md5hash = try_load_config(filename=None)
37+
38+
assert loaded_cfg is cfg
39+
assert md5hash == "no-config-file"
40+
assert cfg.load_partial_called
41+
assert not cfg.load_called
42+
43+
44+
def test_try_load_config_web_with_global_config_option(monkeypatch):
45+
cfg = FakeConfig(underlying_file=None)
46+
47+
monkeypatch.setattr("graphsenselib.cli.common.get_config", lambda: cfg)
48+
monkeypatch.setattr(
49+
"graphsenselib.cli.common.sys.argv",
50+
[
51+
"graphsense-cli",
52+
"--config-file",
53+
"/does/not/exist.yaml",
54+
"web",
55+
"openapi",
56+
],
57+
)
58+
59+
loaded_cfg, md5hash = try_load_config(filename="/does/not/exist.yaml")
60+
61+
assert loaded_cfg is cfg
62+
assert md5hash == "no-config-file"
63+
assert cfg.load_partial_called
64+
assert not cfg.load_called
65+
66+
67+
def test_try_load_config_non_optional_command_is_strict(monkeypatch):
68+
cfg = FakeConfig(underlying_file=None)
69+
70+
monkeypatch.setattr("graphsenselib.cli.common.get_config", lambda: cfg)
71+
monkeypatch.setattr(
72+
"graphsenselib.cli.common.sys.argv",
73+
["graphsense-cli", "rates", "coin-prices"],
74+
)
75+
76+
with pytest.raises(SystemExit) as excinfo:
77+
try_load_config(filename=None)
78+
79+
assert excinfo.value.code == 10
80+
assert not cfg.load_partial_called
81+
assert cfg.load_called
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import json
2+
3+
from click.testing import CliRunner
4+
5+
from graphsenselib.cli.main import cli
6+
7+
8+
def test_web_openapi_works_without_config_file(tmp_path, monkeypatch):
9+
missing_cfg = tmp_path / "does-not-exist.yaml"
10+
monkeypatch.setattr(
11+
"graphsenselib.cli.common.sys.argv",
12+
["graphsense-cli", "web", "openapi"],
13+
)
14+
15+
result = CliRunner().invoke(
16+
cli,
17+
["web", "openapi"],
18+
env={"GRAPHSENSE_CONFIG_YAML": str(missing_cfg)},
19+
)
20+
21+
assert result.exit_code == 0
22+
23+
spec = json.loads(result.output)
24+
assert spec["openapi"].startswith("3.")
25+
assert "paths" in spec
26+
27+
28+
def test_tagpack_tool_version_works_without_config_file(tmp_path, monkeypatch):
29+
missing_cfg = tmp_path / "does-not-exist.yaml"
30+
monkeypatch.setattr(
31+
"graphsenselib.cli.common.sys.argv",
32+
["graphsense-cli", "tagpack-tool", "--version"],
33+
)
34+
35+
result = CliRunner().invoke(
36+
cli,
37+
["tagpack-tool", "--version"],
38+
env={"GRAPHSENSE_CONFIG_YAML": str(missing_cfg)},
39+
)
40+
41+
assert result.exit_code == 0
42+
assert "tagpack-tool" in result.output.lower()
43+
44+
45+
def test_tagstore_version_works_without_config_file(tmp_path, monkeypatch):
46+
missing_cfg = tmp_path / "does-not-exist.yaml"
47+
monkeypatch.setattr(
48+
"graphsenselib.cli.common.sys.argv",
49+
["graphsense-cli", "tagstore", "version"],
50+
)
51+
52+
result = CliRunner().invoke(
53+
cli,
54+
["tagstore", "version"],
55+
env={"GRAPHSENSE_CONFIG_YAML": str(missing_cfg)},
56+
)
57+
58+
assert result.exit_code == 0
59+
assert result.output.strip()

0 commit comments

Comments
 (0)