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.
Files changed (61) hide show
  1. iris_devtester/__init__.py +3 -2
  2. iris_devtester/cli/__init__.py +4 -2
  3. iris_devtester/cli/__main__.py +1 -1
  4. iris_devtester/cli/connection_commands.py +31 -51
  5. iris_devtester/cli/container.py +42 -113
  6. iris_devtester/cli/container_commands.py +6 -4
  7. iris_devtester/cli/fixture_commands.py +97 -73
  8. iris_devtester/config/auto_discovery.py +8 -20
  9. iris_devtester/config/container_config.py +24 -35
  10. iris_devtester/config/container_state.py +19 -43
  11. iris_devtester/config/discovery.py +10 -10
  12. iris_devtester/config/presets.py +3 -10
  13. iris_devtester/config/yaml_loader.py +3 -2
  14. iris_devtester/connections/__init__.py +25 -30
  15. iris_devtester/connections/connection.py +4 -3
  16. iris_devtester/connections/dbapi.py +5 -1
  17. iris_devtester/connections/jdbc.py +2 -6
  18. iris_devtester/connections/manager.py +1 -1
  19. iris_devtester/connections/retry.py +2 -5
  20. iris_devtester/containers/__init__.py +6 -6
  21. iris_devtester/containers/cpf_manager.py +13 -12
  22. iris_devtester/containers/iris_container.py +268 -436
  23. iris_devtester/containers/models.py +18 -43
  24. iris_devtester/containers/monitor_utils.py +1 -3
  25. iris_devtester/containers/monitoring.py +31 -46
  26. iris_devtester/containers/performance.py +5 -5
  27. iris_devtester/containers/validation.py +27 -60
  28. iris_devtester/containers/wait_strategies.py +13 -4
  29. iris_devtester/fixtures/__init__.py +14 -13
  30. iris_devtester/fixtures/creator.py +127 -555
  31. iris_devtester/fixtures/loader.py +221 -78
  32. iris_devtester/fixtures/manifest.py +8 -6
  33. iris_devtester/fixtures/obj_export.py +45 -35
  34. iris_devtester/fixtures/validator.py +4 -7
  35. iris_devtester/integrations/langchain.py +2 -6
  36. iris_devtester/ports/registry.py +5 -4
  37. iris_devtester/testing/__init__.py +3 -0
  38. iris_devtester/testing/fixtures.py +10 -1
  39. iris_devtester/testing/helpers.py +5 -12
  40. iris_devtester/testing/models.py +3 -2
  41. iris_devtester/testing/schema_reset.py +1 -3
  42. iris_devtester/utils/__init__.py +20 -5
  43. iris_devtester/utils/container_port.py +2 -6
  44. iris_devtester/utils/container_status.py +2 -6
  45. iris_devtester/utils/dbapi_compat.py +29 -14
  46. iris_devtester/utils/enable_callin.py +5 -7
  47. iris_devtester/utils/health_checks.py +18 -33
  48. iris_devtester/utils/iris_container_adapter.py +27 -26
  49. iris_devtester/utils/password.py +673 -0
  50. iris_devtester/utils/progress.py +1 -1
  51. iris_devtester/utils/test_connection.py +4 -6
  52. {iris_devtester-1.8.1.dist-info → iris_devtester-1.9.1.dist-info}/METADATA +7 -7
  53. iris_devtester-1.9.1.dist-info/RECORD +66 -0
  54. {iris_devtester-1.8.1.dist-info → iris_devtester-1.9.1.dist-info}/WHEEL +1 -1
  55. iris_devtester/utils/password_reset.py +0 -594
  56. iris_devtester/utils/password_verification.py +0 -350
  57. iris_devtester/utils/unexpire_passwords.py +0 -168
  58. iris_devtester-1.8.1.dist-info/RECORD +0 -68
  59. {iris_devtester-1.8.1.dist-info → iris_devtester-1.9.1.dist-info}/entry_points.txt +0 -0
  60. {iris_devtester-1.8.1.dist-info → iris_devtester-1.9.1.dist-info}/licenses/LICENSE +0 -0
  61. {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
- f"docker start {name}"
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, 'execute_objectscript'):
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, 'execute_objectscript'):
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
- " This connection does not support ObjectScript execution.\n"
956
- " DBAPI connections are SQL-only.\n"
957
- "\n"
958
- "How to fix it:\n"
959
- " 1. Use a JDBC connection (supports ObjectScript via stored procedures)\n"
960
- " 2. Or wait for Feature 003 (Connection Manager) which provides\n"
961
- " hybrid DBAPI/JDBC connections with ObjectScript support\n"
962
- "\n"
963
- "See: docs/learnings/dbapi-objectscript-limitation.md\n"
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, 'execute_objectscript'):
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, 'execute_objectscript'):
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
- "task_id": str(row[0]) if row[0] else "",
1188
- "name": row[1] if row[1] else "",
1189
- "suspended": bool(row[2]) if row[2] is not None else False,
1190
- "daily_increment": int(row[3]) if row[3] else 0,
1191
- "task_class": row[4] if row[4] else "",
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 Optional, TYPE_CHECKING
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(f"✓ Metrics: CPU={metrics.cpu_percent:.1f}% Memory={metrics.memory_percent:.1f}%")
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 Optional, List
15
+ from typing import List, Optional
16
16
 
17
17
  import docker
18
- from docker.errors import DockerException, NotFound, APIError
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
- ["docker", "exec", container_name, "iris", "session", "IRIS", "-U", "%SYS", "W 1", "Halt"],
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