qapu-cli 0.63.0__tar.gz → 0.64.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.
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/PKG-INFO +1 -1
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/pyproject.toml +1 -1
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/device_edit.py +32 -11
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli.egg-info/PKG-INFO +1 -1
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/tests/test_tui_device_edit.py +31 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/README.md +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/__init__.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/client.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/main.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/__init__.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/app.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/app.tcss +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/data.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/rendering.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/__init__.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/catalog.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/catalog_form.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/catalog_picker.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/confirm.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/dashboard.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/device_list.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/equipment_form.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/group_detail.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/group_form.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/group_list.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/infrastructure.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/widgets/__init__.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/widgets/device_table.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli.egg-info/SOURCES.txt +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli.egg-info/dependency_links.txt +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli.egg-info/entry_points.txt +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli.egg-info/requires.txt +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli.egg-info/top_level.txt +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/setup.cfg +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/tests/test_login.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/tests/test_tui_catalog.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/tests/test_tui_groups.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/tests/test_tui_infrastructure.py +0 -0
- {qapu_cli-0.63.0 → qapu_cli-0.64.0}/tests/test_tui_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qapu-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.64.0
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "qapu-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.64.0"
|
|
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"
|
|
@@ -116,35 +116,52 @@ class DeviceEditScreen(ModalScreen[bool]):
|
|
|
116
116
|
self._all_groups = groups
|
|
117
117
|
self._original_group_ids = group_ids
|
|
118
118
|
|
|
119
|
-
# Proje
|
|
119
|
+
# Proje - Cihazın Şu Anki Project.ID Katalogda Bulunmayabilir (en
|
|
120
|
+
# Başta id=0 "Unknown" Placeholder - GET /hermes/projects Onu
|
|
121
|
+
# Bilerek Hariç Tutuyor, bkz. Device.list()'in Aynı Kuralı) -
|
|
122
|
+
# Select.value'yu Sadece Gerçekten Seçenekler Arasındaysa Set Et,
|
|
123
|
+
# Yoksa Boş Bırak (Select.NULL) - Kör Atama Textual'ın Kendi
|
|
124
|
+
# InvalidSelectValueError'ını Fırlatıp Tüm TUI'yi Çökertiyordu
|
|
125
|
+
# (Canlı Yakalandı, 2026-10-05: Project_ID=0 Olan Gerçek bir Cihaz
|
|
126
|
+
# Üzerinde). _do_save() zaten Select.NULL'ü "Kullanıcı Seçim
|
|
127
|
+
# Yapmadı" Olarak Okuyor, o Yüzden Boş Bırakmak Mevcut Değeri
|
|
128
|
+
# Sessizce Korur - Kullanıcı Elle Yeni bir Proje Seçerse Yine
|
|
129
|
+
# Normal Şekilde pending'e Girer.
|
|
130
|
+
project_options = [(p.get("Name") or f"#{p['ID']}", p["ID"]) for p in projects if p.get("ID") is not None]
|
|
120
131
|
project_select = self.query_one("#edit-project", Select)
|
|
121
|
-
project_select.set_options(
|
|
122
|
-
if self._initial_project_id is not None:
|
|
132
|
+
project_select.set_options(project_options)
|
|
133
|
+
if self._initial_project_id is not None and self._initial_project_id in {pid for _, pid in project_options}:
|
|
123
134
|
project_select.value = self._initial_project_id
|
|
124
135
|
|
|
125
136
|
# Cihaz Durumu - Sadece 1xx (Cihaz Durumları) Aralığı, Başlık
|
|
126
|
-
# Satırı (id=100) Hariç - device_edit()'in Kendi Filtresiyle Birebir
|
|
137
|
+
# Satırı (id=100) Hariç - device_edit()'in Kendi Filtresiyle Birebir.
|
|
138
|
+
# Aynı Gerekçeyle (Yukarıdaki Proje Notu) Mevcut Status.ID Filtreli
|
|
139
|
+
# Listede Yoksa Boş Bırak.
|
|
127
140
|
device_statuses = [s for s in statuses if s.get("ID") is not None and 100 <= s["ID"] < 200 and s["ID"] != 100]
|
|
141
|
+
status_options = [(s.get("Name") or f"#{s['ID']}", s["ID"]) for s in device_statuses]
|
|
128
142
|
status_select = self.query_one("#edit-status", Select)
|
|
129
|
-
status_select.set_options(
|
|
130
|
-
if self._initial_status_id is not None:
|
|
143
|
+
status_select.set_options(status_options)
|
|
144
|
+
if self._initial_status_id is not None and self._initial_status_id in {sid for _, sid in status_options}:
|
|
131
145
|
status_select.value = self._initial_status_id
|
|
132
146
|
|
|
133
147
|
# Üretici - Yuvarlak-Bin Kategori Başlık Satırlarını Hariç Tut (Aynı
|
|
134
148
|
# device_edit()/manufacturer_list Konvansiyonu)
|
|
135
149
|
real_manufacturers = [m for m in manufacturers if m.get("ID") is not None and m["ID"] % 1000 != 0]
|
|
150
|
+
manufacturer_options = [(m.get("Name") or f"#{m['ID']}", m["ID"]) for m in real_manufacturers]
|
|
136
151
|
manufacturer_select = self.query_one("#edit-manufacturer", Select)
|
|
137
|
-
manufacturer_select.set_options(
|
|
152
|
+
manufacturer_select.set_options(manufacturer_options)
|
|
138
153
|
|
|
139
154
|
# Model - Mevcut Modelin Üreticisini manufacturer_models'daki Kendi
|
|
140
155
|
# Kaydından Çöz (Hardware.Model'in Manufacturer_ID Taşıdığı
|
|
141
|
-
# Varsayılmıyor - Doğrulanmadı), Sonra o Üreticiye Göre Filtrele
|
|
156
|
+
# Varsayılmıyor - Doğrulanmadı), Sonra o Üreticiye Göre Filtrele.
|
|
157
|
+
# initial_manufacturer_id'nin Üretici Listesinde Gerçekten Var
|
|
158
|
+
# Olduğu da Ayrıca Doğrulanıyor (Aynı Gerekçe).
|
|
142
159
|
initial_manufacturer_id: Optional[int] = None
|
|
143
160
|
if self._initial_model_id is not None:
|
|
144
161
|
current_model = next((mo for mo in models if mo.get("ID") == self._initial_model_id), None)
|
|
145
162
|
if current_model:
|
|
146
163
|
initial_manufacturer_id = current_model.get("Manufacturer_ID")
|
|
147
|
-
if initial_manufacturer_id is not None:
|
|
164
|
+
if initial_manufacturer_id is not None and initial_manufacturer_id in {mid for _, mid in manufacturer_options}:
|
|
148
165
|
manufacturer_select.value = initial_manufacturer_id
|
|
149
166
|
self._set_model_options(initial_manufacturer_id, preselect=self._initial_model_id)
|
|
150
167
|
|
|
@@ -184,8 +201,12 @@ class DeviceEditScreen(ModalScreen[bool]):
|
|
|
184
201
|
def _set_model_options(self, manufacturer_id: int, preselect: Optional[int]) -> None:
|
|
185
202
|
model_select = self.query_one("#edit-model", Select)
|
|
186
203
|
matching = [mo for mo in self._all_models if mo.get("Manufacturer_ID") == manufacturer_id]
|
|
187
|
-
|
|
188
|
-
|
|
204
|
+
model_options = [(mo.get("Name") or f"#{mo['ID']}", mo["ID"]) for mo in matching if mo.get("ID") is not None]
|
|
205
|
+
model_select.set_options(model_options)
|
|
206
|
+
# Aynı Gerekçe (Yukarıdaki Proje/Durum/Üretici Notu) - preselect Bu
|
|
207
|
+
# Üreticinin Filtrelenmiş Model Listesinde Gerçekten Yoksa Boş
|
|
208
|
+
# Bırak, Kör Atama Çökertmesin.
|
|
209
|
+
if preselect is not None and preselect in {mid for _, mid in model_options}:
|
|
189
210
|
model_select.value = preselect
|
|
190
211
|
else:
|
|
191
212
|
model_select.value = Select.NULL
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qapu-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.64.0
|
|
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
|
|
@@ -127,6 +127,37 @@ async def test_group_change_with_assigned_by_calls_assign(monkeypatch):
|
|
|
127
127
|
assert assign_calls == [(2, "TEST-DEVICE-1", 204)]
|
|
128
128
|
|
|
129
129
|
|
|
130
|
+
@pytest.mark.asyncio
|
|
131
|
+
async def test_device_with_project_not_in_catalog_opens_without_crashing(monkeypatch):
|
|
132
|
+
# Real Crash Caught Live (2026-10-05): a Device Whose Current
|
|
133
|
+
# Project.ID is 0 ("Unknown" Placeholder, Deliberately Excluded From
|
|
134
|
+
# GET /hermes/projects - Same Exclusion Project.list() Already
|
|
135
|
+
# Applies) has no Matching Entry in the Catalog-Fetched Options, so
|
|
136
|
+
# Blindly Assigning Select.value = 0 Raised Textual's own
|
|
137
|
+
# InvalidSelectValueError and Crashed the Whole TUI. The Fix Leaves
|
|
138
|
+
# the Field Blank (Select.NULL) Instead of Crashing - Confirmed Here
|
|
139
|
+
# for Project/Status/Manufacturer/Model all at Once (Same Bug Class,
|
|
140
|
+
# Same Fix Shape, in _apply_options()/_set_model_options()).
|
|
141
|
+
_install_catalog_mocks(monkeypatch, device_group_ids=[])
|
|
142
|
+
device_with_unknown_everything = {
|
|
143
|
+
"ID": "TEST-DEVICE-2",
|
|
144
|
+
"Name": "Eski Cihaz",
|
|
145
|
+
"Description": "",
|
|
146
|
+
"Project": {"ID": 0, "Name": "Unknown"},
|
|
147
|
+
"Status": {"ID": 999, "Name": "Not In Catalog"},
|
|
148
|
+
"Hardware": {"Model": {"ID": 9999, "Name": "Not In Catalog"}},
|
|
149
|
+
}
|
|
150
|
+
app = QapuTUIApp()
|
|
151
|
+
async with app.run_test() as pilot:
|
|
152
|
+
await app.push_screen(DeviceEditScreen("TEST-DEVICE-2", device_with_unknown_everything))
|
|
153
|
+
await pilot.pause(0.2)
|
|
154
|
+
from textual.widgets import Select
|
|
155
|
+
assert isinstance(app.screen, DeviceEditScreen)
|
|
156
|
+
assert app.screen.query_one("#edit-project", Select).value is Select.NULL
|
|
157
|
+
assert app.screen.query_one("#edit-status", Select).value is Select.NULL
|
|
158
|
+
assert app.screen.query_one("#edit-model", Select).value is Select.NULL
|
|
159
|
+
|
|
160
|
+
|
|
130
161
|
@pytest.mark.asyncio
|
|
131
162
|
async def test_save_with_nothing_changed_calls_no_mutation(monkeypatch):
|
|
132
163
|
_install_catalog_mocks(monkeypatch, device_group_ids=[1])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|