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.
Files changed (39) hide show
  1. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/PKG-INFO +1 -1
  2. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/pyproject.toml +1 -1
  3. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/device_edit.py +32 -11
  4. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli.egg-info/PKG-INFO +1 -1
  5. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/tests/test_tui_device_edit.py +31 -0
  6. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/README.md +0 -0
  7. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/__init__.py +0 -0
  8. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/client.py +0 -0
  9. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/main.py +0 -0
  10. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/__init__.py +0 -0
  11. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/app.py +0 -0
  12. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/app.tcss +0 -0
  13. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/data.py +0 -0
  14. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/rendering.py +0 -0
  15. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/__init__.py +0 -0
  16. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/catalog.py +0 -0
  17. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/catalog_form.py +0 -0
  18. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/catalog_picker.py +0 -0
  19. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/confirm.py +0 -0
  20. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/dashboard.py +0 -0
  21. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/device_list.py +0 -0
  22. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/equipment_form.py +0 -0
  23. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/group_detail.py +0 -0
  24. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/group_form.py +0 -0
  25. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/group_list.py +0 -0
  26. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/screens/infrastructure.py +0 -0
  27. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/widgets/__init__.py +0 -0
  28. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli/tui/widgets/device_table.py +0 -0
  29. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli.egg-info/SOURCES.txt +0 -0
  30. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli.egg-info/dependency_links.txt +0 -0
  31. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli.egg-info/entry_points.txt +0 -0
  32. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli.egg-info/requires.txt +0 -0
  33. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/qapu_cli.egg-info/top_level.txt +0 -0
  34. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/setup.cfg +0 -0
  35. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/tests/test_login.py +0 -0
  36. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/tests/test_tui_catalog.py +0 -0
  37. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/tests/test_tui_groups.py +0 -0
  38. {qapu_cli-0.63.0 → qapu_cli-0.64.0}/tests/test_tui_infrastructure.py +0 -0
  39. {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.63.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.63.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([(p.get("Name") or f"#{p['ID']}", p["ID"]) for p in projects if p.get("ID") is not None])
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([(s.get("Name") or f"#{s['ID']}", s["ID"]) for s in device_statuses])
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([(m.get("Name") or f"#{m['ID']}", m["ID"]) for m in real_manufacturers])
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
- model_select.set_options([(mo.get("Name") or f"#{mo['ID']}", mo["ID"]) for mo in matching if mo.get("ID") is not None])
188
- if preselect is not None:
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.63.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