qapu-cli 0.6.2__tar.gz → 0.6.4__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.
- {qapu_cli-0.6.2 → qapu_cli-0.6.4}/PKG-INFO +19 -4
- {qapu_cli-0.6.2 → qapu_cli-0.6.4}/README.md +18 -3
- {qapu_cli-0.6.2 → qapu_cli-0.6.4}/pyproject.toml +1 -1
- {qapu_cli-0.6.2 → qapu_cli-0.6.4}/qapu_cli/main.py +120 -0
- {qapu_cli-0.6.2 → qapu_cli-0.6.4}/qapu_cli.egg-info/PKG-INFO +19 -4
- {qapu_cli-0.6.2 → qapu_cli-0.6.4}/qapu_cli/__init__.py +0 -0
- {qapu_cli-0.6.2 → qapu_cli-0.6.4}/qapu_cli/client.py +0 -0
- {qapu_cli-0.6.2 → qapu_cli-0.6.4}/qapu_cli.egg-info/SOURCES.txt +0 -0
- {qapu_cli-0.6.2 → qapu_cli-0.6.4}/qapu_cli.egg-info/dependency_links.txt +0 -0
- {qapu_cli-0.6.2 → qapu_cli-0.6.4}/qapu_cli.egg-info/entry_points.txt +0 -0
- {qapu_cli-0.6.2 → qapu_cli-0.6.4}/qapu_cli.egg-info/requires.txt +0 -0
- {qapu_cli-0.6.2 → qapu_cli-0.6.4}/qapu_cli.egg-info/top_level.txt +0 -0
- {qapu_cli-0.6.2 → qapu_cli-0.6.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qapu-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: CLI client for the Qapu API - built for the Hermes agent, but usable by anyone talking to api.ovoo.com.tr from outside the Swarm.
|
|
5
5
|
Author: OVOO Technology
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -182,7 +182,10 @@ qapu stream list <device_id> [--page N] [--size N] [--json] # GET /hermes/stre
|
|
|
182
182
|
qapu stream get <stream_id> # GET /hermes/stream/detail/{stream_id}
|
|
183
183
|
|
|
184
184
|
qapu synthesis list [--device-id] [--variable-id] [--active/--inactive] [--json] # GET /hermes/synthesis - derived-metric calculation rules
|
|
185
|
-
|
|
185
|
+
qapu synthesis get <rule_id> [--json] # GET /hermes/synthesis/{rule_id}
|
|
186
|
+
qapu synthesis add <variable_id> --required-variables A,B,C --calculation '{"formula": "A + B"}' [--device-id] [--conditions] [--min-value] [--max-value] [--priority] [--active/--inactive] [--description] # POST /hermes/synthesis
|
|
187
|
+
qapu synthesis update <rule_id> [--variable-id] [--required-variables] [--calculation] [--device-id] [--conditions] [--min-value] [--max-value] [--priority] [--active/--inactive] [--description] # PUT /hermes/synthesis/{rule_id}
|
|
188
|
+
qapu synthesis delete <rule_id> # DELETE /hermes/synthesis/{rule_id}
|
|
186
189
|
qapu user-role list [--json] [--limit N] # GET /hermes/user_roles - e.g. admin/farmer/technician, NOT the User class itself (PII, deliberately excluded)
|
|
187
190
|
qapu user-role get <role_id> [--json] # GET /hermes/user_roles/{id}
|
|
188
191
|
qapu user-role add <id> "Name" [--description] # POST /hermes/user_roles - ID is REQUIRED, this catalog's ID is NOT auto-increment
|
|
@@ -200,6 +203,20 @@ qapu project update <project_id> [--name] [--description] [--active/--inactive]
|
|
|
200
203
|
qapu project delete <project_id> # DELETE /hermes/projects/{project_id}
|
|
201
204
|
```
|
|
202
205
|
|
|
206
|
+
**`synthesis` extended to full CRUD (2026-09-03)**, per direct user correction after the read-only version shipped the day before ("qapu synthesis için tam crud yapmamışsın sadece list var"). The 2026-09-02 read-only version was a faithful reflection of the domain class at the time - `Synthesized_Metrics` genuinely had no `add()`/`update()`/`delete()`, only `get_list()`. Added `detail()`/`add()`/`update()`/`delete()` to `common/qapu_common/domain/synthesized_metrics.py` (following the same partial-update/cache-invalidation pattern every other domain class in this session uses), then wired all four into `services/api/src/routers/hermes.py` and the CLI. `--required-variables`/`--conditions` take comma-separated/JSON-string input respectively (`Required_Variables` is a plain list, `Conditions`/`Calculation` are JSON columns) - `--calculation`/`--conditions` are validated as JSON client-side before the request goes out, with a clear error instead of a confusing 400 from the API. New unit tests in `common/tests/test_synthesized_metrics.py` (detail/add/update/delete, 10 new cases) - 502 tests passing.
|
|
207
|
+
|
|
208
|
+
**Two real production findings surfaced while live-testing this, neither a bug in the new code, both since resolved (see CLAUDE.md's 2026-09-03 entries for the full incident/recovery detail)**:
|
|
209
|
+
1. **`synthesis_variable_rules` turned out to be completely empty in production at the time** - `qapu synthesis list` had been showing 12 rules, but that was a stale Redis cache with nothing backing it in the actual table. Force-refreshing that cache to verify the DB state (`Synthesized_Metrics().get_list(update=True)`) deleted the cache key without anything to refill it from, since the DB read came back empty - and since `synthesize_measurements()` (`common/qapu_common/domain/measurement.py`, called on every real device packet by `services/data`) reads through that same cache, this briefly took real, actively-used synthesis logic out of production, not just cleared a harmless stale copy. **Recovered same day**: the user pulled a dump of the equivalent table from the legacy monolith's own (separate, pre-migration) database via pgAdmin and handed over 9 real rule definitions (weather-related ones deliberately excluded), re-inserted via `Synthesized_Metrics().add()` - formulas/required-variables/conditions/min-max/description preserved verbatim. A real ordering bug surfaced in the restored data itself (not caused by the restore) - `PF_IMB` depends on `PF_AVG` but was ID-ordered before it in the legacy dump, so it was silently skipped every evaluation; fixed by re-inserting `PF_AVG` before `PF_IMB` so ID order matches the dependency. All 9 rules verified correct via a real `calculate_synthesized_metrics()` run.
|
|
210
|
+
2. **A "global" rule (`Device_ID="0"`, applied to every device) could not be added at all** - `synthesis_variable_rules.device_id` is a foreign key into `devices.id`, and there was no `id='0'` placeholder row in `devices` (unlike `projects`/`statuses`, which do have a real `id=0` "Unknown" row). **Fixed same day, per explicit user request**: added `devices.id='0'` ("Unknown Device", `Status_ID`/`Project_ID` both `0`, both already real "Unknown" rows) via `Device().add()`; `Device.list()` (`common/qapu_common/domain/device.py`) updated to exclude `id="0"` (same exclusion `Project.list()` already applies to its own Unknown row) so this placeholder never appears in `qapu device list`/`GET /hermes/devices` as if it were a real device.
|
|
211
|
+
|
|
212
|
+
Live-tested against real prod data throughout: the original add→get→update→delete→get(404) cycle on a disposable rule (before the empty-table discovery); the 9-rule restore verified both via `synthesis list` and a real `calculate_synthesized_metrics()` computation; `device list` confirmed to still show exactly 11 real devices, `"0"` excluded.
|
|
213
|
+
|
|
214
|
+
**`--priority` added the same day, per direct user request** ("priority sırasını sen hesaplanacak verilere göre düzenle") - rules are evaluated in `(Priority, ID)` order, and a rule can only see another rule's output if that rule already ran, so relying on insertion (ID) order alone is fragile. `synthesis_variable_rules.priority` turned out to already be a real DB column (`integer NOT NULL DEFAULT 1`) that `common/qapu_common/database/models.py`'s ORM model simply never declared - `get_list()` had been hardcoding `Priority=1` the whole time rather than reading it. Fixed by adding the column to the model; `get_list()`/`add()`/`update()` now read/write it for real. The 9 restored rules were backfilled with real values (independent rules = `1`, one-level-dependent rules = `2` - `VRMS_IMB`/`IRMS_IMB`/`PF_IMB`).
|
|
215
|
+
|
|
216
|
+
**Two real bugs found and fixed live while backfilling those priorities**: `Schema.Synthesis_Variable_Schema.Priority` defaulted to `1` and `.Status` defaulted to `False` (neither `None`) - `update()`'s partial-update logic treats any non-`None` value as "change this," so a Priority-only update silently reset every rule's `Status` to `False` (caught immediately: "kanka bu arada status false yapmışsın"). Fixed both defaults to `None`. A follow-up Status-only fix then *appeared* to reset every `Priority` back to `1` when read through the CLI - that one turned out to be a stale Redis cache read, not a third instance of the bug (a raw DB query confirmed `Priority` was correct throughout; `get_list(update=True)` fixed the CLI's view) - worth remembering that this system's live pipeline (`services/data`, evaluating rules on every real packet) reads through the same cache, so hand-editing rows can produce a confusing stale-vs-real mismatch mid-edit. Final state verified three ways (raw DB, forced cache refresh, CLI): all 9 rules `Status=True`, correct `Priority` split (6×`1`, 3×`2`).
|
|
217
|
+
|
|
218
|
+
**(From the original 2026-09-02 read-only version)** `synthesis list` supports `--device-id`/`--variable-id`/`--active`/`--inactive` filters. A real CLI bug was caught while first testing it: `Calculation` is a JSON object (e.g. `{"formula": "(VRMS_R + VRMS_S + VRMS_T) / 3"}`), not a string - the table crashed with `NotRenderableError: unable to render dict` until values were `json.dumps()`-ed before being handed to the table row.
|
|
219
|
+
|
|
203
220
|
**`project` (added 2026-09-02) is full read/write**, per an analyst-workflow review of the CLI ("Cinga ve diğer ürün/aile ayrımlarında proje bazlı gezinme lazım") - `Project` (`common/qapu_common/domain/project.py`) already had full CRUD (list/detail/add/update/delete) with zero Hermes/CLI wiring at all until now, same "remaining domain, wire it up" pattern as every other catalog this session. Same low-sensitivity reasoning as `group`/`manufacturer`/`calibration`: a plain project catalog (Cinga/WeatherStat/...), no device actuation, no financial/PII. `project devices <project_id>` returns full device detail (not just IDs, unlike `group devices`) via `Device.list(project_id=...)`, which already existed and already returns the full `Device_Detail_Schema` shape - more useful for an analyst than a bare ID list. Live-tested against real prod data: `project list` correctly showed the one real active project (`Cinga V1`, `id=1` - the `id=0` "Unknown" row is deliberately excluded by `Project.list()` itself); `project devices 1 --limit 3` correctly showed 3 real devices; a full add→get(confirm)→update(confirm)→delete→get(404) cycle on a disposable test project, the real `Cinga V1` row never touched.
|
|
204
221
|
|
|
205
222
|
```bash
|
|
@@ -247,8 +264,6 @@ These six catalogs (`manufacturer`/`model`/`modem`/`sim`/`firmware`/`status`) pl
|
|
|
247
264
|
|
|
248
265
|
**`stream get`'s detail was extended the same day**, per user request: `Command_ID` (a bare FK integer, not meaningful on its own) is now resolved and returned as `Command_Name` instead (via `qapu_common.domain.command.Command.detail()`); a new `Variable_Count` field reports how many variables `Stream.get_stream_measurements()` actually recorded from that packet - not previously visible from `stream get` at all. `Size` is now rendered as e.g. `418 byte` in both `stream get` and `stream list`'s table (the raw `--json` output stays a plain int, only the human-readable rendering changed). The Hermes endpoint's `response_model` was dropped to `None` for this route specifically, since the response shape now deliberately diverges from the underlying `Stream_Schema` (swapped field, added field) - every other Hermes route keeps its typed `response_model`, this is a one-off. Live-tested against real prod data: `stream get 4879335` correctly showed `Command_Name: Timed` and `Variable_Count: 18`.
|
|
249
266
|
|
|
250
|
-
**`synthesis` (added 2026-09-02) is read-only, `list` only** - `synthesis_variable_rules` (how a derived metric like `VRMS_A`/`AE_TOT`/power-factor-imbalance gets calculated from raw variables) has no `add()`/`update()`/`delete()` on the domain class at all, it's a small, directly-DB-managed rule set. `Device_ID='0'` rows are global (applied to every device); a real device ID scopes a rule to just that device. `synthesis list` supports `--device-id`/`--variable-id`/`--active`/`--inactive` filters, showing ID/Device/Variable/Priority/Required Variables/Calculation/Active in a table. **Real CLI bug found and fixed while testing this**: `Calculation` is a JSON object (the actual formula, e.g. `{"formula": "(VRMS_R + VRMS_S + VRMS_T) / 3"}`), not a string - the first version of this table crashed with `NotRenderableError: unable to render dict` the moment it hit a real row; fixed by `json.dumps()`-ing dict values before handing them to the table. Live-tested against real prod data after the fix: all 12 real global synthesis rules rendered correctly (readable formulas), `--active`/`--variable-id` filters both confirmed correct counts.
|
|
251
|
-
|
|
252
267
|
**`user-role` (added 2026-09-02) is the ONLY piece of the `user` domain wired up here, and deliberately so** - the user was asked explicitly first, since `user`/`finance` had been excluded from this router since it was first built (a leaked `QAPU_HERMES_KEY` - a shared secret, not real per-admin JWT - must never expose PII or financial data). Chose "read-only, PII-free fields only" as the safe subset: `User_Role` (`common/qapu_common/domain/user.py`) is a plain ID/Name/Description role catalog (admin/farmer/technician/...) with zero PII, structurally identical to `crop-type`/`pump-type`. The actual `User` class (name/phone/email) and `Authorization` (which user owns which device) are NOT wired up and were never considered - PII and device-ownership data stay off this router entirely, same as `finance`.
|
|
253
268
|
- **Real bug found and fixed while testing this**: `User_Role.add()` requires an explicit `ID` - unlike `Status` (which falls back to auto-increment when `ID` is omitted), `user_roles.id` is not an auto-increment column at all (`Column(Integer, primary_key=True, nullable=False)`, no `autoincrement`). The first CLI version only took `Name`/`--description`, so every `add` failed with a misleading `"User role Name is required"` (Name *was* provided - `ID` was the actual missing field). Fixed by making `id` a required positional argument on `user-role add` and clarifying the Hermes error message. Live-tested end to end against real prod data (10 real roles - admin/supervisor/technician/field_technician/accountant/dealer/farmer/farmer_employee/farmer_electrician/demo_user, confirmed zero PII, none touched): a full add(explicit ID `999`)→get→update→get(confirm)→delete→get(404) cycle on a disposable row.
|
|
254
269
|
|
|
@@ -170,7 +170,10 @@ qapu stream list <device_id> [--page N] [--size N] [--json] # GET /hermes/stre
|
|
|
170
170
|
qapu stream get <stream_id> # GET /hermes/stream/detail/{stream_id}
|
|
171
171
|
|
|
172
172
|
qapu synthesis list [--device-id] [--variable-id] [--active/--inactive] [--json] # GET /hermes/synthesis - derived-metric calculation rules
|
|
173
|
-
|
|
173
|
+
qapu synthesis get <rule_id> [--json] # GET /hermes/synthesis/{rule_id}
|
|
174
|
+
qapu synthesis add <variable_id> --required-variables A,B,C --calculation '{"formula": "A + B"}' [--device-id] [--conditions] [--min-value] [--max-value] [--priority] [--active/--inactive] [--description] # POST /hermes/synthesis
|
|
175
|
+
qapu synthesis update <rule_id> [--variable-id] [--required-variables] [--calculation] [--device-id] [--conditions] [--min-value] [--max-value] [--priority] [--active/--inactive] [--description] # PUT /hermes/synthesis/{rule_id}
|
|
176
|
+
qapu synthesis delete <rule_id> # DELETE /hermes/synthesis/{rule_id}
|
|
174
177
|
qapu user-role list [--json] [--limit N] # GET /hermes/user_roles - e.g. admin/farmer/technician, NOT the User class itself (PII, deliberately excluded)
|
|
175
178
|
qapu user-role get <role_id> [--json] # GET /hermes/user_roles/{id}
|
|
176
179
|
qapu user-role add <id> "Name" [--description] # POST /hermes/user_roles - ID is REQUIRED, this catalog's ID is NOT auto-increment
|
|
@@ -188,6 +191,20 @@ qapu project update <project_id> [--name] [--description] [--active/--inactive]
|
|
|
188
191
|
qapu project delete <project_id> # DELETE /hermes/projects/{project_id}
|
|
189
192
|
```
|
|
190
193
|
|
|
194
|
+
**`synthesis` extended to full CRUD (2026-09-03)**, per direct user correction after the read-only version shipped the day before ("qapu synthesis için tam crud yapmamışsın sadece list var"). The 2026-09-02 read-only version was a faithful reflection of the domain class at the time - `Synthesized_Metrics` genuinely had no `add()`/`update()`/`delete()`, only `get_list()`. Added `detail()`/`add()`/`update()`/`delete()` to `common/qapu_common/domain/synthesized_metrics.py` (following the same partial-update/cache-invalidation pattern every other domain class in this session uses), then wired all four into `services/api/src/routers/hermes.py` and the CLI. `--required-variables`/`--conditions` take comma-separated/JSON-string input respectively (`Required_Variables` is a plain list, `Conditions`/`Calculation` are JSON columns) - `--calculation`/`--conditions` are validated as JSON client-side before the request goes out, with a clear error instead of a confusing 400 from the API. New unit tests in `common/tests/test_synthesized_metrics.py` (detail/add/update/delete, 10 new cases) - 502 tests passing.
|
|
195
|
+
|
|
196
|
+
**Two real production findings surfaced while live-testing this, neither a bug in the new code, both since resolved (see CLAUDE.md's 2026-09-03 entries for the full incident/recovery detail)**:
|
|
197
|
+
1. **`synthesis_variable_rules` turned out to be completely empty in production at the time** - `qapu synthesis list` had been showing 12 rules, but that was a stale Redis cache with nothing backing it in the actual table. Force-refreshing that cache to verify the DB state (`Synthesized_Metrics().get_list(update=True)`) deleted the cache key without anything to refill it from, since the DB read came back empty - and since `synthesize_measurements()` (`common/qapu_common/domain/measurement.py`, called on every real device packet by `services/data`) reads through that same cache, this briefly took real, actively-used synthesis logic out of production, not just cleared a harmless stale copy. **Recovered same day**: the user pulled a dump of the equivalent table from the legacy monolith's own (separate, pre-migration) database via pgAdmin and handed over 9 real rule definitions (weather-related ones deliberately excluded), re-inserted via `Synthesized_Metrics().add()` - formulas/required-variables/conditions/min-max/description preserved verbatim. A real ordering bug surfaced in the restored data itself (not caused by the restore) - `PF_IMB` depends on `PF_AVG` but was ID-ordered before it in the legacy dump, so it was silently skipped every evaluation; fixed by re-inserting `PF_AVG` before `PF_IMB` so ID order matches the dependency. All 9 rules verified correct via a real `calculate_synthesized_metrics()` run.
|
|
198
|
+
2. **A "global" rule (`Device_ID="0"`, applied to every device) could not be added at all** - `synthesis_variable_rules.device_id` is a foreign key into `devices.id`, and there was no `id='0'` placeholder row in `devices` (unlike `projects`/`statuses`, which do have a real `id=0` "Unknown" row). **Fixed same day, per explicit user request**: added `devices.id='0'` ("Unknown Device", `Status_ID`/`Project_ID` both `0`, both already real "Unknown" rows) via `Device().add()`; `Device.list()` (`common/qapu_common/domain/device.py`) updated to exclude `id="0"` (same exclusion `Project.list()` already applies to its own Unknown row) so this placeholder never appears in `qapu device list`/`GET /hermes/devices` as if it were a real device.
|
|
199
|
+
|
|
200
|
+
Live-tested against real prod data throughout: the original add→get→update→delete→get(404) cycle on a disposable rule (before the empty-table discovery); the 9-rule restore verified both via `synthesis list` and a real `calculate_synthesized_metrics()` computation; `device list` confirmed to still show exactly 11 real devices, `"0"` excluded.
|
|
201
|
+
|
|
202
|
+
**`--priority` added the same day, per direct user request** ("priority sırasını sen hesaplanacak verilere göre düzenle") - rules are evaluated in `(Priority, ID)` order, and a rule can only see another rule's output if that rule already ran, so relying on insertion (ID) order alone is fragile. `synthesis_variable_rules.priority` turned out to already be a real DB column (`integer NOT NULL DEFAULT 1`) that `common/qapu_common/database/models.py`'s ORM model simply never declared - `get_list()` had been hardcoding `Priority=1` the whole time rather than reading it. Fixed by adding the column to the model; `get_list()`/`add()`/`update()` now read/write it for real. The 9 restored rules were backfilled with real values (independent rules = `1`, one-level-dependent rules = `2` - `VRMS_IMB`/`IRMS_IMB`/`PF_IMB`).
|
|
203
|
+
|
|
204
|
+
**Two real bugs found and fixed live while backfilling those priorities**: `Schema.Synthesis_Variable_Schema.Priority` defaulted to `1` and `.Status` defaulted to `False` (neither `None`) - `update()`'s partial-update logic treats any non-`None` value as "change this," so a Priority-only update silently reset every rule's `Status` to `False` (caught immediately: "kanka bu arada status false yapmışsın"). Fixed both defaults to `None`. A follow-up Status-only fix then *appeared* to reset every `Priority` back to `1` when read through the CLI - that one turned out to be a stale Redis cache read, not a third instance of the bug (a raw DB query confirmed `Priority` was correct throughout; `get_list(update=True)` fixed the CLI's view) - worth remembering that this system's live pipeline (`services/data`, evaluating rules on every real packet) reads through the same cache, so hand-editing rows can produce a confusing stale-vs-real mismatch mid-edit. Final state verified three ways (raw DB, forced cache refresh, CLI): all 9 rules `Status=True`, correct `Priority` split (6×`1`, 3×`2`).
|
|
205
|
+
|
|
206
|
+
**(From the original 2026-09-02 read-only version)** `synthesis list` supports `--device-id`/`--variable-id`/`--active`/`--inactive` filters. A real CLI bug was caught while first testing it: `Calculation` is a JSON object (e.g. `{"formula": "(VRMS_R + VRMS_S + VRMS_T) / 3"}`), not a string - the table crashed with `NotRenderableError: unable to render dict` until values were `json.dumps()`-ed before being handed to the table row.
|
|
207
|
+
|
|
191
208
|
**`project` (added 2026-09-02) is full read/write**, per an analyst-workflow review of the CLI ("Cinga ve diğer ürün/aile ayrımlarında proje bazlı gezinme lazım") - `Project` (`common/qapu_common/domain/project.py`) already had full CRUD (list/detail/add/update/delete) with zero Hermes/CLI wiring at all until now, same "remaining domain, wire it up" pattern as every other catalog this session. Same low-sensitivity reasoning as `group`/`manufacturer`/`calibration`: a plain project catalog (Cinga/WeatherStat/...), no device actuation, no financial/PII. `project devices <project_id>` returns full device detail (not just IDs, unlike `group devices`) via `Device.list(project_id=...)`, which already existed and already returns the full `Device_Detail_Schema` shape - more useful for an analyst than a bare ID list. Live-tested against real prod data: `project list` correctly showed the one real active project (`Cinga V1`, `id=1` - the `id=0` "Unknown" row is deliberately excluded by `Project.list()` itself); `project devices 1 --limit 3` correctly showed 3 real devices; a full add→get(confirm)→update(confirm)→delete→get(404) cycle on a disposable test project, the real `Cinga V1` row never touched.
|
|
192
209
|
|
|
193
210
|
```bash
|
|
@@ -235,8 +252,6 @@ These six catalogs (`manufacturer`/`model`/`modem`/`sim`/`firmware`/`status`) pl
|
|
|
235
252
|
|
|
236
253
|
**`stream get`'s detail was extended the same day**, per user request: `Command_ID` (a bare FK integer, not meaningful on its own) is now resolved and returned as `Command_Name` instead (via `qapu_common.domain.command.Command.detail()`); a new `Variable_Count` field reports how many variables `Stream.get_stream_measurements()` actually recorded from that packet - not previously visible from `stream get` at all. `Size` is now rendered as e.g. `418 byte` in both `stream get` and `stream list`'s table (the raw `--json` output stays a plain int, only the human-readable rendering changed). The Hermes endpoint's `response_model` was dropped to `None` for this route specifically, since the response shape now deliberately diverges from the underlying `Stream_Schema` (swapped field, added field) - every other Hermes route keeps its typed `response_model`, this is a one-off. Live-tested against real prod data: `stream get 4879335` correctly showed `Command_Name: Timed` and `Variable_Count: 18`.
|
|
237
254
|
|
|
238
|
-
**`synthesis` (added 2026-09-02) is read-only, `list` only** - `synthesis_variable_rules` (how a derived metric like `VRMS_A`/`AE_TOT`/power-factor-imbalance gets calculated from raw variables) has no `add()`/`update()`/`delete()` on the domain class at all, it's a small, directly-DB-managed rule set. `Device_ID='0'` rows are global (applied to every device); a real device ID scopes a rule to just that device. `synthesis list` supports `--device-id`/`--variable-id`/`--active`/`--inactive` filters, showing ID/Device/Variable/Priority/Required Variables/Calculation/Active in a table. **Real CLI bug found and fixed while testing this**: `Calculation` is a JSON object (the actual formula, e.g. `{"formula": "(VRMS_R + VRMS_S + VRMS_T) / 3"}`), not a string - the first version of this table crashed with `NotRenderableError: unable to render dict` the moment it hit a real row; fixed by `json.dumps()`-ing dict values before handing them to the table. Live-tested against real prod data after the fix: all 12 real global synthesis rules rendered correctly (readable formulas), `--active`/`--variable-id` filters both confirmed correct counts.
|
|
239
|
-
|
|
240
255
|
**`user-role` (added 2026-09-02) is the ONLY piece of the `user` domain wired up here, and deliberately so** - the user was asked explicitly first, since `user`/`finance` had been excluded from this router since it was first built (a leaked `QAPU_HERMES_KEY` - a shared secret, not real per-admin JWT - must never expose PII or financial data). Chose "read-only, PII-free fields only" as the safe subset: `User_Role` (`common/qapu_common/domain/user.py`) is a plain ID/Name/Description role catalog (admin/farmer/technician/...) with zero PII, structurally identical to `crop-type`/`pump-type`. The actual `User` class (name/phone/email) and `Authorization` (which user owns which device) are NOT wired up and were never considered - PII and device-ownership data stay off this router entirely, same as `finance`.
|
|
241
256
|
- **Real bug found and fixed while testing this**: `User_Role.add()` requires an explicit `ID` - unlike `Status` (which falls back to auto-increment when `ID` is omitted), `user_roles.id` is not an auto-increment column at all (`Column(Integer, primary_key=True, nullable=False)`, no `autoincrement`). The first CLI version only took `Name`/`--description`, so every `add` failed with a misleading `"User role Name is required"` (Name *was* provided - `ID` was the actual missing field). Fixed by making `id` a required positional argument on `user-role add` and clarifying the Hermes error message. Live-tested end to end against real prod data (10 real roles - admin/supervisor/technician/field_technician/accountant/dealer/farmer/farmer_employee/farmer_electrician/demo_user, confirmed zero PII, none touched): a full add(explicit ID `999`)→get→update→get(confirm)→delete→get(404) cycle on a disposable row.
|
|
242
257
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "qapu-cli"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.4"
|
|
4
4
|
description = "CLI client for the Qapu API - built for the Hermes agent, but usable by anyone talking to api.ovoo.com.tr from outside the Swarm."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -2689,6 +2689,126 @@ def synthesis_list(
|
|
|
2689
2689
|
console.print(table)
|
|
2690
2690
|
|
|
2691
2691
|
|
|
2692
|
+
# Get One Synthesis Rule's Detail
|
|
2693
|
+
@synthesis_app.command("get")
|
|
2694
|
+
def synthesis_get(rule_id: int = typer.Argument(...), as_json: bool = typer.Option(False, "--json")):
|
|
2695
|
+
"""Get one synthesis rule's detail (GET /hermes/synthesis/{rule_id})."""
|
|
2696
|
+
result = client.get(f"/hermes/synthesis/{rule_id}")
|
|
2697
|
+
if as_json:
|
|
2698
|
+
typer.echo(json.dumps(result, indent=2))
|
|
2699
|
+
return
|
|
2700
|
+
_print_detail(result)
|
|
2701
|
+
|
|
2702
|
+
|
|
2703
|
+
# Add a New Synthesis Rule
|
|
2704
|
+
@synthesis_app.command("add")
|
|
2705
|
+
def synthesis_add(
|
|
2706
|
+
variable_id: str = typer.Argument(..., help="Variable ID this rule calculates, e.g. 'P_TOT'."),
|
|
2707
|
+
required_variables: str = typer.Option(..., "--required-variables", help="Comma-separated list of variables the formula needs, e.g. P_R,P_S,P_T."),
|
|
2708
|
+
calculation: str = typer.Option(..., "--calculation", help='Calculation formula as JSON, e.g. \'{"formula": "P_R + P_S"}\'.'),
|
|
2709
|
+
device_id: Optional[str] = typer.Option(None, "--device-id", help="Scope to one device instead of every device (default: global, Device_ID='0')."),
|
|
2710
|
+
conditions: Optional[str] = typer.Option(None, "--conditions", help='Conditions as a JSON list, e.g. \'[{"Variable":"X","Condition":">","Value":5}]\'.'),
|
|
2711
|
+
min_value: Optional[float] = typer.Option(None, "--min-value"),
|
|
2712
|
+
max_value: Optional[float] = typer.Option(None, "--max-value"),
|
|
2713
|
+
priority: int = typer.Option(1, "--priority", help="Evaluation order - lower runs first. Must be higher than any rule this one depends on via --required-variables, or that dependency won't be computed yet when this one runs."),
|
|
2714
|
+
active: bool = typer.Option(False, "--active/--inactive"),
|
|
2715
|
+
description: Optional[str] = typer.Option(None, "--description"),
|
|
2716
|
+
):
|
|
2717
|
+
"""Create a new synthesis rule (POST /hermes/synthesis)."""
|
|
2718
|
+
try:
|
|
2719
|
+
calculation_data = json.loads(calculation)
|
|
2720
|
+
except json.JSONDecodeError:
|
|
2721
|
+
typer.echo("Error: --calculation must be valid JSON.", err=True)
|
|
2722
|
+
raise typer.Exit(code=1)
|
|
2723
|
+
conditions_data = None
|
|
2724
|
+
if conditions is not None:
|
|
2725
|
+
try:
|
|
2726
|
+
conditions_data = json.loads(conditions)
|
|
2727
|
+
except json.JSONDecodeError:
|
|
2728
|
+
typer.echo("Error: --conditions must be valid JSON.", err=True)
|
|
2729
|
+
raise typer.Exit(code=1)
|
|
2730
|
+
body: dict[str, Any] = {
|
|
2731
|
+
"Variable_ID": variable_id,
|
|
2732
|
+
"Required_Variables": [v.strip() for v in required_variables.split(",") if v.strip()],
|
|
2733
|
+
"Calculation": calculation_data,
|
|
2734
|
+
"Status": active,
|
|
2735
|
+
}
|
|
2736
|
+
if device_id is not None:
|
|
2737
|
+
body["Device_ID"] = device_id
|
|
2738
|
+
if conditions_data is not None:
|
|
2739
|
+
body["Conditions"] = conditions_data
|
|
2740
|
+
if min_value is not None:
|
|
2741
|
+
body["Minimum_Value"] = min_value
|
|
2742
|
+
if max_value is not None:
|
|
2743
|
+
body["Maximum_Value"] = max_value
|
|
2744
|
+
if priority is not None:
|
|
2745
|
+
body["Priority"] = priority
|
|
2746
|
+
if description is not None:
|
|
2747
|
+
body["Description"] = description
|
|
2748
|
+
result = client.post("/hermes/synthesis", json_body=body)
|
|
2749
|
+
typer.echo(result.get("Message", result))
|
|
2750
|
+
|
|
2751
|
+
|
|
2752
|
+
# Update an Existing Synthesis Rule
|
|
2753
|
+
@synthesis_app.command("update")
|
|
2754
|
+
def synthesis_update(
|
|
2755
|
+
rule_id: int = typer.Argument(...),
|
|
2756
|
+
variable_id: Optional[str] = typer.Option(None, "--variable-id"),
|
|
2757
|
+
required_variables: Optional[str] = typer.Option(None, "--required-variables", help="Comma-separated list, replaces the existing list entirely."),
|
|
2758
|
+
calculation: Optional[str] = typer.Option(None, "--calculation", help='Calculation formula as JSON, e.g. \'{"formula": "P_R + P_S"}\'.'),
|
|
2759
|
+
device_id: Optional[str] = typer.Option(None, "--device-id"),
|
|
2760
|
+
conditions: Optional[str] = typer.Option(None, "--conditions", help="Conditions as a JSON list, replaces the existing list entirely."),
|
|
2761
|
+
min_value: Optional[float] = typer.Option(None, "--min-value"),
|
|
2762
|
+
max_value: Optional[float] = typer.Option(None, "--max-value"),
|
|
2763
|
+
priority: Optional[int] = typer.Option(None, "--priority", help="Evaluation order - lower runs first."),
|
|
2764
|
+
active: Optional[bool] = typer.Option(None, "--active/--inactive"),
|
|
2765
|
+
description: Optional[str] = typer.Option(None, "--description"),
|
|
2766
|
+
):
|
|
2767
|
+
"""Update an existing synthesis rule - only the fields passed are changed (PUT /hermes/synthesis/{rule_id})."""
|
|
2768
|
+
body: dict[str, Any] = {}
|
|
2769
|
+
if variable_id is not None:
|
|
2770
|
+
body["Variable_ID"] = variable_id
|
|
2771
|
+
if required_variables is not None:
|
|
2772
|
+
body["Required_Variables"] = [v.strip() for v in required_variables.split(",") if v.strip()]
|
|
2773
|
+
if calculation is not None:
|
|
2774
|
+
try:
|
|
2775
|
+
body["Calculation"] = json.loads(calculation)
|
|
2776
|
+
except json.JSONDecodeError:
|
|
2777
|
+
typer.echo("Error: --calculation must be valid JSON.", err=True)
|
|
2778
|
+
raise typer.Exit(code=1)
|
|
2779
|
+
if device_id is not None:
|
|
2780
|
+
body["Device_ID"] = device_id
|
|
2781
|
+
if conditions is not None:
|
|
2782
|
+
try:
|
|
2783
|
+
body["Conditions"] = json.loads(conditions)
|
|
2784
|
+
except json.JSONDecodeError:
|
|
2785
|
+
typer.echo("Error: --conditions must be valid JSON.", err=True)
|
|
2786
|
+
raise typer.Exit(code=1)
|
|
2787
|
+
if min_value is not None:
|
|
2788
|
+
body["Minimum_Value"] = min_value
|
|
2789
|
+
if max_value is not None:
|
|
2790
|
+
body["Maximum_Value"] = max_value
|
|
2791
|
+
if priority is not None:
|
|
2792
|
+
body["Priority"] = priority
|
|
2793
|
+
if active is not None:
|
|
2794
|
+
body["Status"] = active
|
|
2795
|
+
if description is not None:
|
|
2796
|
+
body["Description"] = description
|
|
2797
|
+
if not body:
|
|
2798
|
+
typer.echo("Error: nothing to update - pass at least one field.", err=True)
|
|
2799
|
+
raise typer.Exit(code=1)
|
|
2800
|
+
result = client.put(f"/hermes/synthesis/{rule_id}", json_body=body)
|
|
2801
|
+
typer.echo(result.get("Message", result))
|
|
2802
|
+
|
|
2803
|
+
|
|
2804
|
+
# Delete an Existing Synthesis Rule
|
|
2805
|
+
@synthesis_app.command("delete")
|
|
2806
|
+
def synthesis_delete(rule_id: int = typer.Argument(...)):
|
|
2807
|
+
"""Delete an existing synthesis rule (DELETE /hermes/synthesis/{rule_id})."""
|
|
2808
|
+
result = client.delete(f"/hermes/synthesis/{rule_id}")
|
|
2809
|
+
typer.echo(result.get("Message", result))
|
|
2810
|
+
|
|
2811
|
+
|
|
2692
2812
|
# List User Roles
|
|
2693
2813
|
@user_role_app.command("list")
|
|
2694
2814
|
def user_role_list(as_json: bool = typer.Option(False, "--json"), limit: Optional[int] = typer.Option(None, "--limit")):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qapu-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: CLI client for the Qapu API - built for the Hermes agent, but usable by anyone talking to api.ovoo.com.tr from outside the Swarm.
|
|
5
5
|
Author: OVOO Technology
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -182,7 +182,10 @@ qapu stream list <device_id> [--page N] [--size N] [--json] # GET /hermes/stre
|
|
|
182
182
|
qapu stream get <stream_id> # GET /hermes/stream/detail/{stream_id}
|
|
183
183
|
|
|
184
184
|
qapu synthesis list [--device-id] [--variable-id] [--active/--inactive] [--json] # GET /hermes/synthesis - derived-metric calculation rules
|
|
185
|
-
|
|
185
|
+
qapu synthesis get <rule_id> [--json] # GET /hermes/synthesis/{rule_id}
|
|
186
|
+
qapu synthesis add <variable_id> --required-variables A,B,C --calculation '{"formula": "A + B"}' [--device-id] [--conditions] [--min-value] [--max-value] [--priority] [--active/--inactive] [--description] # POST /hermes/synthesis
|
|
187
|
+
qapu synthesis update <rule_id> [--variable-id] [--required-variables] [--calculation] [--device-id] [--conditions] [--min-value] [--max-value] [--priority] [--active/--inactive] [--description] # PUT /hermes/synthesis/{rule_id}
|
|
188
|
+
qapu synthesis delete <rule_id> # DELETE /hermes/synthesis/{rule_id}
|
|
186
189
|
qapu user-role list [--json] [--limit N] # GET /hermes/user_roles - e.g. admin/farmer/technician, NOT the User class itself (PII, deliberately excluded)
|
|
187
190
|
qapu user-role get <role_id> [--json] # GET /hermes/user_roles/{id}
|
|
188
191
|
qapu user-role add <id> "Name" [--description] # POST /hermes/user_roles - ID is REQUIRED, this catalog's ID is NOT auto-increment
|
|
@@ -200,6 +203,20 @@ qapu project update <project_id> [--name] [--description] [--active/--inactive]
|
|
|
200
203
|
qapu project delete <project_id> # DELETE /hermes/projects/{project_id}
|
|
201
204
|
```
|
|
202
205
|
|
|
206
|
+
**`synthesis` extended to full CRUD (2026-09-03)**, per direct user correction after the read-only version shipped the day before ("qapu synthesis için tam crud yapmamışsın sadece list var"). The 2026-09-02 read-only version was a faithful reflection of the domain class at the time - `Synthesized_Metrics` genuinely had no `add()`/`update()`/`delete()`, only `get_list()`. Added `detail()`/`add()`/`update()`/`delete()` to `common/qapu_common/domain/synthesized_metrics.py` (following the same partial-update/cache-invalidation pattern every other domain class in this session uses), then wired all four into `services/api/src/routers/hermes.py` and the CLI. `--required-variables`/`--conditions` take comma-separated/JSON-string input respectively (`Required_Variables` is a plain list, `Conditions`/`Calculation` are JSON columns) - `--calculation`/`--conditions` are validated as JSON client-side before the request goes out, with a clear error instead of a confusing 400 from the API. New unit tests in `common/tests/test_synthesized_metrics.py` (detail/add/update/delete, 10 new cases) - 502 tests passing.
|
|
207
|
+
|
|
208
|
+
**Two real production findings surfaced while live-testing this, neither a bug in the new code, both since resolved (see CLAUDE.md's 2026-09-03 entries for the full incident/recovery detail)**:
|
|
209
|
+
1. **`synthesis_variable_rules` turned out to be completely empty in production at the time** - `qapu synthesis list` had been showing 12 rules, but that was a stale Redis cache with nothing backing it in the actual table. Force-refreshing that cache to verify the DB state (`Synthesized_Metrics().get_list(update=True)`) deleted the cache key without anything to refill it from, since the DB read came back empty - and since `synthesize_measurements()` (`common/qapu_common/domain/measurement.py`, called on every real device packet by `services/data`) reads through that same cache, this briefly took real, actively-used synthesis logic out of production, not just cleared a harmless stale copy. **Recovered same day**: the user pulled a dump of the equivalent table from the legacy monolith's own (separate, pre-migration) database via pgAdmin and handed over 9 real rule definitions (weather-related ones deliberately excluded), re-inserted via `Synthesized_Metrics().add()` - formulas/required-variables/conditions/min-max/description preserved verbatim. A real ordering bug surfaced in the restored data itself (not caused by the restore) - `PF_IMB` depends on `PF_AVG` but was ID-ordered before it in the legacy dump, so it was silently skipped every evaluation; fixed by re-inserting `PF_AVG` before `PF_IMB` so ID order matches the dependency. All 9 rules verified correct via a real `calculate_synthesized_metrics()` run.
|
|
210
|
+
2. **A "global" rule (`Device_ID="0"`, applied to every device) could not be added at all** - `synthesis_variable_rules.device_id` is a foreign key into `devices.id`, and there was no `id='0'` placeholder row in `devices` (unlike `projects`/`statuses`, which do have a real `id=0` "Unknown" row). **Fixed same day, per explicit user request**: added `devices.id='0'` ("Unknown Device", `Status_ID`/`Project_ID` both `0`, both already real "Unknown" rows) via `Device().add()`; `Device.list()` (`common/qapu_common/domain/device.py`) updated to exclude `id="0"` (same exclusion `Project.list()` already applies to its own Unknown row) so this placeholder never appears in `qapu device list`/`GET /hermes/devices` as if it were a real device.
|
|
211
|
+
|
|
212
|
+
Live-tested against real prod data throughout: the original add→get→update→delete→get(404) cycle on a disposable rule (before the empty-table discovery); the 9-rule restore verified both via `synthesis list` and a real `calculate_synthesized_metrics()` computation; `device list` confirmed to still show exactly 11 real devices, `"0"` excluded.
|
|
213
|
+
|
|
214
|
+
**`--priority` added the same day, per direct user request** ("priority sırasını sen hesaplanacak verilere göre düzenle") - rules are evaluated in `(Priority, ID)` order, and a rule can only see another rule's output if that rule already ran, so relying on insertion (ID) order alone is fragile. `synthesis_variable_rules.priority` turned out to already be a real DB column (`integer NOT NULL DEFAULT 1`) that `common/qapu_common/database/models.py`'s ORM model simply never declared - `get_list()` had been hardcoding `Priority=1` the whole time rather than reading it. Fixed by adding the column to the model; `get_list()`/`add()`/`update()` now read/write it for real. The 9 restored rules were backfilled with real values (independent rules = `1`, one-level-dependent rules = `2` - `VRMS_IMB`/`IRMS_IMB`/`PF_IMB`).
|
|
215
|
+
|
|
216
|
+
**Two real bugs found and fixed live while backfilling those priorities**: `Schema.Synthesis_Variable_Schema.Priority` defaulted to `1` and `.Status` defaulted to `False` (neither `None`) - `update()`'s partial-update logic treats any non-`None` value as "change this," so a Priority-only update silently reset every rule's `Status` to `False` (caught immediately: "kanka bu arada status false yapmışsın"). Fixed both defaults to `None`. A follow-up Status-only fix then *appeared* to reset every `Priority` back to `1` when read through the CLI - that one turned out to be a stale Redis cache read, not a third instance of the bug (a raw DB query confirmed `Priority` was correct throughout; `get_list(update=True)` fixed the CLI's view) - worth remembering that this system's live pipeline (`services/data`, evaluating rules on every real packet) reads through the same cache, so hand-editing rows can produce a confusing stale-vs-real mismatch mid-edit. Final state verified three ways (raw DB, forced cache refresh, CLI): all 9 rules `Status=True`, correct `Priority` split (6×`1`, 3×`2`).
|
|
217
|
+
|
|
218
|
+
**(From the original 2026-09-02 read-only version)** `synthesis list` supports `--device-id`/`--variable-id`/`--active`/`--inactive` filters. A real CLI bug was caught while first testing it: `Calculation` is a JSON object (e.g. `{"formula": "(VRMS_R + VRMS_S + VRMS_T) / 3"}`), not a string - the table crashed with `NotRenderableError: unable to render dict` until values were `json.dumps()`-ed before being handed to the table row.
|
|
219
|
+
|
|
203
220
|
**`project` (added 2026-09-02) is full read/write**, per an analyst-workflow review of the CLI ("Cinga ve diğer ürün/aile ayrımlarında proje bazlı gezinme lazım") - `Project` (`common/qapu_common/domain/project.py`) already had full CRUD (list/detail/add/update/delete) with zero Hermes/CLI wiring at all until now, same "remaining domain, wire it up" pattern as every other catalog this session. Same low-sensitivity reasoning as `group`/`manufacturer`/`calibration`: a plain project catalog (Cinga/WeatherStat/...), no device actuation, no financial/PII. `project devices <project_id>` returns full device detail (not just IDs, unlike `group devices`) via `Device.list(project_id=...)`, which already existed and already returns the full `Device_Detail_Schema` shape - more useful for an analyst than a bare ID list. Live-tested against real prod data: `project list` correctly showed the one real active project (`Cinga V1`, `id=1` - the `id=0` "Unknown" row is deliberately excluded by `Project.list()` itself); `project devices 1 --limit 3` correctly showed 3 real devices; a full add→get(confirm)→update(confirm)→delete→get(404) cycle on a disposable test project, the real `Cinga V1` row never touched.
|
|
204
221
|
|
|
205
222
|
```bash
|
|
@@ -247,8 +264,6 @@ These six catalogs (`manufacturer`/`model`/`modem`/`sim`/`firmware`/`status`) pl
|
|
|
247
264
|
|
|
248
265
|
**`stream get`'s detail was extended the same day**, per user request: `Command_ID` (a bare FK integer, not meaningful on its own) is now resolved and returned as `Command_Name` instead (via `qapu_common.domain.command.Command.detail()`); a new `Variable_Count` field reports how many variables `Stream.get_stream_measurements()` actually recorded from that packet - not previously visible from `stream get` at all. `Size` is now rendered as e.g. `418 byte` in both `stream get` and `stream list`'s table (the raw `--json` output stays a plain int, only the human-readable rendering changed). The Hermes endpoint's `response_model` was dropped to `None` for this route specifically, since the response shape now deliberately diverges from the underlying `Stream_Schema` (swapped field, added field) - every other Hermes route keeps its typed `response_model`, this is a one-off. Live-tested against real prod data: `stream get 4879335` correctly showed `Command_Name: Timed` and `Variable_Count: 18`.
|
|
249
266
|
|
|
250
|
-
**`synthesis` (added 2026-09-02) is read-only, `list` only** - `synthesis_variable_rules` (how a derived metric like `VRMS_A`/`AE_TOT`/power-factor-imbalance gets calculated from raw variables) has no `add()`/`update()`/`delete()` on the domain class at all, it's a small, directly-DB-managed rule set. `Device_ID='0'` rows are global (applied to every device); a real device ID scopes a rule to just that device. `synthesis list` supports `--device-id`/`--variable-id`/`--active`/`--inactive` filters, showing ID/Device/Variable/Priority/Required Variables/Calculation/Active in a table. **Real CLI bug found and fixed while testing this**: `Calculation` is a JSON object (the actual formula, e.g. `{"formula": "(VRMS_R + VRMS_S + VRMS_T) / 3"}`), not a string - the first version of this table crashed with `NotRenderableError: unable to render dict` the moment it hit a real row; fixed by `json.dumps()`-ing dict values before handing them to the table. Live-tested against real prod data after the fix: all 12 real global synthesis rules rendered correctly (readable formulas), `--active`/`--variable-id` filters both confirmed correct counts.
|
|
251
|
-
|
|
252
267
|
**`user-role` (added 2026-09-02) is the ONLY piece of the `user` domain wired up here, and deliberately so** - the user was asked explicitly first, since `user`/`finance` had been excluded from this router since it was first built (a leaked `QAPU_HERMES_KEY` - a shared secret, not real per-admin JWT - must never expose PII or financial data). Chose "read-only, PII-free fields only" as the safe subset: `User_Role` (`common/qapu_common/domain/user.py`) is a plain ID/Name/Description role catalog (admin/farmer/technician/...) with zero PII, structurally identical to `crop-type`/`pump-type`. The actual `User` class (name/phone/email) and `Authorization` (which user owns which device) are NOT wired up and were never considered - PII and device-ownership data stay off this router entirely, same as `finance`.
|
|
253
268
|
- **Real bug found and fixed while testing this**: `User_Role.add()` requires an explicit `ID` - unlike `Status` (which falls back to auto-increment when `ID` is omitted), `user_roles.id` is not an auto-increment column at all (`Column(Integer, primary_key=True, nullable=False)`, no `autoincrement`). The first CLI version only took `Name`/`--description`, so every `add` failed with a misleading `"User role Name is required"` (Name *was* provided - `ID` was the actual missing field). Fixed by making `id` a required positional argument on `user-role add` and clarifying the Hermes error message. Live-tested end to end against real prod data (10 real roles - admin/supervisor/technician/field_technician/accountant/dealer/farmer/farmer_employee/farmer_electrician/demo_user, confirmed zero PII, none touched): a full add(explicit ID `999`)→get→update→get(confirm)→delete→get(404) cycle on a disposable row.
|
|
254
269
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|