ventilatepro-cli 0.1.1__tar.gz → 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.
Files changed (27) hide show
  1. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/PKG-INFO +8 -1
  2. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/README.md +7 -0
  3. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/tests/test_cli.py +116 -0
  4. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/ventilatepro/__init__.py +1 -1
  5. ventilatepro_cli-0.2.0/ventilatepro/api_client.py +221 -0
  6. ventilatepro_cli-0.2.0/ventilatepro/calc.py +142 -0
  7. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/ventilatepro/cli.py +13 -35
  8. ventilatepro_cli-0.2.0/ventilatepro/design_day.py +95 -0
  9. ventilatepro_cli-0.2.0/ventilatepro/entities.py +210 -0
  10. ventilatepro_cli-0.2.0/ventilatepro/output.py +31 -0
  11. ventilatepro_cli-0.2.0/ventilatepro/projects.py +55 -0
  12. ventilatepro_cli-0.2.0/ventilatepro/systems.py +76 -0
  13. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/ventilatepro_cli.egg-info/PKG-INFO +8 -1
  14. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/ventilatepro_cli.egg-info/SOURCES.txt +6 -0
  15. ventilatepro_cli-0.1.1/ventilatepro/api_client.py +0 -122
  16. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/pyproject.toml +0 -0
  17. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/setup.cfg +0 -0
  18. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/tests/test_config.py +0 -0
  19. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/ventilatepro/__main__.py +0 -0
  20. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/ventilatepro/auth.py +0 -0
  21. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/ventilatepro/config.py +0 -0
  22. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/ventilatepro/notes.py +0 -0
  23. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/ventilatepro/queue.py +0 -0
  24. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/ventilatepro_cli.egg-info/dependency_links.txt +0 -0
  25. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/ventilatepro_cli.egg-info/entry_points.txt +0 -0
  26. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/ventilatepro_cli.egg-info/requires.txt +0 -0
  27. {ventilatepro_cli-0.1.1 → ventilatepro_cli-0.2.0}/ventilatepro_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ventilatepro-cli
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: VentilatePro CLI
5
5
  Project-URL: Homepage, https://ventilatepro.com/exam/cli/
6
6
  Project-URL: Documentation, https://ventilatepro.com/exam/cli/
@@ -42,6 +42,13 @@ pip install -e .
42
42
  ```bash
43
43
  ventilatepro auth login --url http://localhost:8000
44
44
  ventilatepro projects list
45
+ ventilatepro projects show 1
46
+ ventilatepro rooms list --project 1
47
+ ventilatepro ahus list --project 1
48
+ ventilatepro systems hierarchy --project 1
49
+ ventilatepro calc status --project 1
50
+ ventilatepro calc ahu-properties --ahu 1
51
+ ventilatepro design-day context --project 1
45
52
  ventilatepro notes create --project 1 --body "Captured from terminal"
46
53
  ventilatepro notes sync
47
54
  ```
@@ -28,6 +28,13 @@ pip install -e .
28
28
  ```bash
29
29
  ventilatepro auth login --url http://localhost:8000
30
30
  ventilatepro projects list
31
+ ventilatepro projects show 1
32
+ ventilatepro rooms list --project 1
33
+ ventilatepro ahus list --project 1
34
+ ventilatepro systems hierarchy --project 1
35
+ ventilatepro calc status --project 1
36
+ ventilatepro calc ahu-properties --ahu 1
37
+ ventilatepro design-day context --project 1
31
38
  ventilatepro notes create --project 1 --body "Captured from terminal"
32
39
  ventilatepro notes sync
33
40
  ```
@@ -286,3 +286,119 @@ class CliTests(unittest.TestCase):
286
286
  tags=["field", "urgent"],
287
287
  query="terminal",
288
288
  )
289
+
290
+ def test_projects_list_json_outputs_paginated_payload(self):
291
+ fake_client = mock.Mock()
292
+ fake_client.projects_list.return_value = {
293
+ "count": 1,
294
+ "limit": 20,
295
+ "offset": 0,
296
+ "results": [
297
+ {
298
+ "id": 1,
299
+ "name": "CLI Project",
300
+ }
301
+ ],
302
+ }
303
+
304
+ with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
305
+ result = self.runner.invoke(app, ["projects", "list", "--json"])
306
+
307
+ self.assertEqual(result.exit_code, 0)
308
+ payload = json.loads(result.stdout)
309
+ self.assertEqual(payload["results"][0]["name"], "CLI Project")
310
+ fake_client.projects_list.assert_called_once_with(limit=20, offset=0, query=None)
311
+
312
+ def test_projects_show_calls_api_client(self):
313
+ fake_client = mock.Mock()
314
+ fake_client.project_show.return_value = {
315
+ "id": 7,
316
+ "name": "Hospital South Wing",
317
+ "counts": {"rooms": 3, "zones": 2, "ahus": 1},
318
+ "location": "Los Angeles",
319
+ "time_zone": "America/Los_Angeles",
320
+ }
321
+
322
+ with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
323
+ result = self.runner.invoke(app, ["projects", "show", "7"])
324
+
325
+ self.assertEqual(result.exit_code, 0)
326
+ self.assertIn("Hospital South Wing", result.stdout)
327
+ fake_client.project_show.assert_called_once_with(7)
328
+
329
+ def test_rooms_list_passes_filters_to_api_client(self):
330
+ fake_client = mock.Mock()
331
+ fake_client.list_rooms.return_value = {
332
+ "project_id": 1,
333
+ "count": 0,
334
+ "limit": 5,
335
+ "offset": 10,
336
+ "results": [],
337
+ }
338
+
339
+ with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
340
+ result = self.runner.invoke(
341
+ app,
342
+ [
343
+ "rooms",
344
+ "list",
345
+ "--project",
346
+ "1",
347
+ "--limit",
348
+ "5",
349
+ "--offset",
350
+ "10",
351
+ "--query",
352
+ "operating",
353
+ "--json",
354
+ ],
355
+ )
356
+
357
+ self.assertEqual(result.exit_code, 0)
358
+ fake_client.list_rooms.assert_called_once_with(1, limit=5, offset=10, query="operating")
359
+
360
+ def test_calc_recalculate_stale_passes_mode_scope_and_force(self):
361
+ fake_client = mock.Mock()
362
+ fake_client.calc_recalculate_stale.return_value = {
363
+ "message": "Queued stale recalculation.",
364
+ }
365
+
366
+ with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
367
+ result = self.runner.invoke(
368
+ app,
369
+ [
370
+ "calc",
371
+ "recalculate-stale",
372
+ "--project",
373
+ "1",
374
+ "--scope",
375
+ "zones",
376
+ "--mode",
377
+ "sync",
378
+ "--force",
379
+ "--json",
380
+ ],
381
+ )
382
+
383
+ self.assertEqual(result.exit_code, 0)
384
+ fake_client.calc_recalculate_stale.assert_called_once_with(1, scope="zones", mode="sync", force=True)
385
+
386
+ def test_design_day_run_reads_json_payload_from_file(self):
387
+ fake_client = mock.Mock()
388
+ fake_client.design_day_run.return_value = {
389
+ "id": 42,
390
+ "state": "QUEUED",
391
+ }
392
+
393
+ with tempfile.NamedTemporaryFile("w+", encoding="utf-8") as handle:
394
+ json.dump({"zones": [{"zone_id": 1}]}, handle)
395
+ handle.flush()
396
+
397
+ with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
398
+ result = self.runner.invoke(
399
+ app,
400
+ ["design-day", "run", "--project", "1", "--input", handle.name, "--json"],
401
+ )
402
+
403
+ self.assertEqual(result.exit_code, 0)
404
+ fake_client.design_day_run.assert_called_once_with(1, {"zones": [{"zone_id": 1}]})
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.1.1"
5
+ __version__ = "0.2.0"
@@ -0,0 +1,221 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ import requests
6
+
7
+
8
+ class ApiError(Exception):
9
+ def __init__(self, status_code: int, message: str, payload: Any = None):
10
+ super().__init__(message)
11
+ self.status_code = status_code
12
+ self.payload = payload
13
+
14
+
15
+ class NetworkError(Exception):
16
+ pass
17
+
18
+
19
+ def extract_error_message(payload: Any, fallback: str) -> str:
20
+ if isinstance(payload, dict):
21
+ if isinstance(payload.get("detail"), str):
22
+ return payload["detail"]
23
+ error = payload.get("error")
24
+ if isinstance(error, dict) and isinstance(error.get("message"), str):
25
+ return error["message"]
26
+ return " ".join(str(value) for value in payload.values() if value)
27
+ if isinstance(payload, str) and payload.strip():
28
+ return payload
29
+ return fallback
30
+
31
+
32
+ class ApiClient:
33
+ def __init__(self, base_url: str, token: Optional[str] = None, timeout: int = 15):
34
+ self.base_url = (base_url or "").rstrip("/")
35
+ self.token = token
36
+ self.timeout = timeout
37
+
38
+ def request(
39
+ self,
40
+ method: str,
41
+ path: str,
42
+ *,
43
+ json_payload: Optional[Dict[str, Any]] = None,
44
+ headers: Optional[Dict[str, str]] = None,
45
+ params: Optional[Dict[str, Any]] = None,
46
+ ):
47
+ final_headers = {
48
+ "Accept": "application/json",
49
+ }
50
+ if headers:
51
+ final_headers.update(headers)
52
+ if self.token:
53
+ final_headers["Authorization"] = f"Bearer {self.token}"
54
+ try:
55
+ response = requests.request(
56
+ method=method,
57
+ url=f"{self.base_url}{path}",
58
+ json=json_payload,
59
+ headers=final_headers,
60
+ params=params,
61
+ timeout=self.timeout,
62
+ )
63
+ except requests.RequestException as exc:
64
+ raise NetworkError(str(exc)) from exc
65
+
66
+ payload: Any = None
67
+ if response.content:
68
+ try:
69
+ payload = response.json()
70
+ except ValueError:
71
+ payload = response.text
72
+
73
+ if not response.ok:
74
+ raise ApiError(
75
+ status_code=response.status_code,
76
+ message=extract_error_message(payload, f"Request failed with status {response.status_code}."),
77
+ payload=payload,
78
+ )
79
+ return payload if payload is not None else {}
80
+
81
+ def auth_me(self):
82
+ return self.request("GET", "/api/cli/auth/me/")
83
+
84
+ def projects_list(self, *, limit: int = 20, offset: int = 0, query: Optional[str] = None):
85
+ params: Dict[str, Any] = {"limit": limit, "offset": offset}
86
+ if query:
87
+ params["q"] = query
88
+ return self.request("GET", "/api/cli/projects/", params=params)
89
+
90
+ def project_show(self, project_id: int):
91
+ return self.request("GET", f"/api/cli/projects/{project_id}/")
92
+
93
+ def _list_project_entities(
94
+ self,
95
+ entity_name: str,
96
+ project: int,
97
+ *,
98
+ limit: int = 20,
99
+ offset: int = 0,
100
+ query: Optional[str] = None,
101
+ ):
102
+ params: Dict[str, Any] = {"project": project, "limit": limit, "offset": offset}
103
+ if query:
104
+ params["q"] = query
105
+ return self.request("GET", f"/api/cli/{entity_name}/", params=params)
106
+
107
+ def _show_entity(self, entity_name: str, entity_id: int):
108
+ return self.request("GET", f"/api/cli/{entity_name}/{entity_id}/")
109
+
110
+ def list_rooms(self, project: int, *, limit: int = 20, offset: int = 0, query: Optional[str] = None):
111
+ return self._list_project_entities("rooms", project, limit=limit, offset=offset, query=query)
112
+
113
+ def show_room(self, room_id: int):
114
+ return self._show_entity("rooms", room_id)
115
+
116
+ def list_zones(self, project: int, *, limit: int = 20, offset: int = 0, query: Optional[str] = None):
117
+ return self._list_project_entities("zones", project, limit=limit, offset=offset, query=query)
118
+
119
+ def show_zone(self, zone_id: int):
120
+ return self._show_entity("zones", zone_id)
121
+
122
+ def list_ahus(self, project: int, *, limit: int = 20, offset: int = 0, query: Optional[str] = None):
123
+ return self._list_project_entities("ahus", project, limit=limit, offset=offset, query=query)
124
+
125
+ def show_ahu(self, ahu_id: int):
126
+ return self._show_entity("ahus", ahu_id)
127
+
128
+ def list_plants(self, project: int, *, limit: int = 20, offset: int = 0, query: Optional[str] = None):
129
+ return self._list_project_entities("plants", project, limit=limit, offset=offset, query=query)
130
+
131
+ def show_plant(self, plant_ref: str):
132
+ return self.request("GET", f"/api/cli/plants/{plant_ref}/")
133
+
134
+ def system_hierarchy(self, project_id: int, *, include_envelope: bool = True):
135
+ return self.request(
136
+ "GET",
137
+ f"/api/cli/projects/{project_id}/systems/hierarchy/",
138
+ params={"include_envelope": str(bool(include_envelope)).lower()},
139
+ )
140
+
141
+ def system_graph(self, project_id: int):
142
+ return self.request("GET", f"/api/cli/projects/{project_id}/systems/graph/")
143
+
144
+ def system_topology(self, project_id: int):
145
+ return self.request("GET", f"/api/cli/projects/{project_id}/systems/topology/")
146
+
147
+ def calc_status(self, project_id: int):
148
+ return self.request("GET", f"/api/cli/projects/{project_id}/calc/status/")
149
+
150
+ def calc_ventilation(self, ahu_id: int):
151
+ return self.request("GET", f"/api/cli/ahus/{ahu_id}/calc/ventilation/")
152
+
153
+ def calc_ahu_steps(self, ahu_id: int):
154
+ return self.request("GET", f"/api/cli/ahus/{ahu_id}/calc/steps/")
155
+
156
+ def calc_ahu_properties(self, ahu_id: int, *, procedure: Optional[str] = None):
157
+ params = {"procedure": procedure} if procedure else None
158
+ return self.request("GET", f"/api/cli/ahus/{ahu_id}/calc/properties/", params=params)
159
+
160
+ def calc_recalculate_airflows(self, project_id: int):
161
+ return self.request("POST", f"/api/cli/projects/{project_id}/calc/recalculate-airflows/")
162
+
163
+ def calc_recalculate_stale(self, project_id: int, *, scope: str, mode: str, force: bool):
164
+ return self.request(
165
+ "POST",
166
+ f"/api/cli/projects/{project_id}/calc/recalculate-stale/",
167
+ json_payload={
168
+ "scope": scope,
169
+ "mode": mode,
170
+ "force": force,
171
+ },
172
+ )
173
+
174
+ def calc_cav(self, ahu_id: int):
175
+ return self.request("GET", f"/api/cli/ahus/{ahu_id}/calc/cav/")
176
+
177
+ def design_day_context(self, project_id: int):
178
+ return self.request("GET", f"/api/cli/projects/{project_id}/design-day/context/")
179
+
180
+ def design_day_run(self, project_id: int, payload: Dict[str, Any]):
181
+ return self.request("POST", f"/api/cli/projects/{project_id}/design-day/run/", json_payload=payload)
182
+
183
+ def design_day_results(self, job_id: int):
184
+ return self.request("GET", f"/api/cli/design-day/jobs/{job_id}/results/")
185
+
186
+ def create_note(self, payload: Dict[str, Any], *, idempotency_key: str):
187
+ return self.request(
188
+ "POST",
189
+ "/api/cli/notes/",
190
+ json_payload=payload,
191
+ headers={"Idempotency-Key": idempotency_key},
192
+ )
193
+
194
+ def list_notes(
195
+ self,
196
+ project: int,
197
+ *,
198
+ limit: int = 20,
199
+ offset: int = 0,
200
+ note_type: Optional[str] = None,
201
+ source: Optional[str] = None,
202
+ tags: Optional[list[str]] = None,
203
+ query: Optional[str] = None,
204
+ ):
205
+ params: Dict[str, Any] = {
206
+ "project": project,
207
+ "limit": limit,
208
+ "offset": offset,
209
+ }
210
+ if note_type:
211
+ params["type"] = note_type
212
+ if source:
213
+ params["source"] = source
214
+ if tags:
215
+ params["tag"] = tags
216
+ if query:
217
+ params["q"] = query
218
+ return self.request("GET", "/api/cli/notes/", params=params)
219
+
220
+ def show_note(self, note_ref: str, *, project: int):
221
+ return self.request("GET", f"/api/cli/notes/{note_ref}/", params={"project": project})
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from . import auth
6
+ from .api_client import ApiError, NetworkError
7
+ from .output import emit, emit_error
8
+
9
+ app = typer.Typer(help="Calculation commands")
10
+
11
+
12
+ @app.command("status")
13
+ def calc_status(
14
+ project: int = typer.Option(..., "--project", help="Project ID"),
15
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
16
+ ):
17
+ try:
18
+ client, _ = auth.get_authenticated_client()
19
+ payload = client.calc_status(project)
20
+ except (ValueError, ApiError, NetworkError) as exc:
21
+ emit_error(str(exc), json_output=json_output)
22
+ raise typer.Exit(1)
23
+
24
+ summary = payload.get("summary") or {}
25
+ zone_summary = summary.get("zones") or {}
26
+ ahu_summary = summary.get("ahus") or {}
27
+ lines = [
28
+ f"Project: {payload.get('project_name')}",
29
+ f"Zones: total={zone_summary.get('total', 0)} stale={zone_summary.get('stale', 0)} failed={zone_summary.get('failed', 0)}",
30
+ f"AHUs: total={ahu_summary.get('total', 0)} stale={ahu_summary.get('stale', 0)} failed={ahu_summary.get('failed', 0)}",
31
+ ]
32
+ emit(payload, json_output=json_output, lines=lines)
33
+
34
+
35
+ @app.command("ventilation")
36
+ def calc_ventilation(
37
+ ahu: int = typer.Option(..., "--ahu", help="AHU ID"),
38
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
39
+ ):
40
+ try:
41
+ client, _ = auth.get_authenticated_client()
42
+ payload = client.calc_ventilation(ahu)
43
+ except (ValueError, ApiError, NetworkError) as exc:
44
+ emit_error(str(exc), json_output=json_output)
45
+ raise typer.Exit(1)
46
+
47
+ lines = [
48
+ f"AHU: {payload.get('ahu_name') or ahu}",
49
+ f"OA CFM: {payload.get('final_outside_air_cfm') or payload.get('outside_air_cfm') or 'N/A'}",
50
+ ]
51
+ emit(payload, json_output=json_output, lines=lines)
52
+
53
+
54
+ @app.command("ahu-steps")
55
+ def calc_ahu_steps(
56
+ ahu: int = typer.Option(..., "--ahu", help="AHU ID"),
57
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
58
+ ):
59
+ try:
60
+ client, _ = auth.get_authenticated_client()
61
+ payload = client.calc_ahu_steps(ahu)
62
+ except (ValueError, ApiError, NetworkError) as exc:
63
+ emit_error(str(exc), json_output=json_output)
64
+ raise typer.Exit(1)
65
+
66
+ lines = [f"Returned AHU calculation steps for AHU {ahu}."]
67
+ emit(payload, json_output=json_output, lines=lines)
68
+
69
+
70
+ @app.command("ahu-properties")
71
+ def calc_ahu_properties(
72
+ ahu: int = typer.Option(..., "--ahu", help="AHU ID"),
73
+ procedure: str = typer.Option(None, "--procedure", help="Optional AHU calculation procedure"),
74
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
75
+ ):
76
+ try:
77
+ client, _ = auth.get_authenticated_client()
78
+ payload = client.calc_ahu_properties(ahu, procedure=procedure)
79
+ except (ValueError, ApiError, NetworkError) as exc:
80
+ emit_error(str(exc), json_output=json_output)
81
+ raise typer.Exit(1)
82
+
83
+ lines = [f"Calculated AHU properties for AHU {ahu}."]
84
+ emit(payload, json_output=json_output, lines=lines)
85
+
86
+
87
+ @app.command("recalculate-airflows")
88
+ def calc_recalculate_airflows(
89
+ project: int = typer.Option(..., "--project", help="Project ID"),
90
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
91
+ ):
92
+ try:
93
+ client, _ = auth.get_authenticated_client()
94
+ payload = client.calc_recalculate_airflows(project)
95
+ except (ValueError, ApiError, NetworkError) as exc:
96
+ emit_error(str(exc), json_output=json_output)
97
+ raise typer.Exit(1)
98
+
99
+ lines = [payload.get("message") or "Recalculated room airflows."]
100
+ emit(payload, json_output=json_output, lines=lines)
101
+
102
+
103
+ @app.command("recalculate-stale")
104
+ def calc_recalculate_stale(
105
+ project: int = typer.Option(..., "--project", help="Project ID"),
106
+ scope: str = typer.Option("all", "--scope", help="all, ahus, or zones"),
107
+ mode: str = typer.Option("async", "--mode", help="async or sync"),
108
+ force: bool = typer.Option(False, "--force", help="Force recalculation"),
109
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
110
+ ):
111
+ try:
112
+ client, _ = auth.get_authenticated_client()
113
+ payload = client.calc_recalculate_stale(project, scope=scope, mode=mode, force=force)
114
+ except (ValueError, ApiError, NetworkError) as exc:
115
+ emit_error(str(exc), json_output=json_output)
116
+ raise typer.Exit(1)
117
+
118
+ message = payload.get("message") or payload.get("detail") or "Requested stale recalculation."
119
+ emit(payload, json_output=json_output, lines=[message])
120
+
121
+
122
+ @app.command("cav")
123
+ def calc_cav(
124
+ ahu: int = typer.Option(..., "--ahu", help="AHU ID"),
125
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
126
+ ):
127
+ try:
128
+ client, _ = auth.get_authenticated_client()
129
+ payload = client.calc_cav(ahu)
130
+ except (ValueError, ApiError, NetworkError) as exc:
131
+ emit_error(str(exc), json_output=json_output)
132
+ raise typer.Exit(1)
133
+
134
+ analysis = payload.get("analysis") or {}
135
+ critical_room = analysis.get("critical_room") or {}
136
+ lines = [
137
+ f"AHU: {payload.get('ahu_name') or ahu}",
138
+ f"Status: {payload.get('status') or 'N/A'}",
139
+ f"Critical Room: {critical_room.get('room_number') or '-'} {critical_room.get('room_name') or ''}".strip(),
140
+ f"Required AHU OSA Fraction: {analysis.get('required_ahu_osa_fraction')}",
141
+ ]
142
+ emit(payload, json_output=json_output, lines=lines)
@@ -1,20 +1,30 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
-
5
3
  import typer
6
4
 
7
5
  from . import auth, notes
8
6
  from .api_client import ApiError, NetworkError
7
+ from .calc import app as calc_app
8
+ from .design_day import app as design_day_app
9
+ from .entities import ahus_app, plants_app, rooms_app, zones_app
10
+ from .output import emit, emit_error
11
+ from .projects import app as projects_app
9
12
  from .queue import QueueStore
13
+ from .systems import app as systems_app
10
14
 
11
15
  app = typer.Typer(help="VentilatePro CLI")
12
16
  auth_app = typer.Typer(help="Authentication commands")
13
- projects_app = typer.Typer(help="Project commands")
14
17
  notes_app = typer.Typer(help="Notes commands")
15
18
 
16
19
  app.add_typer(auth_app, name="auth")
17
20
  app.add_typer(projects_app, name="projects")
21
+ app.add_typer(rooms_app, name="rooms")
22
+ app.add_typer(zones_app, name="zones")
23
+ app.add_typer(ahus_app, name="ahus")
24
+ app.add_typer(plants_app, name="plants")
25
+ app.add_typer(systems_app, name="systems")
26
+ app.add_typer(calc_app, name="calc")
27
+ app.add_typer(design_day_app, name="design-day")
18
28
  app.add_typer(notes_app, name="notes")
19
29
 
20
30
 
@@ -22,21 +32,6 @@ def main():
22
32
  app()
23
33
 
24
34
 
25
- def emit(payload, *, json_output: bool, lines: list[str]):
26
- if json_output:
27
- typer.echo(json.dumps(payload, indent=2, default=str))
28
- return
29
- for line in lines:
30
- typer.echo(line)
31
-
32
-
33
- def emit_error(message: str, *, json_output: bool):
34
- if json_output:
35
- typer.echo(json.dumps({"error": message}))
36
- else:
37
- typer.echo(f"Error: {message}", err=True)
38
-
39
-
40
35
  @auth_app.command("login")
41
36
  def auth_login(
42
37
  url: str = typer.Option(None, "--url", help="VentilatePro base URL"),
@@ -108,23 +103,6 @@ def auth_logout(
108
103
  emit(result, json_output=json_output, lines=["Removed local credentials."])
109
104
 
110
105
 
111
- @projects_app.command("list")
112
- def projects_list(
113
- json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
114
- ):
115
- try:
116
- client, _ = auth.get_authenticated_client()
117
- result = client.projects_list()
118
- except (ValueError, ApiError, NetworkError) as exc:
119
- emit_error(str(exc), json_output=json_output)
120
- raise typer.Exit(1)
121
-
122
- lines = [f"{project['id']}\t{project['name']}" for project in result]
123
- if not lines:
124
- lines = ["No accessible projects found."]
125
- emit(result, json_output=json_output, lines=lines)
126
-
127
-
128
106
  @notes_app.command("create")
129
107
  def notes_create(
130
108
  project: int = typer.Option(..., "--project", help="Target project ID"),
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ from . import auth
10
+ from .api_client import ApiError, NetworkError
11
+ from .output import emit, emit_error
12
+
13
+ app = typer.Typer(help="Design-day commands")
14
+
15
+
16
+ def _load_json_payload(input_path: str | None, use_stdin: bool):
17
+ if input_path and use_stdin:
18
+ raise ValueError("Use either --input or --stdin, not both.")
19
+ if use_stdin:
20
+ raw = sys.stdin.read()
21
+ if not raw.strip():
22
+ return {}
23
+ return json.loads(raw)
24
+ if input_path:
25
+ raw = Path(input_path).read_text(encoding="utf-8")
26
+ if not raw.strip():
27
+ return {}
28
+ return json.loads(raw)
29
+ return {}
30
+
31
+
32
+ @app.command("context")
33
+ def design_day_context(
34
+ project: int = typer.Option(..., "--project", help="Project ID"),
35
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
36
+ ):
37
+ try:
38
+ client, _ = auth.get_authenticated_client()
39
+ payload = client.design_day_context(project)
40
+ except (ValueError, ApiError, NetworkError) as exc:
41
+ emit_error(str(exc), json_output=json_output)
42
+ raise typer.Exit(1)
43
+
44
+ readiness = payload.get("readiness") or {}
45
+ lines = [
46
+ f"Project: {payload.get('project', {}).get('name')}",
47
+ f"Zones: {len(payload.get('zones') or [])}",
48
+ f"Ready: {not bool(readiness.get('missing_required'))}",
49
+ ]
50
+ emit(payload, json_output=json_output, lines=lines)
51
+
52
+
53
+ @app.command("run")
54
+ def design_day_run(
55
+ project: int = typer.Option(..., "--project", help="Project ID"),
56
+ input_path: str = typer.Option(None, "--input", help="Path to a JSON request payload"),
57
+ use_stdin: bool = typer.Option(False, "--stdin", help="Read a JSON request payload from stdin"),
58
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
59
+ ):
60
+ try:
61
+ payload_in = _load_json_payload(input_path, use_stdin)
62
+ client, _ = auth.get_authenticated_client()
63
+ payload = client.design_day_run(project, payload_in)
64
+ except json.JSONDecodeError as exc:
65
+ emit_error(f"Invalid JSON input: {exc}", json_output=json_output)
66
+ raise typer.Exit(1)
67
+ except (ValueError, ApiError, NetworkError) as exc:
68
+ emit_error(str(exc), json_output=json_output)
69
+ raise typer.Exit(1)
70
+
71
+ lines = [
72
+ f"Submitted design-day job {payload.get('id')}.",
73
+ f"State: {payload.get('state') or 'queued'}",
74
+ ]
75
+ emit(payload, json_output=json_output, lines=lines)
76
+
77
+
78
+ @app.command("results")
79
+ def design_day_results(
80
+ job: int = typer.Option(..., "--job", help="Design-day job ID"),
81
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
82
+ ):
83
+ try:
84
+ client, _ = auth.get_authenticated_client()
85
+ payload = client.design_day_results(job)
86
+ except (ValueError, ApiError, NetworkError) as exc:
87
+ emit_error(str(exc), json_output=json_output)
88
+ raise typer.Exit(1)
89
+
90
+ lines = [
91
+ f"Status: {payload.get('status') or 'unknown'}",
92
+ f"Method: {payload.get('method') or 'N/A'}",
93
+ f"Generated At: {payload.get('generated_at') or 'N/A'}",
94
+ ]
95
+ emit(payload, json_output=json_output, lines=lines)
@@ -0,0 +1,210 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from . import auth
6
+ from .api_client import ApiError, NetworkError
7
+ from .output import emit, emit_error, extract_results
8
+
9
+ rooms_app = typer.Typer(help="Room commands")
10
+ zones_app = typer.Typer(help="Zone commands")
11
+ ahus_app = typer.Typer(help="AHU commands")
12
+ plants_app = typer.Typer(help="Plant commands")
13
+
14
+
15
+ def _handle_list(payload, *, json_output: bool, line_builder, empty_message: str):
16
+ results = extract_results(payload)
17
+ lines = [line_builder(item) for item in results]
18
+ if not lines:
19
+ lines = [empty_message]
20
+ emit(payload, json_output=json_output, lines=lines)
21
+
22
+
23
+ @rooms_app.command("list")
24
+ def rooms_list(
25
+ project: int = typer.Option(..., "--project", help="Project ID"),
26
+ limit: int = typer.Option(20, "--limit", help="Maximum rooms to return"),
27
+ offset: int = typer.Option(0, "--offset", help="Starting offset"),
28
+ query: str = typer.Option(None, "--query", help="Filter by room name or number"),
29
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
30
+ ):
31
+ try:
32
+ client, _ = auth.get_authenticated_client()
33
+ payload = client.list_rooms(project, limit=limit, offset=offset, query=query)
34
+ except (ValueError, ApiError, NetworkError) as exc:
35
+ emit_error(str(exc), json_output=json_output)
36
+ raise typer.Exit(1)
37
+
38
+ _handle_list(
39
+ payload,
40
+ json_output=json_output,
41
+ line_builder=lambda item: f"{item['id']}\t{item.get('number') or '-'}\t{item['name']}",
42
+ empty_message="No rooms found for the current filters.",
43
+ )
44
+
45
+
46
+ @rooms_app.command("show")
47
+ def rooms_show(
48
+ room_id: int = typer.Argument(..., help="Room ID"),
49
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
50
+ ):
51
+ try:
52
+ client, _ = auth.get_authenticated_client()
53
+ payload = client.show_room(room_id)
54
+ except (ValueError, ApiError, NetworkError) as exc:
55
+ emit_error(str(exc), json_output=json_output)
56
+ raise typer.Exit(1)
57
+
58
+ airflows = payload.get("airflows") or {}
59
+ lines = [
60
+ f"Room: {payload.get('number') or '-'} {payload.get('name')}",
61
+ f"Project: {payload.get('project_id')}",
62
+ f"Area: {payload.get('area')}",
63
+ f"Supply: {airflows.get('supply_airflow')}",
64
+ f"Return: {airflows.get('return_airflow')}",
65
+ f"Exhaust: {airflows.get('exhaust_airflow')}",
66
+ ]
67
+ emit(payload, json_output=json_output, lines=lines)
68
+
69
+
70
+ @zones_app.command("list")
71
+ def zones_list(
72
+ project: int = typer.Option(..., "--project", help="Project ID"),
73
+ limit: int = typer.Option(20, "--limit", help="Maximum zones to return"),
74
+ offset: int = typer.Option(0, "--offset", help="Starting offset"),
75
+ query: str = typer.Option(None, "--query", help="Filter by zone name"),
76
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
77
+ ):
78
+ try:
79
+ client, _ = auth.get_authenticated_client()
80
+ payload = client.list_zones(project, limit=limit, offset=offset, query=query)
81
+ except (ValueError, ApiError, NetworkError) as exc:
82
+ emit_error(str(exc), json_output=json_output)
83
+ raise typer.Exit(1)
84
+
85
+ _handle_list(
86
+ payload,
87
+ json_output=json_output,
88
+ line_builder=lambda item: f"{item['id']}\t{item['name']}\trooms={len(item.get('rooms') or [])}\tahus={len(item.get('ahus') or [])}",
89
+ empty_message="No zones found for the current filters.",
90
+ )
91
+
92
+
93
+ @zones_app.command("show")
94
+ def zones_show(
95
+ zone_id: int = typer.Argument(..., help="Zone ID"),
96
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
97
+ ):
98
+ try:
99
+ client, _ = auth.get_authenticated_client()
100
+ payload = client.show_zone(zone_id)
101
+ except (ValueError, ApiError, NetworkError) as exc:
102
+ emit_error(str(exc), json_output=json_output)
103
+ raise typer.Exit(1)
104
+
105
+ airflows = payload.get("airflows") or {}
106
+ calc = payload.get("calc") or {}
107
+ lines = [
108
+ f"Zone: {payload.get('name')}",
109
+ f"Project: {payload.get('project_id')}",
110
+ f"Max CFM: {airflows.get('maximum_airflow_cfm')}",
111
+ f"Min CFM: {airflows.get('minimum_airflow_cfm')}",
112
+ f"Heating CFM: {airflows.get('heating_airflow_cfm')}",
113
+ f"Calc Status: {calc.get('calc_status') or 'N/A'}",
114
+ ]
115
+ emit(payload, json_output=json_output, lines=lines)
116
+
117
+
118
+ @ahus_app.command("list")
119
+ def ahus_list(
120
+ project: int = typer.Option(..., "--project", help="Project ID"),
121
+ limit: int = typer.Option(20, "--limit", help="Maximum AHUs to return"),
122
+ offset: int = typer.Option(0, "--offset", help="Starting offset"),
123
+ query: str = typer.Option(None, "--query", help="Filter by AHU name"),
124
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
125
+ ):
126
+ try:
127
+ client, _ = auth.get_authenticated_client()
128
+ payload = client.list_ahus(project, limit=limit, offset=offset, query=query)
129
+ except (ValueError, ApiError, NetworkError) as exc:
130
+ emit_error(str(exc), json_output=json_output)
131
+ raise typer.Exit(1)
132
+
133
+ _handle_list(
134
+ payload,
135
+ json_output=json_output,
136
+ line_builder=lambda item: f"{item['id']}\t{item['name']}\t{item.get('system_type') or '-'}",
137
+ empty_message="No AHUs found for the current filters.",
138
+ )
139
+
140
+
141
+ @ahus_app.command("show")
142
+ def ahus_show(
143
+ ahu_id: int = typer.Argument(..., help="AHU ID"),
144
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
145
+ ):
146
+ try:
147
+ client, _ = auth.get_authenticated_client()
148
+ payload = client.show_ahu(ahu_id)
149
+ except (ValueError, ApiError, NetworkError) as exc:
150
+ emit_error(str(exc), json_output=json_output)
151
+ raise typer.Exit(1)
152
+
153
+ airflows = payload.get("airflows") or {}
154
+ calc = payload.get("calc") or {}
155
+ lines = [
156
+ f"AHU: {payload.get('name')}",
157
+ f"System Type: {payload.get('system_type') or '-'}",
158
+ f"Supply CFM: {airflows.get('total_supply_air_cfm')}",
159
+ f"Return CFM: {airflows.get('total_return_air_cfm')}",
160
+ f"OA CFM: {airflows.get('final_outside_air_cfm')}",
161
+ f"Calc Status: {calc.get('calc_status') or 'N/A'}",
162
+ ]
163
+ emit(payload, json_output=json_output, lines=lines)
164
+
165
+
166
+ @plants_app.command("list")
167
+ def plants_list(
168
+ project: int = typer.Option(..., "--project", help="Project ID"),
169
+ limit: int = typer.Option(20, "--limit", help="Maximum plants to return"),
170
+ offset: int = typer.Option(0, "--offset", help="Starting offset"),
171
+ query: str = typer.Option(None, "--query", help="Filter by plant name or type"),
172
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
173
+ ):
174
+ try:
175
+ client, _ = auth.get_authenticated_client()
176
+ payload = client.list_plants(project, limit=limit, offset=offset, query=query)
177
+ except (ValueError, ApiError, NetworkError) as exc:
178
+ emit_error(str(exc), json_output=json_output)
179
+ raise typer.Exit(1)
180
+
181
+ _handle_list(
182
+ payload,
183
+ json_output=json_output,
184
+ line_builder=lambda item: f"{item['plant_ref']}\t{item['name']}\t{item['type_label']}",
185
+ empty_message="No plants found for the current filters.",
186
+ )
187
+
188
+
189
+ @plants_app.command("show")
190
+ def plants_show(
191
+ plant_ref: str = typer.Argument(..., help="Typed plant reference, such as chilled-water:12"),
192
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
193
+ ):
194
+ try:
195
+ client, _ = auth.get_authenticated_client()
196
+ payload = client.show_plant(plant_ref)
197
+ except (ValueError, ApiError, NetworkError) as exc:
198
+ emit_error(str(exc), json_output=json_output)
199
+ raise typer.Exit(1)
200
+
201
+ metrics = payload.get("metrics") or {}
202
+ lines = [
203
+ f"Plant: {payload.get('name')}",
204
+ f"Ref: {payload.get('plant_ref')}",
205
+ f"Type: {payload.get('type_label')}",
206
+ f"Project: {payload.get('project_id')}",
207
+ f"Design Supply Temp: {metrics.get('design_supply_temp_f')}",
208
+ f"Design Return Temp: {metrics.get('design_return_temp_f')}",
209
+ ]
210
+ emit(payload, json_output=json_output, lines=lines)
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ import typer
7
+
8
+
9
+ def emit(payload: Any, *, json_output: bool, lines: list[str]):
10
+ if json_output:
11
+ typer.echo(json.dumps(payload, indent=2, default=str))
12
+ return
13
+ for line in lines:
14
+ typer.echo(line)
15
+
16
+
17
+ def emit_error(message: str, *, json_output: bool):
18
+ if json_output:
19
+ typer.echo(json.dumps({"error": message}))
20
+ else:
21
+ typer.echo(f"Error: {message}", err=True)
22
+
23
+
24
+ def extract_results(payload: Any):
25
+ if isinstance(payload, list):
26
+ return payload
27
+ if isinstance(payload, dict):
28
+ results = payload.get("results")
29
+ if isinstance(results, list):
30
+ return results
31
+ return []
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from . import auth
6
+ from .api_client import ApiError, NetworkError
7
+ from .output import emit, emit_error, extract_results
8
+
9
+ app = typer.Typer(help="Project commands")
10
+
11
+
12
+ @app.command("list")
13
+ def projects_list(
14
+ limit: int = typer.Option(20, "--limit", help="Maximum projects to return"),
15
+ offset: int = typer.Option(0, "--offset", help="Starting offset"),
16
+ query: str = typer.Option(None, "--query", help="Filter by project name or metadata"),
17
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
18
+ ):
19
+ try:
20
+ client, _ = auth.get_authenticated_client()
21
+ payload = client.projects_list(limit=limit, offset=offset, query=query)
22
+ except (ValueError, ApiError, NetworkError) as exc:
23
+ emit_error(str(exc), json_output=json_output)
24
+ raise typer.Exit(1)
25
+
26
+ results = extract_results(payload)
27
+ lines = [f"{project['id']}\t{project['name']}" for project in results]
28
+ if not lines:
29
+ lines = ["No accessible projects found."]
30
+ emit(payload, json_output=json_output, lines=lines)
31
+
32
+
33
+ @app.command("show")
34
+ def projects_show(
35
+ project_id: int = typer.Argument(..., help="Project ID"),
36
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
37
+ ):
38
+ try:
39
+ client, _ = auth.get_authenticated_client()
40
+ payload = client.project_show(project_id)
41
+ except (ValueError, ApiError, NetworkError) as exc:
42
+ emit_error(str(exc), json_output=json_output)
43
+ raise typer.Exit(1)
44
+
45
+ counts = payload.get("counts") or {}
46
+ lines = [
47
+ f"Project: {payload.get('name')}",
48
+ f"ID: {payload.get('id')}",
49
+ f"Location: {payload.get('location') or 'N/A'}",
50
+ f"Time Zone: {payload.get('time_zone') or 'N/A'}",
51
+ f"Rooms: {counts.get('rooms', 0)}",
52
+ f"Zones: {counts.get('zones', 0)}",
53
+ f"AHUs: {counts.get('ahus', 0)}",
54
+ ]
55
+ emit(payload, json_output=json_output, lines=lines)
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from . import auth
6
+ from .api_client import ApiError, NetworkError
7
+ from .output import emit, emit_error
8
+
9
+ app = typer.Typer(help="System graph and hierarchy commands")
10
+
11
+
12
+ @app.command("hierarchy")
13
+ def system_hierarchy(
14
+ project: int = typer.Option(..., "--project", help="Project ID"),
15
+ include_envelope: bool = typer.Option(True, "--include-envelope/--no-include-envelope", help="Include envelope data"),
16
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
17
+ ):
18
+ try:
19
+ client, _ = auth.get_authenticated_client()
20
+ payload = client.system_hierarchy(project, include_envelope=include_envelope)
21
+ except (ValueError, ApiError, NetworkError) as exc:
22
+ emit_error(str(exc), json_output=json_output)
23
+ raise typer.Exit(1)
24
+
25
+ hierarchy = payload.get("hierarchy") or {}
26
+ lines = [
27
+ f"Project: {payload.get('project', {}).get('name')}",
28
+ f"Chilled Water Plants: {len(hierarchy.get('chilled_water_plants') or [])}",
29
+ f"Heating Plants: {len(hierarchy.get('heating_plants') or [])}",
30
+ f"AHUs Without Plants: {len(hierarchy.get('ahus_without_plants') or [])}",
31
+ f"Zones Without AHUs: {len(hierarchy.get('zones_without_ahus') or [])}",
32
+ f"Rooms Without Zones: {len(hierarchy.get('rooms_without_zones') or [])}",
33
+ ]
34
+ emit(payload, json_output=json_output, lines=lines)
35
+
36
+
37
+ @app.command("graph")
38
+ def system_graph(
39
+ project: int = typer.Option(..., "--project", help="Project ID"),
40
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
41
+ ):
42
+ try:
43
+ client, _ = auth.get_authenticated_client()
44
+ payload = client.system_graph(project)
45
+ except (ValueError, ApiError, NetworkError) as exc:
46
+ emit_error(str(exc), json_output=json_output)
47
+ raise typer.Exit(1)
48
+
49
+ counts = payload.get("graph", {}).get("counts") or {}
50
+ lines = [
51
+ f"Project: {payload.get('project', {}).get('name')}",
52
+ f"Nodes: {counts.get('nodes', 0)}",
53
+ f"Edges: {counts.get('edges', 0)}",
54
+ ]
55
+ emit(payload, json_output=json_output, lines=lines)
56
+
57
+
58
+ @app.command("topology")
59
+ def system_topology(
60
+ project: int = typer.Option(..., "--project", help="Project ID"),
61
+ json_output: bool = typer.Option(False, "--json", help="Return JSON output"),
62
+ ):
63
+ try:
64
+ client, _ = auth.get_authenticated_client()
65
+ payload = client.system_topology(project)
66
+ except (ValueError, ApiError, NetworkError) as exc:
67
+ emit_error(str(exc), json_output=json_output)
68
+ raise typer.Exit(1)
69
+
70
+ topology = payload.get("topology") or {}
71
+ lines = [
72
+ f"Project: {payload.get('project', {}).get('name')}",
73
+ f"Nodes: {len(topology.get('nodes') or [])}",
74
+ f"Links: {len(topology.get('links') or [])}",
75
+ ]
76
+ emit(payload, json_output=json_output, lines=lines)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ventilatepro-cli
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: VentilatePro CLI
5
5
  Project-URL: Homepage, https://ventilatepro.com/exam/cli/
6
6
  Project-URL: Documentation, https://ventilatepro.com/exam/cli/
@@ -42,6 +42,13 @@ pip install -e .
42
42
  ```bash
43
43
  ventilatepro auth login --url http://localhost:8000
44
44
  ventilatepro projects list
45
+ ventilatepro projects show 1
46
+ ventilatepro rooms list --project 1
47
+ ventilatepro ahus list --project 1
48
+ ventilatepro systems hierarchy --project 1
49
+ ventilatepro calc status --project 1
50
+ ventilatepro calc ahu-properties --ahu 1
51
+ ventilatepro design-day context --project 1
45
52
  ventilatepro notes create --project 1 --body "Captured from terminal"
46
53
  ventilatepro notes sync
47
54
  ```
@@ -6,10 +6,16 @@ ventilatepro/__init__.py
6
6
  ventilatepro/__main__.py
7
7
  ventilatepro/api_client.py
8
8
  ventilatepro/auth.py
9
+ ventilatepro/calc.py
9
10
  ventilatepro/cli.py
10
11
  ventilatepro/config.py
12
+ ventilatepro/design_day.py
13
+ ventilatepro/entities.py
11
14
  ventilatepro/notes.py
15
+ ventilatepro/output.py
16
+ ventilatepro/projects.py
12
17
  ventilatepro/queue.py
18
+ ventilatepro/systems.py
13
19
  ventilatepro_cli.egg-info/PKG-INFO
14
20
  ventilatepro_cli.egg-info/SOURCES.txt
15
21
  ventilatepro_cli.egg-info/dependency_links.txt
@@ -1,122 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any, Dict, Optional
4
-
5
- import requests
6
-
7
-
8
- class ApiError(Exception):
9
- def __init__(self, status_code: int, message: str, payload: Any = None):
10
- super().__init__(message)
11
- self.status_code = status_code
12
- self.payload = payload
13
-
14
-
15
- class NetworkError(Exception):
16
- pass
17
-
18
-
19
- def extract_error_message(payload: Any, fallback: str) -> str:
20
- if isinstance(payload, dict):
21
- if isinstance(payload.get("detail"), str):
22
- return payload["detail"]
23
- error = payload.get("error")
24
- if isinstance(error, dict) and isinstance(error.get("message"), str):
25
- return error["message"]
26
- return " ".join(str(value) for value in payload.values() if value)
27
- if isinstance(payload, str) and payload.strip():
28
- return payload
29
- return fallback
30
-
31
-
32
- class ApiClient:
33
- def __init__(self, base_url: str, token: Optional[str] = None, timeout: int = 15):
34
- self.base_url = (base_url or "").rstrip("/")
35
- self.token = token
36
- self.timeout = timeout
37
-
38
- def request(
39
- self,
40
- method: str,
41
- path: str,
42
- *,
43
- json_payload: Optional[Dict[str, Any]] = None,
44
- headers: Optional[Dict[str, str]] = None,
45
- params: Optional[Dict[str, Any]] = None,
46
- ):
47
- final_headers = {
48
- "Accept": "application/json",
49
- }
50
- if headers:
51
- final_headers.update(headers)
52
- if self.token:
53
- final_headers["Authorization"] = f"Bearer {self.token}"
54
- try:
55
- response = requests.request(
56
- method=method,
57
- url=f"{self.base_url}{path}",
58
- json=json_payload,
59
- headers=final_headers,
60
- params=params,
61
- timeout=self.timeout,
62
- )
63
- except requests.RequestException as exc:
64
- raise NetworkError(str(exc)) from exc
65
-
66
- payload: Any = None
67
- if response.content:
68
- try:
69
- payload = response.json()
70
- except ValueError:
71
- payload = response.text
72
-
73
- if not response.ok:
74
- raise ApiError(
75
- status_code=response.status_code,
76
- message=extract_error_message(payload, f"Request failed with status {response.status_code}."),
77
- payload=payload,
78
- )
79
- return payload if payload is not None else {}
80
-
81
- def auth_me(self):
82
- return self.request("GET", "/api/cli/auth/me/")
83
-
84
- def projects_list(self):
85
- return self.request("GET", "/api/cli/projects/")
86
-
87
- def create_note(self, payload: Dict[str, Any], *, idempotency_key: str):
88
- return self.request(
89
- "POST",
90
- "/api/cli/notes/",
91
- json_payload=payload,
92
- headers={"Idempotency-Key": idempotency_key},
93
- )
94
-
95
- def list_notes(
96
- self,
97
- project: int,
98
- *,
99
- limit: int = 20,
100
- offset: int = 0,
101
- note_type: Optional[str] = None,
102
- source: Optional[str] = None,
103
- tags: Optional[list[str]] = None,
104
- query: Optional[str] = None,
105
- ):
106
- params: Dict[str, Any] = {
107
- "project": project,
108
- "limit": limit,
109
- "offset": offset,
110
- }
111
- if note_type:
112
- params["type"] = note_type
113
- if source:
114
- params["source"] = source
115
- if tags:
116
- params["tag"] = tags
117
- if query:
118
- params["q"] = query
119
- return self.request("GET", "/api/cli/notes/", params=params)
120
-
121
- def show_note(self, note_ref: str, *, project: int):
122
- return self.request("GET", f"/api/cli/notes/{note_ref}/", params={"project": project})