ventilatepro-cli 0.1.0__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.
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/PKG-INFO +20 -1
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/README.md +19 -0
- ventilatepro_cli-0.2.0/tests/test_cli.py +404 -0
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/ventilatepro/__init__.py +1 -1
- ventilatepro_cli-0.2.0/ventilatepro/api_client.py +221 -0
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/ventilatepro/auth.py +31 -2
- ventilatepro_cli-0.2.0/ventilatepro/calc.py +142 -0
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/ventilatepro/cli.py +39 -38
- ventilatepro_cli-0.2.0/ventilatepro/design_day.py +95 -0
- ventilatepro_cli-0.2.0/ventilatepro/entities.py +210 -0
- ventilatepro_cli-0.2.0/ventilatepro/output.py +31 -0
- ventilatepro_cli-0.2.0/ventilatepro/projects.py +55 -0
- ventilatepro_cli-0.2.0/ventilatepro/systems.py +76 -0
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/ventilatepro_cli.egg-info/PKG-INFO +20 -1
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/ventilatepro_cli.egg-info/SOURCES.txt +6 -0
- ventilatepro_cli-0.1.0/tests/test_cli.py +0 -171
- ventilatepro_cli-0.1.0/ventilatepro/api_client.py +0 -90
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/pyproject.toml +0 -0
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/setup.cfg +0 -0
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/tests/test_config.py +0 -0
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/ventilatepro/__main__.py +0 -0
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/ventilatepro/config.py +0 -0
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/ventilatepro/notes.py +0 -0
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/ventilatepro/queue.py +0 -0
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/ventilatepro_cli.egg-info/dependency_links.txt +0 -0
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/ventilatepro_cli.egg-info/entry_points.txt +0 -0
- {ventilatepro_cli-0.1.0 → ventilatepro_cli-0.2.0}/ventilatepro_cli.egg-info/requires.txt +0 -0
- {ventilatepro_cli-0.1.0 → 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.
|
|
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,8 +42,27 @@ 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
|
```
|
|
48
55
|
|
|
56
|
+
If the hidden token prompt is awkward in your terminal, use either:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ventilatepro auth login --url https://ventilatepro.com --token-prompt-visible
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
or pipe the token through stdin:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
printf '%s' "$VPCLI_TOKEN" | ventilatepro auth login --url https://ventilatepro.com --token-stdin
|
|
66
|
+
```
|
|
67
|
+
|
|
49
68
|
See the hosted install guide at `https://ventilatepro.com/exam/cli/`, plus [docs/commands.md](./docs/commands.md), [docs/auth.md](./docs/auth.md), [docs/api.md](./docs/api.md), and [docs/releasing.md](./docs/releasing.md) in this repo.
|
|
@@ -28,8 +28,27 @@ 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
|
```
|
|
34
41
|
|
|
42
|
+
If the hidden token prompt is awkward in your terminal, use either:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
ventilatepro auth login --url https://ventilatepro.com --token-prompt-visible
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
or pipe the token through stdin:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
printf '%s' "$VPCLI_TOKEN" | ventilatepro auth login --url https://ventilatepro.com --token-stdin
|
|
52
|
+
```
|
|
53
|
+
|
|
35
54
|
See the hosted install guide at `https://ventilatepro.com/exam/cli/`, plus [docs/commands.md](./docs/commands.md), [docs/auth.md](./docs/auth.md), [docs/api.md](./docs/api.md), and [docs/releasing.md](./docs/releasing.md) in this repo.
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest import mock
|
|
6
|
+
|
|
7
|
+
from typer.testing import CliRunner
|
|
8
|
+
|
|
9
|
+
from ventilatepro import auth, config
|
|
10
|
+
from ventilatepro.api_client import ApiError, NetworkError
|
|
11
|
+
from ventilatepro.cli import app
|
|
12
|
+
from ventilatepro.queue import QueueStore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CliTests(unittest.TestCase):
|
|
16
|
+
def setUp(self):
|
|
17
|
+
self.runner = CliRunner()
|
|
18
|
+
|
|
19
|
+
def test_auth_login_reads_token_from_stdin(self):
|
|
20
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
21
|
+
os.environ[config.CONFIG_ENV_VAR] = tmpdir
|
|
22
|
+
os.environ[config.DATA_ENV_VAR] = tmpdir
|
|
23
|
+
try:
|
|
24
|
+
fake_client = mock.Mock()
|
|
25
|
+
fake_client.auth_me.return_value = {
|
|
26
|
+
"username": "demo-user",
|
|
27
|
+
"email": "demo@example.com",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
with mock.patch.object(auth, "ApiClient", return_value=fake_client):
|
|
31
|
+
with mock.patch.object(config, "_try_get_keyring", return_value=None):
|
|
32
|
+
result = self.runner.invoke(
|
|
33
|
+
app,
|
|
34
|
+
["auth", "login", "--url", "http://localhost:8000", "--token-stdin", "--json"],
|
|
35
|
+
input="vpcli_demo_secret\n",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
self.assertEqual(result.exit_code, 0)
|
|
39
|
+
payload = json.loads(result.stdout)
|
|
40
|
+
self.assertEqual(payload["status"], "authenticated")
|
|
41
|
+
self.assertEqual(config.get_token(), "vpcli_demo_secret")
|
|
42
|
+
self.assertEqual(config.get_base_url(), "http://localhost:8000")
|
|
43
|
+
finally:
|
|
44
|
+
os.environ.pop(config.CONFIG_ENV_VAR, None)
|
|
45
|
+
os.environ.pop(config.DATA_ENV_VAR, None)
|
|
46
|
+
|
|
47
|
+
def test_auth_login_accepts_visible_prompt_input(self):
|
|
48
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
49
|
+
os.environ[config.CONFIG_ENV_VAR] = tmpdir
|
|
50
|
+
os.environ[config.DATA_ENV_VAR] = tmpdir
|
|
51
|
+
try:
|
|
52
|
+
fake_client = mock.Mock()
|
|
53
|
+
fake_client.auth_me.return_value = {
|
|
54
|
+
"username": "demo-user",
|
|
55
|
+
"email": "demo@example.com",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
with mock.patch.object(auth, "ApiClient", return_value=fake_client):
|
|
59
|
+
with mock.patch.object(config, "_try_get_keyring", return_value=None):
|
|
60
|
+
result = self.runner.invoke(
|
|
61
|
+
app,
|
|
62
|
+
["auth", "login", "--url", "http://localhost:8000", "--token-prompt-visible", "--json"],
|
|
63
|
+
input="vpcli_demo_secret\n",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self.assertEqual(result.exit_code, 0)
|
|
67
|
+
self.assertIn("Personal access token (visible):", result.stdout)
|
|
68
|
+
self.assertEqual(config.get_token(), "vpcli_demo_secret")
|
|
69
|
+
self.assertEqual(config.get_base_url(), "http://localhost:8000")
|
|
70
|
+
finally:
|
|
71
|
+
os.environ.pop(config.CONFIG_ENV_VAR, None)
|
|
72
|
+
os.environ.pop(config.DATA_ENV_VAR, None)
|
|
73
|
+
|
|
74
|
+
def test_auth_login_rejects_conflicting_token_inputs(self):
|
|
75
|
+
result = self.runner.invoke(
|
|
76
|
+
app,
|
|
77
|
+
[
|
|
78
|
+
"auth",
|
|
79
|
+
"login",
|
|
80
|
+
"--url",
|
|
81
|
+
"http://localhost:8000",
|
|
82
|
+
"--token",
|
|
83
|
+
"vpcli_demo_secret",
|
|
84
|
+
"--token-stdin",
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.assertNotEqual(result.exit_code, 0)
|
|
89
|
+
self.assertIn("either --token or --token-stdin", result.output)
|
|
90
|
+
|
|
91
|
+
def test_status_json_reports_not_authenticated(self):
|
|
92
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
93
|
+
os.environ[config.CONFIG_ENV_VAR] = tmpdir
|
|
94
|
+
os.environ[config.DATA_ENV_VAR] = tmpdir
|
|
95
|
+
try:
|
|
96
|
+
result = self.runner.invoke(app, ["auth", "status", "--json"])
|
|
97
|
+
self.assertEqual(result.exit_code, 0)
|
|
98
|
+
payload = json.loads(result.stdout)
|
|
99
|
+
self.assertFalse(payload["authenticated"])
|
|
100
|
+
finally:
|
|
101
|
+
os.environ.pop(config.CONFIG_ENV_VAR, None)
|
|
102
|
+
os.environ.pop(config.DATA_ENV_VAR, None)
|
|
103
|
+
|
|
104
|
+
def test_notes_create_queues_on_network_error(self):
|
|
105
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
106
|
+
os.environ[config.CONFIG_ENV_VAR] = tmpdir
|
|
107
|
+
os.environ[config.DATA_ENV_VAR] = tmpdir
|
|
108
|
+
try:
|
|
109
|
+
config.set_base_url("http://localhost:8000")
|
|
110
|
+
with mock.patch.object(config, "_try_get_keyring", return_value=None):
|
|
111
|
+
config.save_token("vpcli_demo_secret")
|
|
112
|
+
|
|
113
|
+
fake_client = mock.Mock()
|
|
114
|
+
fake_client.create_note.side_effect = NetworkError("connection dropped")
|
|
115
|
+
|
|
116
|
+
with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
|
|
117
|
+
result = self.runner.invoke(
|
|
118
|
+
app,
|
|
119
|
+
["notes", "create", "--project", "1", "--body", "Captured from CLI"],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
self.assertEqual(result.exit_code, 0)
|
|
123
|
+
queue_store = QueueStore()
|
|
124
|
+
queued = queue_store.list_unsynced()
|
|
125
|
+
self.assertEqual(len(queued), 1)
|
|
126
|
+
self.assertEqual(queued[0].sync_state, "pending")
|
|
127
|
+
finally:
|
|
128
|
+
os.environ.pop(config.CONFIG_ENV_VAR, None)
|
|
129
|
+
os.environ.pop(config.DATA_ENV_VAR, None)
|
|
130
|
+
|
|
131
|
+
def test_notes_create_does_not_queue_validation_errors(self):
|
|
132
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
133
|
+
os.environ[config.CONFIG_ENV_VAR] = tmpdir
|
|
134
|
+
os.environ[config.DATA_ENV_VAR] = tmpdir
|
|
135
|
+
try:
|
|
136
|
+
config.set_base_url("http://localhost:8000")
|
|
137
|
+
with mock.patch.object(config, "_try_get_keyring", return_value=None):
|
|
138
|
+
config.save_token("vpcli_demo_secret")
|
|
139
|
+
|
|
140
|
+
fake_client = mock.Mock()
|
|
141
|
+
fake_client.create_note.side_effect = ApiError(400, "Validation failed")
|
|
142
|
+
|
|
143
|
+
with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
|
|
144
|
+
result = self.runner.invoke(
|
|
145
|
+
app,
|
|
146
|
+
["notes", "create", "--project", "1", "--body", "Captured from CLI"],
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
self.assertNotEqual(result.exit_code, 0)
|
|
150
|
+
queue_store = QueueStore()
|
|
151
|
+
self.assertEqual(queue_store.count(), 0)
|
|
152
|
+
finally:
|
|
153
|
+
os.environ.pop(config.CONFIG_ENV_VAR, None)
|
|
154
|
+
os.environ.pop(config.DATA_ENV_VAR, None)
|
|
155
|
+
|
|
156
|
+
def test_notes_create_derives_title_from_first_non_empty_body_line(self):
|
|
157
|
+
fake_client = mock.Mock()
|
|
158
|
+
fake_client.create_note.return_value = {
|
|
159
|
+
"note": {"note_ref": "general-info:1"},
|
|
160
|
+
"audit_entry_id": 12,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
|
|
164
|
+
result = self.runner.invoke(
|
|
165
|
+
app,
|
|
166
|
+
["notes", "create", "--project", "1", "--body", "\n\n First captured line \nSecond line"],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
self.assertEqual(result.exit_code, 0)
|
|
170
|
+
payload = fake_client.create_note.call_args.args[0]
|
|
171
|
+
self.assertEqual(payload["title"], "First captured line")
|
|
172
|
+
self.assertEqual(payload["body"], "\n\n First captured line \nSecond line")
|
|
173
|
+
|
|
174
|
+
def test_notes_sync_retries_queued_notes_and_marks_them_synced(self):
|
|
175
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
176
|
+
os.environ[config.CONFIG_ENV_VAR] = tmpdir
|
|
177
|
+
os.environ[config.DATA_ENV_VAR] = tmpdir
|
|
178
|
+
try:
|
|
179
|
+
queue_store = QueueStore()
|
|
180
|
+
queued = queue_store.enqueue(
|
|
181
|
+
project_id=1,
|
|
182
|
+
title="Queued note",
|
|
183
|
+
body="Queued body",
|
|
184
|
+
tags=["cli"],
|
|
185
|
+
captured_at="2026-03-10T10:00:00-07:00",
|
|
186
|
+
idempotency_key="queued-note-1",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
fake_client = mock.Mock()
|
|
190
|
+
fake_client.create_note.return_value = {
|
|
191
|
+
"note": {"note_ref": "general-info:99"},
|
|
192
|
+
"audit_entry_id": 25,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
|
|
196
|
+
result = self.runner.invoke(app, ["notes", "sync", "--project", "1", "--json"])
|
|
197
|
+
|
|
198
|
+
self.assertEqual(result.exit_code, 0)
|
|
199
|
+
payload = json.loads(result.stdout)
|
|
200
|
+
self.assertEqual(payload["total"], 1)
|
|
201
|
+
self.assertEqual(payload["synced"], 1)
|
|
202
|
+
self.assertEqual(payload["failed"], 0)
|
|
203
|
+
self.assertEqual(payload["results"][0]["note_ref"], "general-info:99")
|
|
204
|
+
|
|
205
|
+
synced = queue_store.get(queued.local_id)
|
|
206
|
+
self.assertEqual(synced.sync_state, "synced")
|
|
207
|
+
self.assertEqual(synced.server_note_ref, "general-info:99")
|
|
208
|
+
fake_client.create_note.assert_called_once_with(
|
|
209
|
+
{
|
|
210
|
+
"project": 1,
|
|
211
|
+
"title": "Queued note",
|
|
212
|
+
"body": "Queued body",
|
|
213
|
+
"tags": ["cli"],
|
|
214
|
+
"captured_at": "2026-03-10T10:00:00-07:00",
|
|
215
|
+
},
|
|
216
|
+
idempotency_key="queued-note-1",
|
|
217
|
+
)
|
|
218
|
+
finally:
|
|
219
|
+
os.environ.pop(config.CONFIG_ENV_VAR, None)
|
|
220
|
+
os.environ.pop(config.DATA_ENV_VAR, None)
|
|
221
|
+
|
|
222
|
+
def test_notes_list_json_outputs_server_payload(self):
|
|
223
|
+
fake_client = mock.Mock()
|
|
224
|
+
fake_client.list_notes.return_value = {
|
|
225
|
+
"project_id": 1,
|
|
226
|
+
"count": 1,
|
|
227
|
+
"limit": 20,
|
|
228
|
+
"offset": 0,
|
|
229
|
+
"results": [
|
|
230
|
+
{
|
|
231
|
+
"note_ref": "general-info:1",
|
|
232
|
+
"title": "CLI Note",
|
|
233
|
+
"type": "general-info",
|
|
234
|
+
"source": "cli",
|
|
235
|
+
}
|
|
236
|
+
],
|
|
237
|
+
}
|
|
238
|
+
with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
|
|
239
|
+
result = self.runner.invoke(app, ["notes", "list", "--project", "1", "--json"])
|
|
240
|
+
|
|
241
|
+
self.assertEqual(result.exit_code, 0)
|
|
242
|
+
payload = json.loads(result.stdout)
|
|
243
|
+
self.assertEqual(payload["results"][0]["note_ref"], "general-info:1")
|
|
244
|
+
|
|
245
|
+
def test_notes_list_passes_filters_to_api_client(self):
|
|
246
|
+
fake_client = mock.Mock()
|
|
247
|
+
fake_client.list_notes.return_value = {
|
|
248
|
+
"project_id": 1,
|
|
249
|
+
"count": 0,
|
|
250
|
+
"limit": 5,
|
|
251
|
+
"offset": 10,
|
|
252
|
+
"results": [],
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
with mock.patch.object(auth, "get_authenticated_client", return_value=(fake_client, "http://localhost:8000")):
|
|
256
|
+
result = self.runner.invoke(
|
|
257
|
+
app,
|
|
258
|
+
[
|
|
259
|
+
"notes",
|
|
260
|
+
"list",
|
|
261
|
+
"--project",
|
|
262
|
+
"1",
|
|
263
|
+
"--limit",
|
|
264
|
+
"5",
|
|
265
|
+
"--offset",
|
|
266
|
+
"10",
|
|
267
|
+
"--type",
|
|
268
|
+
"general-info",
|
|
269
|
+
"--source",
|
|
270
|
+
"cli",
|
|
271
|
+
"--tags",
|
|
272
|
+
"field,urgent",
|
|
273
|
+
"--query",
|
|
274
|
+
"terminal",
|
|
275
|
+
"--json",
|
|
276
|
+
],
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
self.assertEqual(result.exit_code, 0)
|
|
280
|
+
fake_client.list_notes.assert_called_once_with(
|
|
281
|
+
1,
|
|
282
|
+
limit=5,
|
|
283
|
+
offset=10,
|
|
284
|
+
note_type="general-info",
|
|
285
|
+
source="cli",
|
|
286
|
+
tags=["field", "urgent"],
|
|
287
|
+
query="terminal",
|
|
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}]})
|