zapi-mcp 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,242 @@
1
+ Metadata-Version: 2.4
2
+ Name: zapi-mcp
3
+ Version: 0.2.0
4
+ Summary: MCP server for Zabbix API — daily brief, problems, hosts, items
5
+ Author: AIKAWA Shigechika
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/shigechika/zapi-mcp
8
+ Project-URL: Repository, https://github.com/shigechika/zapi-mcp
9
+ Project-URL: Issues, https://github.com/shigechika/zapi-mcp/issues
10
+ Keywords: zabbix,mcp,model-context-protocol,monitoring,network
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: System :: Monitoring
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: mcp>=1.0
22
+ Requires-Dist: httpx
23
+
24
+ <!-- mcp-name: io.github.shigechika/zapi-mcp -->
25
+
26
+ # zapi-mcp
27
+
28
+ English | [日本語](README.ja.md)
29
+
30
+ MCP (Model Context Protocol) server for the [Zabbix](https://www.zabbix.com/) API.
31
+
32
+ Built for network operations: a single `daily_brief` call summarizes active
33
+ problems plus site-specific categories (DHCP pool usage, SNAT session usage,
34
+ core-network problems, …), and individual tools query problems, hosts, and item
35
+ values. Organization-specific tags live in a config file, not the code, so the
36
+ server stays generic.
37
+
38
+ Version-adaptive auth: works against Zabbix 6.0 LTS (`user` + `auth` field) and
39
+ forward-compatible with 6.4 / 7.0 (`username` + `Authorization: Bearer`).
40
+
41
+ ## Features
42
+
43
+ | Tool | Description |
44
+ |------|-------------|
45
+ | `daily_brief` | Morning patrol: active problems (Warning+) plus one section per configured category |
46
+ | `get_problems` | Active problems by severity and tag, newest-first with age; header shows the true total (`showing N of TOTAL` when capped); output includes `eventid` |
47
+ | `get_hosts` | List hosts filtered by role/tag/group, with IP and tags |
48
+ | `get_host_items` | Current item values for a host (server-side host filter) |
49
+ | `acknowledge_problem` | Acknowledge problems and add a message (does not close them) |
50
+
51
+ ## Setup
52
+
53
+ ```bash
54
+ # uv
55
+ uv pip install zapi-mcp
56
+
57
+ # pip
58
+ pip install zapi-mcp
59
+ ```
60
+
61
+ Or from source:
62
+
63
+ ```bash
64
+ git clone https://github.com/shigechika/zapi-mcp.git
65
+ cd zapi-mcp
66
+
67
+ # uv
68
+ uv sync
69
+
70
+ # pip
71
+ pip install -e .
72
+ ```
73
+
74
+ ## Configuration
75
+
76
+ Set the following environment variables:
77
+
78
+ | Variable | Description | Default |
79
+ |---|---|---|
80
+ | `ZABBIX_URL` | Zabbix base URL (e.g. `https://zabbix.example.com`); `/api_jsonrpc.php` is appended if absent | *required* |
81
+ | `ZABBIX_USER` | Zabbix API user | *required* |
82
+ | `ZABBIX_PASSWORD` | Zabbix API password | *required* |
83
+ | `ZABBIX_CATEGORIES_INI` | Path to a categories INI file for `daily_brief` (optional) | — |
84
+ | `ZABBIX_BRIEF_RECENT_HOURS` | `daily_brief` "recent" window in hours; problems older than this are folded to a count | `24` |
85
+ | `ZABBIX_BRIEF_PROBLEM_LIMIT` | Max active problems `daily_brief` fetches per call before counting the rest | `1000` |
86
+
87
+ The API user needs read permission for the host groups you query, plus
88
+ acknowledge permission if you use `acknowledge_problem`.
89
+
90
+ ### Active problems in `daily_brief`
91
+
92
+ Problems are grouped by severity and listed **newest-first**, each annotated with
93
+ its age (e.g. `3h ago`). Problems older than the recent window
94
+ (`ZABBIX_BRIEF_RECENT_HOURS`, default 24h) are folded to a single
95
+ `… and N older (stale; oldest …)` line — so a backlog of alerts that Zabbix
96
+ keeps active because their recovery is never auto-confirmed (ICMP ping down, RDP
97
+ down, …) doesn't bury what just happened. Section headers carry the true total
98
+ and show `showing N of TOTAL` when the fetch is capped, never a silent truncation.
99
+
100
+ ### Categories for `daily_brief` (optional)
101
+
102
+ `daily_brief` always lists active problems. To add site-specific sections —
103
+ DHCP pool exhaustion, SNAT session usage, core-network problems — point
104
+ `ZABBIX_CATEGORIES_INI` at an INI file. Each `[section]` is one category:
105
+
106
+ ```ini
107
+ [dhcp]
108
+ name = DHCP Pool Usage
109
+ tag = dhcp-pool-usage ; Zabbix host tag identifying the group
110
+ item_key = usage ; report current values for this exact item key
111
+ threshold = 80 ; flag values >= this
112
+
113
+ [snat]
114
+ name = SNAT Session Pool
115
+ tag = snat-pool-usage
116
+ item_key_search = .usage ; substring match (catches pool.node0.usage etc.)
117
+ threshold = 80
118
+
119
+ [core]
120
+ name = Core Network
121
+ tag = role
122
+ tag_value = main ; tag must equal this value
123
+ ; no item key -> report active problems instead
124
+ ```
125
+
126
+ - `tag` (required): host tag identifying the category. With `tag_value`, the tag
127
+ must equal it (Equal); without, any host carrying the tag matches (Exists).
128
+ - `item_key` / `item_key_search`: when either is set, the section reports current
129
+ item values sorted high-to-low. `item_key` matches the key exactly; use
130
+ `item_key_search` for keys that embed an id (e.g. `.usage` catches
131
+ `pool.node0.usage`). When neither is set, it reports active problems for the tag.
132
+ - `threshold`: optional; values at or above it are flagged.
133
+
134
+ See [`categories.ini.example`](categories.ini.example). When the variable is
135
+ unset or the file is missing, `daily_brief` reports active problems only.
136
+
137
+ ## Usage
138
+
139
+ ### Claude Code
140
+
141
+ Add to `.mcp.json`:
142
+
143
+ ```json
144
+ {
145
+ "mcpServers": {
146
+ "zapi-mcp": {
147
+ "type": "stdio",
148
+ "command": "zapi-mcp",
149
+ "env": {
150
+ "ZABBIX_URL": "https://zabbix.example.com",
151
+ "ZABBIX_USER": "api-user",
152
+ "ZABBIX_PASSWORD": "",
153
+ "ZABBIX_CATEGORIES_INI": "/path/to/categories.ini"
154
+ }
155
+ }
156
+ }
157
+ }
158
+ ```
159
+
160
+ ### Claude Desktop
161
+
162
+ Add to `claude_desktop_config.json`:
163
+
164
+ ```json
165
+ {
166
+ "mcpServers": {
167
+ "zapi-mcp": {
168
+ "command": "zapi-mcp",
169
+ "env": {
170
+ "ZABBIX_URL": "https://zabbix.example.com",
171
+ "ZABBIX_USER": "api-user",
172
+ "ZABBIX_PASSWORD": ""
173
+ }
174
+ }
175
+ }
176
+ }
177
+ ```
178
+
179
+ ### Direct Execution
180
+
181
+ ```bash
182
+ export ZABBIX_URL=https://zabbix.example.com
183
+ export ZABBIX_USER=api-user
184
+ export ZABBIX_PASSWORD=your-password
185
+ zapi-mcp
186
+ ```
187
+
188
+ ### CLI Options
189
+
190
+ ```bash
191
+ zapi-mcp --version # Print version and exit
192
+ zapi-mcp --check # Verify environment variables and authentication, then exit
193
+ zapi-mcp --brief # Print the daily_brief to stdout and exit (handy for cron)
194
+ zapi-mcp # Start MCP server (STDIO, default)
195
+ ```
196
+
197
+ `--check` exit codes: `0` success, `1` config error, `2` auth/connection error.
198
+
199
+ ## Development
200
+
201
+ ```bash
202
+ git clone https://github.com/shigechika/zapi-mcp.git
203
+ cd zapi-mcp
204
+
205
+ # uv
206
+ uv sync --dev
207
+ uv run pytest -v
208
+ uv run ruff check .
209
+
210
+ # pip
211
+ python3 -m venv .venv
212
+ .venv/bin/pip install -e . && .venv/bin/pip install pytest pytest-cov respx ruff
213
+ .venv/bin/pytest -v
214
+ .venv/bin/ruff check .
215
+ ```
216
+
217
+ ## Releasing
218
+
219
+ Releases are automated with [release-please](https://github.com/googleapis/release-please).
220
+ Merging [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, …)
221
+ to `main` keeps a release PR open with the next version and changelog. Merging
222
+ that PR tags `vX.Y.Z` and publishes a GitHub Release, whose `release: published`
223
+ event triggers the `release` workflow to build and publish to PyPI and the MCP
224
+ Registry. release-please owns the version in `zapi_mcp/__init__.py` and
225
+ `server.json` (do not bump them by hand).
226
+
227
+ > [!IMPORTANT]
228
+ > The release-please workflow should be given a repository secret
229
+ > `RELEASE_PLEASE_TOKEN` (a PAT with `contents: write` + `pull-requests: write`).
230
+ > The default `GITHUB_TOKEN` cannot create the Release that triggers the
231
+ > downstream `release` workflow (GitHub blocks workflow runs triggered by
232
+ > `GITHUB_TOKEN`), so without the PAT nothing gets published. The workflow falls
233
+ > back to `GITHUB_TOKEN` when the secret is unset so PR CI keeps working on forks.
234
+
235
+ ## Roadmap
236
+
237
+ - Streamable HTTP transport + OAuth2 for remote / mobile use
238
+ - Visual rendering of key metrics
239
+
240
+ ## License
241
+
242
+ MIT
@@ -0,0 +1,219 @@
1
+ <!-- mcp-name: io.github.shigechika/zapi-mcp -->
2
+
3
+ # zapi-mcp
4
+
5
+ English | [日本語](README.ja.md)
6
+
7
+ MCP (Model Context Protocol) server for the [Zabbix](https://www.zabbix.com/) API.
8
+
9
+ Built for network operations: a single `daily_brief` call summarizes active
10
+ problems plus site-specific categories (DHCP pool usage, SNAT session usage,
11
+ core-network problems, …), and individual tools query problems, hosts, and item
12
+ values. Organization-specific tags live in a config file, not the code, so the
13
+ server stays generic.
14
+
15
+ Version-adaptive auth: works against Zabbix 6.0 LTS (`user` + `auth` field) and
16
+ forward-compatible with 6.4 / 7.0 (`username` + `Authorization: Bearer`).
17
+
18
+ ## Features
19
+
20
+ | Tool | Description |
21
+ |------|-------------|
22
+ | `daily_brief` | Morning patrol: active problems (Warning+) plus one section per configured category |
23
+ | `get_problems` | Active problems by severity and tag, newest-first with age; header shows the true total (`showing N of TOTAL` when capped); output includes `eventid` |
24
+ | `get_hosts` | List hosts filtered by role/tag/group, with IP and tags |
25
+ | `get_host_items` | Current item values for a host (server-side host filter) |
26
+ | `acknowledge_problem` | Acknowledge problems and add a message (does not close them) |
27
+
28
+ ## Setup
29
+
30
+ ```bash
31
+ # uv
32
+ uv pip install zapi-mcp
33
+
34
+ # pip
35
+ pip install zapi-mcp
36
+ ```
37
+
38
+ Or from source:
39
+
40
+ ```bash
41
+ git clone https://github.com/shigechika/zapi-mcp.git
42
+ cd zapi-mcp
43
+
44
+ # uv
45
+ uv sync
46
+
47
+ # pip
48
+ pip install -e .
49
+ ```
50
+
51
+ ## Configuration
52
+
53
+ Set the following environment variables:
54
+
55
+ | Variable | Description | Default |
56
+ |---|---|---|
57
+ | `ZABBIX_URL` | Zabbix base URL (e.g. `https://zabbix.example.com`); `/api_jsonrpc.php` is appended if absent | *required* |
58
+ | `ZABBIX_USER` | Zabbix API user | *required* |
59
+ | `ZABBIX_PASSWORD` | Zabbix API password | *required* |
60
+ | `ZABBIX_CATEGORIES_INI` | Path to a categories INI file for `daily_brief` (optional) | — |
61
+ | `ZABBIX_BRIEF_RECENT_HOURS` | `daily_brief` "recent" window in hours; problems older than this are folded to a count | `24` |
62
+ | `ZABBIX_BRIEF_PROBLEM_LIMIT` | Max active problems `daily_brief` fetches per call before counting the rest | `1000` |
63
+
64
+ The API user needs read permission for the host groups you query, plus
65
+ acknowledge permission if you use `acknowledge_problem`.
66
+
67
+ ### Active problems in `daily_brief`
68
+
69
+ Problems are grouped by severity and listed **newest-first**, each annotated with
70
+ its age (e.g. `3h ago`). Problems older than the recent window
71
+ (`ZABBIX_BRIEF_RECENT_HOURS`, default 24h) are folded to a single
72
+ `… and N older (stale; oldest …)` line — so a backlog of alerts that Zabbix
73
+ keeps active because their recovery is never auto-confirmed (ICMP ping down, RDP
74
+ down, …) doesn't bury what just happened. Section headers carry the true total
75
+ and show `showing N of TOTAL` when the fetch is capped, never a silent truncation.
76
+
77
+ ### Categories for `daily_brief` (optional)
78
+
79
+ `daily_brief` always lists active problems. To add site-specific sections —
80
+ DHCP pool exhaustion, SNAT session usage, core-network problems — point
81
+ `ZABBIX_CATEGORIES_INI` at an INI file. Each `[section]` is one category:
82
+
83
+ ```ini
84
+ [dhcp]
85
+ name = DHCP Pool Usage
86
+ tag = dhcp-pool-usage ; Zabbix host tag identifying the group
87
+ item_key = usage ; report current values for this exact item key
88
+ threshold = 80 ; flag values >= this
89
+
90
+ [snat]
91
+ name = SNAT Session Pool
92
+ tag = snat-pool-usage
93
+ item_key_search = .usage ; substring match (catches pool.node0.usage etc.)
94
+ threshold = 80
95
+
96
+ [core]
97
+ name = Core Network
98
+ tag = role
99
+ tag_value = main ; tag must equal this value
100
+ ; no item key -> report active problems instead
101
+ ```
102
+
103
+ - `tag` (required): host tag identifying the category. With `tag_value`, the tag
104
+ must equal it (Equal); without, any host carrying the tag matches (Exists).
105
+ - `item_key` / `item_key_search`: when either is set, the section reports current
106
+ item values sorted high-to-low. `item_key` matches the key exactly; use
107
+ `item_key_search` for keys that embed an id (e.g. `.usage` catches
108
+ `pool.node0.usage`). When neither is set, it reports active problems for the tag.
109
+ - `threshold`: optional; values at or above it are flagged.
110
+
111
+ See [`categories.ini.example`](categories.ini.example). When the variable is
112
+ unset or the file is missing, `daily_brief` reports active problems only.
113
+
114
+ ## Usage
115
+
116
+ ### Claude Code
117
+
118
+ Add to `.mcp.json`:
119
+
120
+ ```json
121
+ {
122
+ "mcpServers": {
123
+ "zapi-mcp": {
124
+ "type": "stdio",
125
+ "command": "zapi-mcp",
126
+ "env": {
127
+ "ZABBIX_URL": "https://zabbix.example.com",
128
+ "ZABBIX_USER": "api-user",
129
+ "ZABBIX_PASSWORD": "",
130
+ "ZABBIX_CATEGORIES_INI": "/path/to/categories.ini"
131
+ }
132
+ }
133
+ }
134
+ }
135
+ ```
136
+
137
+ ### Claude Desktop
138
+
139
+ Add to `claude_desktop_config.json`:
140
+
141
+ ```json
142
+ {
143
+ "mcpServers": {
144
+ "zapi-mcp": {
145
+ "command": "zapi-mcp",
146
+ "env": {
147
+ "ZABBIX_URL": "https://zabbix.example.com",
148
+ "ZABBIX_USER": "api-user",
149
+ "ZABBIX_PASSWORD": ""
150
+ }
151
+ }
152
+ }
153
+ }
154
+ ```
155
+
156
+ ### Direct Execution
157
+
158
+ ```bash
159
+ export ZABBIX_URL=https://zabbix.example.com
160
+ export ZABBIX_USER=api-user
161
+ export ZABBIX_PASSWORD=your-password
162
+ zapi-mcp
163
+ ```
164
+
165
+ ### CLI Options
166
+
167
+ ```bash
168
+ zapi-mcp --version # Print version and exit
169
+ zapi-mcp --check # Verify environment variables and authentication, then exit
170
+ zapi-mcp --brief # Print the daily_brief to stdout and exit (handy for cron)
171
+ zapi-mcp # Start MCP server (STDIO, default)
172
+ ```
173
+
174
+ `--check` exit codes: `0` success, `1` config error, `2` auth/connection error.
175
+
176
+ ## Development
177
+
178
+ ```bash
179
+ git clone https://github.com/shigechika/zapi-mcp.git
180
+ cd zapi-mcp
181
+
182
+ # uv
183
+ uv sync --dev
184
+ uv run pytest -v
185
+ uv run ruff check .
186
+
187
+ # pip
188
+ python3 -m venv .venv
189
+ .venv/bin/pip install -e . && .venv/bin/pip install pytest pytest-cov respx ruff
190
+ .venv/bin/pytest -v
191
+ .venv/bin/ruff check .
192
+ ```
193
+
194
+ ## Releasing
195
+
196
+ Releases are automated with [release-please](https://github.com/googleapis/release-please).
197
+ Merging [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, …)
198
+ to `main` keeps a release PR open with the next version and changelog. Merging
199
+ that PR tags `vX.Y.Z` and publishes a GitHub Release, whose `release: published`
200
+ event triggers the `release` workflow to build and publish to PyPI and the MCP
201
+ Registry. release-please owns the version in `zapi_mcp/__init__.py` and
202
+ `server.json` (do not bump them by hand).
203
+
204
+ > [!IMPORTANT]
205
+ > The release-please workflow should be given a repository secret
206
+ > `RELEASE_PLEASE_TOKEN` (a PAT with `contents: write` + `pull-requests: write`).
207
+ > The default `GITHUB_TOKEN` cannot create the Release that triggers the
208
+ > downstream `release` workflow (GitHub blocks workflow runs triggered by
209
+ > `GITHUB_TOKEN`), so without the PAT nothing gets published. The workflow falls
210
+ > back to `GITHUB_TOKEN` when the secret is unset so PR CI keeps working on forks.
211
+
212
+ ## Roadmap
213
+
214
+ - Streamable HTTP transport + OAuth2 for remote / mobile use
215
+ - Visual rendering of key metrics
216
+
217
+ ## License
218
+
219
+ MIT
@@ -0,0 +1,62 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "zapi-mcp"
7
+ dynamic = ["version"]
8
+ description = "MCP server for Zabbix API — daily brief, problems, hosts, items"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "AIKAWA Shigechika"},
14
+ ]
15
+ keywords = ["zabbix", "mcp", "model-context-protocol", "monitoring", "network"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: System Administrators",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: System :: Monitoring",
25
+ ]
26
+ dependencies = [
27
+ "mcp>=1.0",
28
+ "httpx",
29
+ ]
30
+
31
+ [project.scripts]
32
+ zapi-mcp = "zapi_mcp.__main__:main"
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/shigechika/zapi-mcp"
36
+ Repository = "https://github.com/shigechika/zapi-mcp"
37
+ Issues = "https://github.com/shigechika/zapi-mcp/issues"
38
+
39
+ [dependency-groups]
40
+ dev = [
41
+ "pytest",
42
+ "pytest-cov",
43
+ "respx",
44
+ "ruff",
45
+ "freezegun",
46
+ ]
47
+
48
+ [tool.setuptools]
49
+ packages = ["zapi_mcp"]
50
+
51
+ [tool.setuptools.dynamic]
52
+ version = {attr = "zapi_mcp.__version__"}
53
+
54
+ [tool.ruff]
55
+ target-version = "py310"
56
+ line-length = 120
57
+
58
+ [tool.ruff.lint]
59
+ select = ["E", "F", "I", "W", "UP"]
60
+
61
+ [tool.pytest.ini_options]
62
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,72 @@
1
+ """Tests for category config loading."""
2
+
3
+ from zapi_mcp.categories import load_categories
4
+
5
+
6
+ def test_no_path_returns_empty(monkeypatch):
7
+ monkeypatch.delenv("ZABBIX_CATEGORIES_INI", raising=False)
8
+ assert load_categories() == []
9
+
10
+
11
+ def test_missing_file_returns_empty(tmp_path):
12
+ assert load_categories(str(tmp_path / "nope.ini")) == []
13
+
14
+
15
+ def test_item_category(tmp_path):
16
+ p = tmp_path / "cats.ini"
17
+ p.write_text("[dhcp]\nname = DHCP Pool Usage\ntag = dhcp-pool-usage\nitem_key = usage\nthreshold = 80\n")
18
+ cats = load_categories(str(p))
19
+ assert len(cats) == 1
20
+ c = cats[0]
21
+ assert c.key == "dhcp"
22
+ assert c.name == "DHCP Pool Usage"
23
+ assert c.tag == "dhcp-pool-usage"
24
+ assert c.item_key == "usage"
25
+ assert c.threshold == 80.0
26
+ assert c.kind == "items"
27
+
28
+
29
+ def test_problem_category_without_item_key(tmp_path):
30
+ p = tmp_path / "cats.ini"
31
+ p.write_text("[core]\nname = Core Network\ntag = role\ntag_value = main\n")
32
+ c = load_categories(str(p))[0]
33
+ assert c.tag_value == "main"
34
+ assert c.item_key is None
35
+ assert c.kind == "problems"
36
+
37
+
38
+ def test_section_without_tag_is_skipped(tmp_path):
39
+ p = tmp_path / "cats.ini"
40
+ p.write_text("[bad]\nname = No Tag\n\n[good]\ntag = role\n")
41
+ cats = load_categories(str(p))
42
+ assert [c.key for c in cats] == ["good"]
43
+
44
+
45
+ def test_name_defaults_to_section(tmp_path):
46
+ p = tmp_path / "cats.ini"
47
+ p.write_text("[edge]\ntag = role\n")
48
+ assert load_categories(str(p))[0].name == "edge"
49
+
50
+
51
+ def test_env_var_used(tmp_path, monkeypatch):
52
+ p = tmp_path / "cats.ini"
53
+ p.write_text("[dhcp]\ntag = dhcp\nitem_key = usage\n")
54
+ monkeypatch.setenv("ZABBIX_CATEGORIES_INI", str(p))
55
+ cats = load_categories()
56
+ assert cats[0].key == "dhcp"
57
+
58
+
59
+ def test_non_numeric_threshold_does_not_crash(tmp_path):
60
+ p = tmp_path / "cats.ini"
61
+ p.write_text("[dhcp]\ntag = dhcp\nitem_key = usage\nthreshold = high\n")
62
+ cats = load_categories(str(p))
63
+ assert cats[0].threshold is None
64
+
65
+
66
+ def test_item_key_search_makes_items_category(tmp_path):
67
+ p = tmp_path / "cats.ini"
68
+ p.write_text("[snat]\ntag = snat\nitem_key_search = .usage\n")
69
+ c = load_categories(str(p))[0]
70
+ assert c.item_key is None
71
+ assert c.item_key_search == ".usage"
72
+ assert c.kind == "items"