iris-devtester 1.8.1__py3-none-any.whl → 1.9.1__py3-none-any.whl
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.
- iris_devtester/__init__.py +3 -2
- iris_devtester/cli/__init__.py +4 -2
- iris_devtester/cli/__main__.py +1 -1
- iris_devtester/cli/connection_commands.py +31 -51
- iris_devtester/cli/container.py +42 -113
- iris_devtester/cli/container_commands.py +6 -4
- iris_devtester/cli/fixture_commands.py +97 -73
- iris_devtester/config/auto_discovery.py +8 -20
- iris_devtester/config/container_config.py +24 -35
- iris_devtester/config/container_state.py +19 -43
- iris_devtester/config/discovery.py +10 -10
- iris_devtester/config/presets.py +3 -10
- iris_devtester/config/yaml_loader.py +3 -2
- iris_devtester/connections/__init__.py +25 -30
- iris_devtester/connections/connection.py +4 -3
- iris_devtester/connections/dbapi.py +5 -1
- iris_devtester/connections/jdbc.py +2 -6
- iris_devtester/connections/manager.py +1 -1
- iris_devtester/connections/retry.py +2 -5
- iris_devtester/containers/__init__.py +6 -6
- iris_devtester/containers/cpf_manager.py +13 -12
- iris_devtester/containers/iris_container.py +268 -436
- iris_devtester/containers/models.py +18 -43
- iris_devtester/containers/monitor_utils.py +1 -3
- iris_devtester/containers/monitoring.py +31 -46
- iris_devtester/containers/performance.py +5 -5
- iris_devtester/containers/validation.py +27 -60
- iris_devtester/containers/wait_strategies.py +13 -4
- iris_devtester/fixtures/__init__.py +14 -13
- iris_devtester/fixtures/creator.py +127 -555
- iris_devtester/fixtures/loader.py +221 -78
- iris_devtester/fixtures/manifest.py +8 -6
- iris_devtester/fixtures/obj_export.py +45 -35
- iris_devtester/fixtures/validator.py +4 -7
- iris_devtester/integrations/langchain.py +2 -6
- iris_devtester/ports/registry.py +5 -4
- iris_devtester/testing/__init__.py +3 -0
- iris_devtester/testing/fixtures.py +10 -1
- iris_devtester/testing/helpers.py +5 -12
- iris_devtester/testing/models.py +3 -2
- iris_devtester/testing/schema_reset.py +1 -3
- iris_devtester/utils/__init__.py +20 -5
- iris_devtester/utils/container_port.py +2 -6
- iris_devtester/utils/container_status.py +2 -6
- iris_devtester/utils/dbapi_compat.py +29 -14
- iris_devtester/utils/enable_callin.py +5 -7
- iris_devtester/utils/health_checks.py +18 -33
- iris_devtester/utils/iris_container_adapter.py +27 -26
- iris_devtester/utils/password.py +673 -0
- iris_devtester/utils/progress.py +1 -1
- iris_devtester/utils/test_connection.py +4 -6
- {iris_devtester-1.8.1.dist-info → iris_devtester-1.9.1.dist-info}/METADATA +7 -7
- iris_devtester-1.9.1.dist-info/RECORD +66 -0
- {iris_devtester-1.8.1.dist-info → iris_devtester-1.9.1.dist-info}/WHEEL +1 -1
- iris_devtester/utils/password_reset.py +0 -594
- iris_devtester/utils/password_verification.py +0 -350
- iris_devtester/utils/unexpire_passwords.py +0 -168
- iris_devtester-1.8.1.dist-info/RECORD +0 -68
- {iris_devtester-1.8.1.dist-info → iris_devtester-1.9.1.dist-info}/entry_points.txt +0 -0
- {iris_devtester-1.8.1.dist-info → iris_devtester-1.9.1.dist-info}/licenses/LICENSE +0 -0
- {iris_devtester-1.8.1.dist-info → iris_devtester-1.9.1.dist-info}/top_level.txt +0 -0
|
@@ -159,12 +159,7 @@ class ValidationResult:
|
|
|
159
159
|
return "\n".join(lines).rstrip()
|
|
160
160
|
|
|
161
161
|
@classmethod
|
|
162
|
-
def healthy(
|
|
163
|
-
cls,
|
|
164
|
-
name: str,
|
|
165
|
-
container_id: str,
|
|
166
|
-
validation_time: float
|
|
167
|
-
) -> "ValidationResult":
|
|
162
|
+
def healthy(cls, name: str, container_id: str, validation_time: float) -> "ValidationResult":
|
|
168
163
|
"""Factory method for healthy container.
|
|
169
164
|
|
|
170
165
|
Args:
|
|
@@ -182,15 +177,12 @@ class ValidationResult:
|
|
|
182
177
|
container_id=container_id,
|
|
183
178
|
message=f"Container '{name}' is running and accessible",
|
|
184
179
|
remediation_steps=[],
|
|
185
|
-
validation_time=validation_time
|
|
180
|
+
validation_time=validation_time,
|
|
186
181
|
)
|
|
187
182
|
|
|
188
183
|
@classmethod
|
|
189
184
|
def not_found(
|
|
190
|
-
cls,
|
|
191
|
-
name: str,
|
|
192
|
-
available_containers: List[str],
|
|
193
|
-
validation_time: float
|
|
185
|
+
cls, name: str, available_containers: List[str], validation_time: float
|
|
194
186
|
) -> "ValidationResult":
|
|
195
187
|
"""Factory method for container not found.
|
|
196
188
|
|
|
@@ -211,19 +203,15 @@ class ValidationResult:
|
|
|
211
203
|
remediation_steps=[
|
|
212
204
|
"1. List all containers:\n docker ps -a",
|
|
213
205
|
f"2. Start container if it exists:\n docker start {name}",
|
|
214
|
-
f"3. Or create new container:\n docker run -d --name {name} intersystemsdc/iris-community:latest"
|
|
206
|
+
f"3. Or create new container:\n docker run -d --name {name} intersystemsdc/iris-community:latest",
|
|
215
207
|
],
|
|
216
208
|
available_containers=available_containers,
|
|
217
|
-
validation_time=validation_time
|
|
209
|
+
validation_time=validation_time,
|
|
218
210
|
)
|
|
219
211
|
|
|
220
212
|
@classmethod
|
|
221
213
|
def not_running(
|
|
222
|
-
cls,
|
|
223
|
-
name: str,
|
|
224
|
-
container_id: str,
|
|
225
|
-
validation_time: float,
|
|
226
|
-
container_status: str = "exited"
|
|
214
|
+
cls, name: str, container_id: str, validation_time: float, container_status: str = "exited"
|
|
227
215
|
) -> "ValidationResult":
|
|
228
216
|
"""Factory method for stopped container.
|
|
229
217
|
|
|
@@ -242,19 +230,13 @@ class ValidationResult:
|
|
|
242
230
|
container_name=name,
|
|
243
231
|
container_id=container_id,
|
|
244
232
|
message=f"Container exists but is not running (status: {container_status}).",
|
|
245
|
-
remediation_steps=[
|
|
246
|
-
|
|
247
|
-
],
|
|
248
|
-
validation_time=validation_time
|
|
233
|
+
remediation_steps=[f"docker start {name}"],
|
|
234
|
+
validation_time=validation_time,
|
|
249
235
|
)
|
|
250
236
|
|
|
251
237
|
@classmethod
|
|
252
238
|
def not_accessible(
|
|
253
|
-
cls,
|
|
254
|
-
name: str,
|
|
255
|
-
container_id: str,
|
|
256
|
-
error: str,
|
|
257
|
-
validation_time: float
|
|
239
|
+
cls, name: str, container_id: str, error: str, validation_time: float
|
|
258
240
|
) -> "ValidationResult":
|
|
259
241
|
"""Factory method for inaccessible container.
|
|
260
242
|
|
|
@@ -276,18 +258,14 @@ class ValidationResult:
|
|
|
276
258
|
remediation_steps=[
|
|
277
259
|
f"1. Restart container:\n docker restart {name}",
|
|
278
260
|
f"2. Check container logs:\n docker logs {name} | tail -20",
|
|
279
|
-
f"3. Enable CallIn service (for IRIS):\n iris-devtester container enable-callin {name}"
|
|
261
|
+
f"3. Enable CallIn service (for IRIS):\n iris-devtester container enable-callin {name}",
|
|
280
262
|
],
|
|
281
|
-
validation_time=validation_time
|
|
263
|
+
validation_time=validation_time,
|
|
282
264
|
)
|
|
283
265
|
|
|
284
266
|
@classmethod
|
|
285
267
|
def stale_reference(
|
|
286
|
-
cls,
|
|
287
|
-
name: str,
|
|
288
|
-
cached_id: str,
|
|
289
|
-
current_id: str,
|
|
290
|
-
validation_time: float
|
|
268
|
+
cls, name: str, cached_id: str, current_id: str, validation_time: float
|
|
291
269
|
) -> "ValidationResult":
|
|
292
270
|
"""Factory method for stale container reference.
|
|
293
271
|
|
|
@@ -314,17 +292,14 @@ class ValidationResult:
|
|
|
314
292
|
"1. Clear cached references and restart:\n"
|
|
315
293
|
" # Exit Python session and restart\n"
|
|
316
294
|
" # Or recreate IRISContainer context manager",
|
|
317
|
-
f"2. Verify container is running:\n docker ps | grep {name}"
|
|
295
|
+
f"2. Verify container is running:\n docker ps | grep {name}",
|
|
318
296
|
],
|
|
319
|
-
validation_time=validation_time
|
|
297
|
+
validation_time=validation_time,
|
|
320
298
|
)
|
|
321
299
|
|
|
322
300
|
@classmethod
|
|
323
301
|
def docker_error(
|
|
324
|
-
cls,
|
|
325
|
-
name: str,
|
|
326
|
-
error: Exception,
|
|
327
|
-
validation_time: float
|
|
302
|
+
cls, name: str, error: Exception, validation_time: float
|
|
328
303
|
) -> "ValidationResult":
|
|
329
304
|
"""Factory method for Docker daemon errors.
|
|
330
305
|
|
|
@@ -347,9 +322,9 @@ class ValidationResult:
|
|
|
347
322
|
"2. Start Docker Desktop (macOS/Windows)\n"
|
|
348
323
|
" # Or start Docker daemon (Linux):\n"
|
|
349
324
|
" sudo systemctl start docker",
|
|
350
|
-
"3. Verify Docker is accessible:\n docker ps"
|
|
325
|
+
"3. Verify Docker is accessible:\n docker ps",
|
|
351
326
|
],
|
|
352
|
-
validation_time=validation_time
|
|
327
|
+
validation_time=validation_time,
|
|
353
328
|
)
|
|
354
329
|
|
|
355
330
|
|
|
@@ -420,7 +395,7 @@ class ContainerHealth:
|
|
|
420
395
|
"started_at": self.started_at,
|
|
421
396
|
"port_bindings": self.port_bindings,
|
|
422
397
|
"image": self.image,
|
|
423
|
-
"docker_sdk_version": self.docker_sdk_version
|
|
398
|
+
"docker_sdk_version": self.docker_sdk_version,
|
|
424
399
|
}
|
|
425
400
|
|
|
426
401
|
def is_healthy(self) -> bool:
|
|
@@ -103,9 +103,7 @@ def is_monitor_collecting(conn) -> Tuple[bool, int]:
|
|
|
103
103
|
return False, 0
|
|
104
104
|
|
|
105
105
|
|
|
106
|
-
def get_monitor_samples(
|
|
107
|
-
conn, table: str = "HistoryPerf", limit: int = 10
|
|
108
|
-
) -> List[Dict]:
|
|
106
|
+
def get_monitor_samples(conn, table: str = "HistoryPerf", limit: int = 10) -> List[Dict]:
|
|
109
107
|
"""
|
|
110
108
|
Get recent monitoring samples from %Monitor.System.
|
|
111
109
|
|
|
@@ -136,9 +136,7 @@ class MonitoringPolicy:
|
|
|
136
136
|
|
|
137
137
|
# Validate output directory is absolute path
|
|
138
138
|
if not self.output_directory.startswith("/"):
|
|
139
|
-
raise ValueError(
|
|
140
|
-
f"Output directory must be absolute path: {self.output_directory}"
|
|
141
|
-
)
|
|
139
|
+
raise ValueError(f"Output directory must be absolute path: {self.output_directory}")
|
|
142
140
|
|
|
143
141
|
def to_objectscript(self) -> str:
|
|
144
142
|
"""
|
|
@@ -369,8 +367,7 @@ class ResourceThresholds:
|
|
|
369
367
|
True if monitoring should be disabled
|
|
370
368
|
"""
|
|
371
369
|
return (
|
|
372
|
-
cpu_percent > self.cpu_disable_percent
|
|
373
|
-
or memory_percent > self.memory_disable_percent
|
|
370
|
+
cpu_percent > self.cpu_disable_percent or memory_percent > self.memory_disable_percent
|
|
374
371
|
)
|
|
375
372
|
|
|
376
373
|
def should_enable(self, cpu_percent: float, memory_percent: float) -> bool:
|
|
@@ -384,10 +381,7 @@ class ResourceThresholds:
|
|
|
384
381
|
Returns:
|
|
385
382
|
True if monitoring can be safely re-enabled
|
|
386
383
|
"""
|
|
387
|
-
return
|
|
388
|
-
cpu_percent < self.cpu_enable_percent
|
|
389
|
-
and memory_percent < self.memory_enable_percent
|
|
390
|
-
)
|
|
384
|
+
return cpu_percent < self.cpu_enable_percent and memory_percent < self.memory_enable_percent
|
|
391
385
|
|
|
392
386
|
|
|
393
387
|
@dataclass
|
|
@@ -520,7 +514,7 @@ def configure_monitoring(
|
|
|
520
514
|
logger.debug(f"Executing ObjectScript to configure policy '{policy.name}'")
|
|
521
515
|
|
|
522
516
|
# Check if connection has execute_objectscript method (test fixture provides this)
|
|
523
|
-
if hasattr(conn,
|
|
517
|
+
if hasattr(conn, "execute_objectscript"):
|
|
524
518
|
conn.execute_objectscript(objectscript)
|
|
525
519
|
else:
|
|
526
520
|
# Fallback error for production (until Feature 003)
|
|
@@ -800,9 +794,7 @@ def create_task(conn, schedule: TaskSchedule) -> str:
|
|
|
800
794
|
result = cursor.fetchone()
|
|
801
795
|
|
|
802
796
|
if not result:
|
|
803
|
-
raise RuntimeError(
|
|
804
|
-
f"Task '{schedule.name}' was created but ID could not be retrieved"
|
|
805
|
-
)
|
|
797
|
+
raise RuntimeError(f"Task '{schedule.name}' was created but ID could not be retrieved")
|
|
806
798
|
|
|
807
799
|
task_id = str(result[0])
|
|
808
800
|
logger.info(f"✓ Created Task Manager task: {schedule.name} (ID: {task_id})")
|
|
@@ -927,10 +919,7 @@ def suspend_task(conn, task_id: str) -> bool:
|
|
|
927
919
|
|
|
928
920
|
# Use SQL UPDATE to suspend the task (works with DBAPI!)
|
|
929
921
|
cursor = conn.cursor()
|
|
930
|
-
cursor.execute(
|
|
931
|
-
"UPDATE %SYS.Task SET Suspended = 1 WHERE ID = ?",
|
|
932
|
-
(task_id,)
|
|
933
|
-
)
|
|
922
|
+
cursor.execute("UPDATE %SYS.Task SET Suspended = 1 WHERE ID = ?", (task_id,))
|
|
934
923
|
conn.commit()
|
|
935
924
|
|
|
936
925
|
# Verify it was updated
|
|
@@ -945,23 +934,23 @@ def suspend_task(conn, task_id: str) -> bool:
|
|
|
945
934
|
schedule = TaskSchedule(task_id=task_id)
|
|
946
935
|
objectscript = schedule.disable()
|
|
947
936
|
|
|
948
|
-
if hasattr(conn,
|
|
937
|
+
if hasattr(conn, "execute_objectscript"):
|
|
949
938
|
conn.execute_objectscript(objectscript)
|
|
950
939
|
else:
|
|
951
940
|
raise NotImplementedError(
|
|
952
941
|
"ObjectScript execution not available\n"
|
|
953
942
|
"\n"
|
|
954
943
|
"What went wrong:\n"
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
944
|
+
" This connection does not support ObjectScript execution.\n"
|
|
945
|
+
" DBAPI connections are SQL-only.\n"
|
|
946
|
+
"\n"
|
|
947
|
+
"How to fix it:\n"
|
|
948
|
+
" 1. Use a JDBC connection (supports ObjectScript via stored procedures)\n"
|
|
949
|
+
" 2. Or wait for Feature 003 (Connection Manager) which provides\n"
|
|
950
|
+
" hybrid DBAPI/JDBC connections with ObjectScript support\n"
|
|
951
|
+
"\n"
|
|
952
|
+
"See: docs/learnings/dbapi-objectscript-limitation.md\n"
|
|
953
|
+
)
|
|
965
954
|
|
|
966
955
|
logger.info(f"✓ Suspended task: {task_id}")
|
|
967
956
|
return True
|
|
@@ -1013,10 +1002,7 @@ def resume_task(conn, task_id: str) -> bool:
|
|
|
1013
1002
|
|
|
1014
1003
|
# Use SQL UPDATE to resume the task (works with DBAPI!)
|
|
1015
1004
|
cursor = conn.cursor()
|
|
1016
|
-
cursor.execute(
|
|
1017
|
-
"UPDATE %SYS.Task SET Suspended = 0 WHERE ID = ?",
|
|
1018
|
-
(task_id,)
|
|
1019
|
-
)
|
|
1005
|
+
cursor.execute("UPDATE %SYS.Task SET Suspended = 0 WHERE ID = ?", (task_id,))
|
|
1020
1006
|
conn.commit()
|
|
1021
1007
|
|
|
1022
1008
|
# Verify it was updated
|
|
@@ -1031,7 +1017,7 @@ def resume_task(conn, task_id: str) -> bool:
|
|
|
1031
1017
|
schedule = TaskSchedule(task_id=task_id)
|
|
1032
1018
|
objectscript = schedule.enable()
|
|
1033
1019
|
|
|
1034
|
-
if hasattr(conn,
|
|
1020
|
+
if hasattr(conn, "execute_objectscript"):
|
|
1035
1021
|
conn.execute_objectscript(objectscript)
|
|
1036
1022
|
else:
|
|
1037
1023
|
raise NotImplementedError(
|
|
@@ -1047,7 +1033,7 @@ def resume_task(conn, task_id: str) -> bool:
|
|
|
1047
1033
|
" hybrid DBAPI/JDBC connections with ObjectScript support\n"
|
|
1048
1034
|
"\n"
|
|
1049
1035
|
"See: docs/learnings/dbapi-objectscript-limitation.md\n"
|
|
1050
|
-
|
|
1036
|
+
)
|
|
1051
1037
|
|
|
1052
1038
|
logger.info(f"✓ Resumed task: {task_id}")
|
|
1053
1039
|
return True
|
|
@@ -1096,16 +1082,13 @@ def delete_task(conn, task_id: str) -> bool:
|
|
|
1096
1082
|
|
|
1097
1083
|
# Use SQL DELETE (works with DBAPI!)
|
|
1098
1084
|
cursor = conn.cursor()
|
|
1099
|
-
cursor.execute(
|
|
1100
|
-
"DELETE FROM %SYS.Task WHERE ID = ?",
|
|
1101
|
-
(task_id,)
|
|
1102
|
-
)
|
|
1085
|
+
cursor.execute("DELETE FROM %SYS.Task WHERE ID = ?", (task_id,))
|
|
1103
1086
|
conn.commit()
|
|
1104
1087
|
|
|
1105
1088
|
# Check if anything was deleted
|
|
1106
1089
|
if cursor.rowcount == 0:
|
|
1107
1090
|
# Fallback to ObjectScript if SQL didn't work
|
|
1108
|
-
if hasattr(conn,
|
|
1091
|
+
if hasattr(conn, "execute_objectscript"):
|
|
1109
1092
|
objectscript = f"""
|
|
1110
1093
|
set task = ##class(%SYS.Task).%OpenId("{task_id}")
|
|
1111
1094
|
if $IsObject(task) {{
|
|
@@ -1183,13 +1166,15 @@ def list_monitoring_tasks(conn) -> list:
|
|
|
1183
1166
|
|
|
1184
1167
|
tasks = []
|
|
1185
1168
|
for row in results:
|
|
1186
|
-
tasks.append(
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1169
|
+
tasks.append(
|
|
1170
|
+
{
|
|
1171
|
+
"task_id": str(row[0]) if row[0] else "",
|
|
1172
|
+
"name": row[1] if row[1] else "",
|
|
1173
|
+
"suspended": bool(row[2]) if row[2] is not None else False,
|
|
1174
|
+
"daily_increment": int(row[3]) if row[3] else 0,
|
|
1175
|
+
"task_class": row[4] if row[4] else "",
|
|
1176
|
+
}
|
|
1177
|
+
)
|
|
1193
1178
|
|
|
1194
1179
|
logger.info(f"✓ Found {len(tasks)} monitoring task(s)")
|
|
1195
1180
|
return tasks
|
|
@@ -11,7 +11,7 @@ import json
|
|
|
11
11
|
import logging
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from datetime import datetime
|
|
14
|
-
from typing import
|
|
14
|
+
from typing import TYPE_CHECKING, Optional
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from iris_devtester.containers.monitoring import ResourceThresholds
|
|
@@ -142,7 +142,9 @@ def get_resource_metrics(conn) -> PerformanceMetrics:
|
|
|
142
142
|
monitoring_enabled=has_active_task,
|
|
143
143
|
)
|
|
144
144
|
|
|
145
|
-
logger.debug(
|
|
145
|
+
logger.debug(
|
|
146
|
+
f"✓ Metrics: CPU={metrics.cpu_percent:.1f}% Memory={metrics.memory_percent:.1f}%"
|
|
147
|
+
)
|
|
146
148
|
return metrics
|
|
147
149
|
|
|
148
150
|
except Exception as e:
|
|
@@ -161,9 +163,7 @@ def get_resource_metrics(conn) -> PerformanceMetrics:
|
|
|
161
163
|
raise RuntimeError(error_msg) from e
|
|
162
164
|
|
|
163
165
|
|
|
164
|
-
def check_resource_thresholds(
|
|
165
|
-
conn, thresholds: "ResourceThresholds"
|
|
166
|
-
) -> tuple:
|
|
166
|
+
def check_resource_thresholds(conn, thresholds: "ResourceThresholds") -> tuple:
|
|
167
167
|
"""
|
|
168
168
|
Check if current resources exceed thresholds.
|
|
169
169
|
|
|
@@ -12,16 +12,16 @@ Constitutional Compliance:
|
|
|
12
12
|
|
|
13
13
|
import logging
|
|
14
14
|
import time
|
|
15
|
-
from typing import
|
|
15
|
+
from typing import List, Optional
|
|
16
16
|
|
|
17
17
|
import docker
|
|
18
|
-
from docker.errors import DockerException, NotFound
|
|
18
|
+
from docker.errors import APIError, DockerException, NotFound
|
|
19
19
|
|
|
20
20
|
from iris_devtester.containers.models import (
|
|
21
|
+
ContainerHealth,
|
|
21
22
|
ContainerHealthStatus,
|
|
22
23
|
HealthCheckLevel,
|
|
23
24
|
ValidationResult,
|
|
24
|
-
ContainerHealth,
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
logger = logging.getLogger(__name__)
|
|
@@ -31,7 +31,7 @@ def validate_container(
|
|
|
31
31
|
container_name: str,
|
|
32
32
|
level: HealthCheckLevel = HealthCheckLevel.STANDARD,
|
|
33
33
|
timeout: int = 10,
|
|
34
|
-
docker_client: Optional[docker.DockerClient] = None
|
|
34
|
+
docker_client: Optional[docker.DockerClient] = None,
|
|
35
35
|
) -> ValidationResult:
|
|
36
36
|
"""
|
|
37
37
|
Validate Docker container health with progressive checks.
|
|
@@ -90,9 +90,7 @@ def validate_container(
|
|
|
90
90
|
except DockerException as e:
|
|
91
91
|
elapsed = time.time() - start_time
|
|
92
92
|
return ValidationResult.docker_error(
|
|
93
|
-
name=container_name,
|
|
94
|
-
error=e,
|
|
95
|
-
validation_time=elapsed
|
|
93
|
+
name=container_name, error=e, validation_time=elapsed
|
|
96
94
|
)
|
|
97
95
|
|
|
98
96
|
# Progressive validation strategy
|
|
@@ -105,9 +103,7 @@ def validate_container(
|
|
|
105
103
|
elapsed = time.time() - start_time
|
|
106
104
|
available = _get_available_containers(docker_client)
|
|
107
105
|
return ValidationResult.not_found(
|
|
108
|
-
name=container_name,
|
|
109
|
-
available_containers=available,
|
|
110
|
-
validation_time=elapsed
|
|
106
|
+
name=container_name, available_containers=available, validation_time=elapsed
|
|
111
107
|
)
|
|
112
108
|
|
|
113
109
|
# Step 2: Check running status (MINIMAL level)
|
|
@@ -120,23 +116,18 @@ def validate_container(
|
|
|
120
116
|
name=container_name,
|
|
121
117
|
container_id=container.id,
|
|
122
118
|
validation_time=elapsed,
|
|
123
|
-
container_status=container.status
|
|
119
|
+
container_status=container.status,
|
|
124
120
|
)
|
|
125
121
|
|
|
126
122
|
# MINIMAL check complete
|
|
127
123
|
if level == HealthCheckLevel.MINIMAL:
|
|
128
124
|
elapsed = time.time() - start_time
|
|
129
125
|
return ValidationResult.healthy(
|
|
130
|
-
name=container_name,
|
|
131
|
-
container_id=container.id,
|
|
132
|
-
validation_time=elapsed
|
|
126
|
+
name=container_name, container_id=container.id, validation_time=elapsed
|
|
133
127
|
)
|
|
134
128
|
|
|
135
129
|
# Step 3: Check exec accessibility (STANDARD level)
|
|
136
|
-
is_accessible, access_error = _check_exec_accessibility(
|
|
137
|
-
container,
|
|
138
|
-
timeout=timeout
|
|
139
|
-
)
|
|
130
|
+
is_accessible, access_error = _check_exec_accessibility(container, timeout=timeout)
|
|
140
131
|
|
|
141
132
|
if not is_accessible:
|
|
142
133
|
elapsed = time.time() - start_time
|
|
@@ -144,23 +135,18 @@ def validate_container(
|
|
|
144
135
|
name=container_name,
|
|
145
136
|
container_id=container.id,
|
|
146
137
|
error=access_error or "Unknown exec error",
|
|
147
|
-
validation_time=elapsed
|
|
138
|
+
validation_time=elapsed,
|
|
148
139
|
)
|
|
149
140
|
|
|
150
141
|
# STANDARD check complete
|
|
151
142
|
if level == HealthCheckLevel.STANDARD:
|
|
152
143
|
elapsed = time.time() - start_time
|
|
153
144
|
return ValidationResult.healthy(
|
|
154
|
-
name=container_name,
|
|
155
|
-
container_id=container.id,
|
|
156
|
-
validation_time=elapsed
|
|
145
|
+
name=container_name, container_id=container.id, validation_time=elapsed
|
|
157
146
|
)
|
|
158
147
|
|
|
159
148
|
# Step 4: IRIS-specific health check (FULL level)
|
|
160
|
-
is_iris_healthy, iris_error = _check_iris_health(
|
|
161
|
-
container,
|
|
162
|
-
timeout=timeout
|
|
163
|
-
)
|
|
149
|
+
is_iris_healthy, iris_error = _check_iris_health(container, timeout=timeout)
|
|
164
150
|
|
|
165
151
|
if not is_iris_healthy:
|
|
166
152
|
elapsed = time.time() - start_time
|
|
@@ -168,29 +154,22 @@ def validate_container(
|
|
|
168
154
|
name=container_name,
|
|
169
155
|
container_id=container.id,
|
|
170
156
|
error=f"IRIS not responsive: {iris_error}",
|
|
171
|
-
validation_time=elapsed
|
|
157
|
+
validation_time=elapsed,
|
|
172
158
|
)
|
|
173
159
|
|
|
174
160
|
# FULL check complete - container is healthy
|
|
175
161
|
elapsed = time.time() - start_time
|
|
176
162
|
return ValidationResult.healthy(
|
|
177
|
-
name=container_name,
|
|
178
|
-
container_id=container.id,
|
|
179
|
-
validation_time=elapsed
|
|
163
|
+
name=container_name, container_id=container.id, validation_time=elapsed
|
|
180
164
|
)
|
|
181
165
|
|
|
182
166
|
except DockerException as e:
|
|
183
167
|
elapsed = time.time() - start_time
|
|
184
|
-
return ValidationResult.docker_error(
|
|
185
|
-
name=container_name,
|
|
186
|
-
error=e,
|
|
187
|
-
validation_time=elapsed
|
|
188
|
-
)
|
|
168
|
+
return ValidationResult.docker_error(name=container_name, error=e, validation_time=elapsed)
|
|
189
169
|
|
|
190
170
|
|
|
191
171
|
def _get_container_by_name(
|
|
192
|
-
client: docker.DockerClient,
|
|
193
|
-
name: str
|
|
172
|
+
client: docker.DockerClient, name: str
|
|
194
173
|
) -> Optional[docker.models.containers.Container]:
|
|
195
174
|
"""Get container by name.
|
|
196
175
|
|
|
@@ -210,9 +189,7 @@ def _get_container_by_name(
|
|
|
210
189
|
return None
|
|
211
190
|
|
|
212
191
|
|
|
213
|
-
def _get_available_containers(
|
|
214
|
-
client: docker.DockerClient
|
|
215
|
-
) -> List[str]:
|
|
192
|
+
def _get_available_containers(client: docker.DockerClient) -> List[str]:
|
|
216
193
|
"""Get list of available container names.
|
|
217
194
|
|
|
218
195
|
Args:
|
|
@@ -234,8 +211,7 @@ def _get_available_containers(
|
|
|
234
211
|
|
|
235
212
|
|
|
236
213
|
def _check_exec_accessibility(
|
|
237
|
-
container: docker.models.containers.Container,
|
|
238
|
-
timeout: int = 10
|
|
214
|
+
container: docker.models.containers.Container, timeout: int = 10
|
|
239
215
|
) -> tuple[bool, Optional[str]]:
|
|
240
216
|
"""Check if container accepts exec commands.
|
|
241
217
|
|
|
@@ -248,10 +224,7 @@ def _check_exec_accessibility(
|
|
|
248
224
|
"""
|
|
249
225
|
try:
|
|
250
226
|
# Simple echo command to test exec
|
|
251
|
-
exec_result = container.exec_run(
|
|
252
|
-
"echo healthy",
|
|
253
|
-
demux=False
|
|
254
|
-
)
|
|
227
|
+
exec_result = container.exec_run("echo healthy", demux=False)
|
|
255
228
|
|
|
256
229
|
if exec_result.exit_code == 0:
|
|
257
230
|
return True, None
|
|
@@ -263,8 +236,7 @@ def _check_exec_accessibility(
|
|
|
263
236
|
|
|
264
237
|
|
|
265
238
|
def _check_iris_health(
|
|
266
|
-
container: docker.models.containers.Container,
|
|
267
|
-
timeout: int = 10
|
|
239
|
+
container: docker.models.containers.Container, timeout: int = 10
|
|
268
240
|
) -> tuple[bool, Optional[str]]:
|
|
269
241
|
"""Check IRIS-specific health (FULL validation level).
|
|
270
242
|
|
|
@@ -281,8 +253,7 @@ def _check_iris_health(
|
|
|
281
253
|
# Try to execute simple IRIS query
|
|
282
254
|
# This uses the iris session command available in IRIS containers
|
|
283
255
|
exec_result = container.exec_run(
|
|
284
|
-
"iris session IRIS -U %SYS '##class(%SYSTEM.Process).CurrentDirectory()'",
|
|
285
|
-
demux=False
|
|
256
|
+
"iris session IRIS -U %SYS '##class(%SYSTEM.Process).CurrentDirectory()'", demux=False
|
|
286
257
|
)
|
|
287
258
|
|
|
288
259
|
if exec_result.exit_code == 0:
|
|
@@ -312,7 +283,7 @@ class ContainerValidator:
|
|
|
312
283
|
self,
|
|
313
284
|
container_name: str,
|
|
314
285
|
docker_client: Optional[docker.DockerClient] = None,
|
|
315
|
-
cache_ttl: int = 5
|
|
286
|
+
cache_ttl: int = 5,
|
|
316
287
|
):
|
|
317
288
|
"""
|
|
318
289
|
Initialize validator for specific container.
|
|
@@ -335,9 +306,7 @@ class ContainerValidator:
|
|
|
335
306
|
self._cached_health: Optional[ContainerHealth] = None
|
|
336
307
|
|
|
337
308
|
def validate(
|
|
338
|
-
self,
|
|
339
|
-
level: HealthCheckLevel = HealthCheckLevel.STANDARD,
|
|
340
|
-
force_refresh: bool = False
|
|
309
|
+
self, level: HealthCheckLevel = HealthCheckLevel.STANDARD, force_refresh: bool = False
|
|
341
310
|
) -> ValidationResult:
|
|
342
311
|
"""
|
|
343
312
|
Validate container health.
|
|
@@ -350,15 +319,13 @@ class ContainerValidator:
|
|
|
350
319
|
ValidationResult (may be cached).
|
|
351
320
|
"""
|
|
352
321
|
# Check cache
|
|
353
|
-
if not force_refresh and self._is_cache_valid():
|
|
322
|
+
if not force_refresh and self._is_cache_valid() and self._cached_result is not None:
|
|
354
323
|
logger.debug(f"Using cached validation result for {self._container_name}")
|
|
355
324
|
return self._cached_result
|
|
356
325
|
|
|
357
326
|
# Perform validation
|
|
358
327
|
result = validate_container(
|
|
359
|
-
container_name=self._container_name,
|
|
360
|
-
level=level,
|
|
361
|
-
docker_client=self._docker_client
|
|
328
|
+
container_name=self._container_name, level=level, docker_client=self._docker_client
|
|
362
329
|
)
|
|
363
330
|
|
|
364
331
|
# Update cache
|
|
@@ -419,7 +386,7 @@ class ContainerValidator:
|
|
|
419
386
|
started_at=container.attrs.get("State", {}).get("StartedAt"),
|
|
420
387
|
port_bindings=port_bindings,
|
|
421
388
|
image=container.image.tags[0] if container.image.tags else None,
|
|
422
|
-
docker_sdk_version=docker.__version__
|
|
389
|
+
docker_sdk_version=docker.__version__,
|
|
423
390
|
)
|
|
424
391
|
|
|
425
392
|
# Cache result
|
|
@@ -455,7 +422,7 @@ class ContainerValidator:
|
|
|
455
422
|
|
|
456
423
|
try:
|
|
457
424
|
container = self._docker_client.containers.get(self._container_name)
|
|
458
|
-
return container.id
|
|
425
|
+
return str(container.id) if container.id else None
|
|
459
426
|
except Exception:
|
|
460
427
|
return None
|
|
461
428
|
|
|
@@ -129,7 +129,18 @@ class IRISReadyWaitStrategy:
|
|
|
129
129
|
def check_iris_initialized(self, container_name: str) -> bool:
|
|
130
130
|
try:
|
|
131
131
|
result = subprocess.run(
|
|
132
|
-
[
|
|
132
|
+
[
|
|
133
|
+
"docker",
|
|
134
|
+
"exec",
|
|
135
|
+
container_name,
|
|
136
|
+
"iris",
|
|
137
|
+
"session",
|
|
138
|
+
"IRIS",
|
|
139
|
+
"-U",
|
|
140
|
+
"%SYS",
|
|
141
|
+
"W 1",
|
|
142
|
+
"Halt",
|
|
143
|
+
],
|
|
133
144
|
capture_output=True,
|
|
134
145
|
text=True,
|
|
135
146
|
timeout=10,
|
|
@@ -218,9 +229,7 @@ def wait_for_iris_ready(
|
|
|
218
229
|
... else:
|
|
219
230
|
... print("Timeout waiting for IRIS")
|
|
220
231
|
"""
|
|
221
|
-
strategy = IRISReadyWaitStrategy(
|
|
222
|
-
port=port, timeout=timeout, poll_interval=poll_interval
|
|
223
|
-
)
|
|
232
|
+
strategy = IRISReadyWaitStrategy(port=port, timeout=timeout, poll_interval=poll_interval)
|
|
224
233
|
|
|
225
234
|
try:
|
|
226
235
|
return strategy.wait_until_ready(host, port, timeout)
|
|
@@ -52,36 +52,37 @@ pytest Integration:
|
|
|
52
52
|
|
|
53
53
|
__version__ = "0.1.0"
|
|
54
54
|
|
|
55
|
+
from .creator import FixtureCreator
|
|
56
|
+
from .loader import DATFixtureLoader
|
|
57
|
+
|
|
55
58
|
# Import data models and exceptions
|
|
56
59
|
from .manifest import (
|
|
60
|
+
ChecksumMismatchError,
|
|
61
|
+
FixtureCreateError,
|
|
62
|
+
FixtureError,
|
|
63
|
+
FixtureLoadError,
|
|
57
64
|
FixtureManifest,
|
|
65
|
+
FixtureValidationError,
|
|
66
|
+
LoadResult,
|
|
58
67
|
TableInfo,
|
|
59
68
|
ValidationResult,
|
|
60
|
-
LoadResult,
|
|
61
|
-
FixtureError,
|
|
62
|
-
FixtureValidationError,
|
|
63
|
-
FixtureLoadError,
|
|
64
|
-
FixtureCreateError,
|
|
65
|
-
ChecksumMismatchError,
|
|
66
69
|
)
|
|
67
70
|
|
|
68
|
-
# Import validator, loader, and creator
|
|
69
|
-
from .validator import FixtureValidator
|
|
70
|
-
from .loader import DATFixtureLoader
|
|
71
|
-
from .creator import FixtureCreator
|
|
72
|
-
|
|
73
71
|
# Import $SYSTEM.OBJ export/import utilities
|
|
74
72
|
# Source: docs/learnings/iris-backup-patterns.md
|
|
75
73
|
from .obj_export import (
|
|
76
74
|
ExportResult,
|
|
77
75
|
ImportResult,
|
|
78
76
|
export_classes,
|
|
79
|
-
import_classes,
|
|
80
77
|
export_global,
|
|
81
|
-
import_global,
|
|
82
78
|
export_package,
|
|
79
|
+
import_classes,
|
|
80
|
+
import_global,
|
|
83
81
|
)
|
|
84
82
|
|
|
83
|
+
# Import validator, loader, and creator
|
|
84
|
+
from .validator import FixtureValidator
|
|
85
|
+
|
|
85
86
|
# Public API
|
|
86
87
|
__all__ = [
|
|
87
88
|
# Data models
|