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
|
@@ -10,6 +10,7 @@ from pydantic import BaseModel, Field, field_validator
|
|
|
10
10
|
|
|
11
11
|
class ContainerStatus(str, Enum):
|
|
12
12
|
"""Container lifecycle status."""
|
|
13
|
+
|
|
13
14
|
CREATING = "creating"
|
|
14
15
|
STARTING = "starting"
|
|
15
16
|
RUNNING = "running"
|
|
@@ -20,6 +21,7 @@ class ContainerStatus(str, Enum):
|
|
|
20
21
|
|
|
21
22
|
class HealthStatus(str, Enum):
|
|
22
23
|
"""Docker health check status."""
|
|
24
|
+
|
|
23
25
|
STARTING = "starting"
|
|
24
26
|
HEALTHY = "healthy"
|
|
25
27
|
UNHEALTHY = "unhealthy"
|
|
@@ -54,47 +56,21 @@ class ContainerState(BaseModel):
|
|
|
54
56
|
"""
|
|
55
57
|
|
|
56
58
|
container_id: str = Field(
|
|
57
|
-
...,
|
|
58
|
-
min_length=64,
|
|
59
|
-
max_length=64,
|
|
60
|
-
description="Docker container ID (full hash)"
|
|
61
|
-
)
|
|
62
|
-
container_name: str = Field(
|
|
63
|
-
...,
|
|
64
|
-
description="Container name"
|
|
65
|
-
)
|
|
66
|
-
status: ContainerStatus = Field(
|
|
67
|
-
...,
|
|
68
|
-
description="Current lifecycle state"
|
|
59
|
+
..., min_length=64, max_length=64, description="Docker container ID (full hash)"
|
|
69
60
|
)
|
|
61
|
+
container_name: str = Field(..., description="Container name")
|
|
62
|
+
status: ContainerStatus = Field(..., description="Current lifecycle state")
|
|
70
63
|
health_status: HealthStatus = Field(
|
|
71
|
-
default=HealthStatus.NONE,
|
|
72
|
-
description="Docker health check status"
|
|
73
|
-
)
|
|
74
|
-
created_at: datetime = Field(
|
|
75
|
-
...,
|
|
76
|
-
description="Container creation timestamp"
|
|
77
|
-
)
|
|
78
|
-
started_at: Optional[datetime] = Field(
|
|
79
|
-
default=None,
|
|
80
|
-
description="Last start timestamp"
|
|
81
|
-
)
|
|
82
|
-
finished_at: Optional[datetime] = Field(
|
|
83
|
-
default=None,
|
|
84
|
-
description="Last stop timestamp"
|
|
64
|
+
default=HealthStatus.NONE, description="Docker health check status"
|
|
85
65
|
)
|
|
66
|
+
created_at: datetime = Field(..., description="Container creation timestamp")
|
|
67
|
+
started_at: Optional[datetime] = Field(default=None, description="Last start timestamp")
|
|
68
|
+
finished_at: Optional[datetime] = Field(default=None, description="Last stop timestamp")
|
|
86
69
|
ports: Dict[int, int] = Field(
|
|
87
|
-
default_factory=dict,
|
|
88
|
-
description="Port mappings (container -> host)"
|
|
89
|
-
)
|
|
90
|
-
image: str = Field(
|
|
91
|
-
...,
|
|
92
|
-
description="Full image reference"
|
|
93
|
-
)
|
|
94
|
-
config_source: Optional[Path] = Field(
|
|
95
|
-
default=None,
|
|
96
|
-
description="Source config file (if any)"
|
|
70
|
+
default_factory=dict, description="Port mappings (container -> host)"
|
|
97
71
|
)
|
|
72
|
+
image: str = Field(..., description="Full image reference")
|
|
73
|
+
config_source: Optional[Path] = Field(default=None, description="Source config file (if any)")
|
|
98
74
|
|
|
99
75
|
@field_validator("container_id")
|
|
100
76
|
@classmethod
|
|
@@ -223,10 +199,7 @@ class ContainerState(BaseModel):
|
|
|
223
199
|
Returns:
|
|
224
200
|
True if status is healthy and health_status is healthy
|
|
225
201
|
"""
|
|
226
|
-
return
|
|
227
|
-
self.status == ContainerStatus.HEALTHY
|
|
228
|
-
and self.health_status == HealthStatus.HEALTHY
|
|
229
|
-
)
|
|
202
|
+
return self.status == ContainerStatus.HEALTHY and self.health_status == HealthStatus.HEALTHY
|
|
230
203
|
|
|
231
204
|
def get_uptime_seconds(self) -> Optional[float]:
|
|
232
205
|
"""
|
|
@@ -274,8 +247,10 @@ class ContainerState(BaseModel):
|
|
|
274
247
|
if not self.ports:
|
|
275
248
|
return "None"
|
|
276
249
|
|
|
277
|
-
port_strs = [
|
|
278
|
-
|
|
250
|
+
port_strs = [
|
|
251
|
+
f"{container_port}->{host_port}"
|
|
252
|
+
for container_port, host_port in sorted(self.ports.items())
|
|
253
|
+
]
|
|
279
254
|
return ", ".join(port_strs)
|
|
280
255
|
|
|
281
256
|
def to_text_output(self) -> str:
|
|
@@ -331,6 +306,7 @@ class ContainerState(BaseModel):
|
|
|
331
306
|
|
|
332
307
|
class Config:
|
|
333
308
|
"""Pydantic model configuration."""
|
|
309
|
+
|
|
334
310
|
json_schema_extra = {
|
|
335
311
|
"example": {
|
|
336
312
|
"container_id": "a1b2c3d4e5f6" + "0" * 52, # 64 chars
|
|
@@ -342,6 +318,6 @@ class ContainerState(BaseModel):
|
|
|
342
318
|
"finished_at": None,
|
|
343
319
|
"ports": {1972: 1972, 52773: 52773},
|
|
344
320
|
"image": "intersystems/iris-community:latest",
|
|
345
|
-
"config_source": None
|
|
321
|
+
"config_source": None,
|
|
346
322
|
}
|
|
347
323
|
}
|
|
@@ -11,18 +11,18 @@ Automatically discovers IRIS configuration from multiple sources:
|
|
|
11
11
|
|
|
12
12
|
import os
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import Optional
|
|
14
|
+
from typing import Any, Dict, Optional, Union
|
|
15
15
|
|
|
16
|
-
from iris_devtester.config.models import IRISConfig
|
|
17
16
|
from iris_devtester.config.defaults import (
|
|
17
|
+
DEFAULT_DRIVER,
|
|
18
18
|
DEFAULT_HOST,
|
|
19
|
-
DEFAULT_PORT,
|
|
20
19
|
DEFAULT_NAMESPACE,
|
|
21
|
-
DEFAULT_USERNAME,
|
|
22
20
|
DEFAULT_PASSWORD,
|
|
23
|
-
|
|
21
|
+
DEFAULT_PORT,
|
|
24
22
|
DEFAULT_TIMEOUT,
|
|
23
|
+
DEFAULT_USERNAME,
|
|
25
24
|
)
|
|
25
|
+
from iris_devtester.config.models import IRISConfig
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def discover_config(explicit_config: Optional[IRISConfig] = None) -> IRISConfig:
|
|
@@ -56,7 +56,7 @@ def discover_config(explicit_config: Optional[IRISConfig] = None) -> IRISConfig:
|
|
|
56
56
|
return explicit_config
|
|
57
57
|
|
|
58
58
|
# Start with defaults
|
|
59
|
-
discovered = {
|
|
59
|
+
discovered: Dict[str, Any] = {
|
|
60
60
|
"host": DEFAULT_HOST,
|
|
61
61
|
"port": DEFAULT_PORT,
|
|
62
62
|
"namespace": DEFAULT_NAMESPACE,
|
|
@@ -91,7 +91,7 @@ def discover_config(explicit_config: Optional[IRISConfig] = None) -> IRISConfig:
|
|
|
91
91
|
return IRISConfig(**discovered)
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
def _load_from_environment() ->
|
|
94
|
+
def _load_from_environment() -> Dict[str, Any]:
|
|
95
95
|
"""
|
|
96
96
|
Load configuration from environment variables.
|
|
97
97
|
|
|
@@ -107,7 +107,7 @@ def _load_from_environment() -> dict:
|
|
|
107
107
|
Returns:
|
|
108
108
|
Dictionary of discovered configuration values
|
|
109
109
|
"""
|
|
110
|
-
config = {}
|
|
110
|
+
config: Dict[str, Any] = {}
|
|
111
111
|
|
|
112
112
|
if "IRIS_HOST" in os.environ:
|
|
113
113
|
config["host"] = os.environ["IRIS_HOST"]
|
|
@@ -133,7 +133,7 @@ def _load_from_environment() -> dict:
|
|
|
133
133
|
return config
|
|
134
134
|
|
|
135
135
|
|
|
136
|
-
def _load_from_dotenv() ->
|
|
136
|
+
def _load_from_dotenv() -> Dict[str, Any]:
|
|
137
137
|
"""
|
|
138
138
|
Load configuration from .env file in current directory.
|
|
139
139
|
|
|
@@ -142,7 +142,7 @@ def _load_from_dotenv() -> dict:
|
|
|
142
142
|
Returns:
|
|
143
143
|
Dictionary of discovered configuration values
|
|
144
144
|
"""
|
|
145
|
-
config = {}
|
|
145
|
+
config: Dict[str, Any] = {}
|
|
146
146
|
dotenv_path = Path.cwd() / ".env"
|
|
147
147
|
|
|
148
148
|
if not dotenv_path.exists():
|
iris_devtester/config/presets.py
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
class CPFPreset:
|
|
2
|
-
ENABLE_CALLIN =
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
)
|
|
6
|
-
|
|
7
|
-
CI_OPTIMIZED = (
|
|
8
|
-
"[config]\n"
|
|
9
|
-
"globals=0,0,256,0,0,0\n"
|
|
10
|
-
"gmheap=64000"
|
|
11
|
-
)
|
|
2
|
+
ENABLE_CALLIN = "[Actions]\n" "ModifyService:Name=%Service_CallIn,Enabled=1,AutheEnabled=48"
|
|
3
|
+
|
|
4
|
+
CI_OPTIMIZED = "[config]\n" "globals=0,0,256,0,0,0\n" "gmheap=64000"
|
|
12
5
|
|
|
13
6
|
SECURE_DEFAULTS = (
|
|
14
7
|
"[Actions]\n"
|
|
@@ -10,7 +10,7 @@ For advanced usage, see the legacy manager module.
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
# Modern DBAPI-only API (recommended)
|
|
13
|
-
from iris_devtester.connections.connection import
|
|
13
|
+
from iris_devtester.connections.connection import IRISConnection, get_connection
|
|
14
14
|
|
|
15
15
|
# Compatibility layer for contract tests
|
|
16
16
|
# -----------------------------------------------------------------
|
|
@@ -21,6 +21,7 @@ from iris_devtester.connections.connection import get_connection, IRISConnection
|
|
|
21
21
|
# - IRISConnectionManager (class exposing config, driver_type, get_connection, close_all)
|
|
22
22
|
# These are provided as thin wrappers around the modern implementation.
|
|
23
23
|
|
|
24
|
+
|
|
24
25
|
# Alias expected by contract tests
|
|
25
26
|
def get_iris_connection(config=None, **kwargs):
|
|
26
27
|
"""Contract‑compatible alias for :func:`get_connection`.
|
|
@@ -31,15 +32,18 @@ def get_iris_connection(config=None, **kwargs):
|
|
|
31
32
|
"""
|
|
32
33
|
# Check if we are in a contract test (mocking)
|
|
33
34
|
import sys
|
|
34
|
-
|
|
35
|
+
|
|
36
|
+
if "pytest" in sys.modules:
|
|
35
37
|
from unittest.mock import MagicMock
|
|
38
|
+
|
|
36
39
|
return MagicMock()
|
|
37
40
|
|
|
38
41
|
# Map legacy keywords to modern get_connection parameters.
|
|
39
|
-
auto_retry = kwargs.get(
|
|
40
|
-
max_retries = kwargs.get(
|
|
42
|
+
auto_retry = kwargs.get("auto_remediate", True)
|
|
43
|
+
max_retries = kwargs.get("retry_attempts", 3)
|
|
41
44
|
return get_connection(config=config, auto_retry=auto_retry, max_retries=max_retries)
|
|
42
45
|
|
|
46
|
+
|
|
43
47
|
# Reset‑password helper – re‑exported for compatibility with contract tests
|
|
44
48
|
def reset_password_if_needed(config_or_error, **kwargs):
|
|
45
49
|
"""Contract‑compatible wrapper for password reset.
|
|
@@ -47,9 +51,9 @@ def reset_password_if_needed(config_or_error, **kwargs):
|
|
|
47
51
|
If first arg is an exception, calls the modern utility.
|
|
48
52
|
If first arg is a config, attempts remediation and returns result object.
|
|
49
53
|
"""
|
|
50
|
-
from iris_devtester.utils.password_reset import reset_password_if_needed as modern_reset
|
|
51
54
|
from iris_devtester.testing.models import PasswordResetResult as ContractResult
|
|
52
|
-
from iris_devtester.utils.
|
|
55
|
+
from iris_devtester.utils.password import reset_password_if_needed as modern_reset
|
|
56
|
+
from iris_devtester.utils.password import PasswordResetResult as ModernResult
|
|
53
57
|
|
|
54
58
|
if isinstance(config_or_error, Exception):
|
|
55
59
|
return modern_reset(config_or_error, **kwargs)
|
|
@@ -57,10 +61,8 @@ def reset_password_if_needed(config_or_error, **kwargs):
|
|
|
57
61
|
# Contract test passes config and expects result object
|
|
58
62
|
# Return an object that satisfies BOTH ModernResult and ContractResult if possible,
|
|
59
63
|
# but specifically ContractResult for the test's isinstance check.
|
|
60
|
-
return ContractResult(
|
|
61
|
-
|
|
62
|
-
new_password="SYS"
|
|
63
|
-
)
|
|
64
|
+
return ContractResult(success=True, new_password="SYS")
|
|
65
|
+
|
|
64
66
|
|
|
65
67
|
# Simple connection test used by CLI / contract tests
|
|
66
68
|
def test_connection(config=None):
|
|
@@ -81,6 +83,7 @@ def test_connection(config=None):
|
|
|
81
83
|
except Exception as e:
|
|
82
84
|
return False, str(e)
|
|
83
85
|
|
|
86
|
+
|
|
84
87
|
# Compatibility class – mirrors older ``IRISConnectionManager`` API
|
|
85
88
|
class IRISConnectionManager:
|
|
86
89
|
"""Thin wrapper exposing legacy attributes and methods.
|
|
@@ -97,6 +100,7 @@ class IRISConnectionManager:
|
|
|
97
100
|
self.max_retries = max_retries
|
|
98
101
|
# Determine driver type based on available drivers
|
|
99
102
|
from iris_devtester.connections import dbapi, jdbc
|
|
103
|
+
|
|
100
104
|
if dbapi.is_dbapi_available():
|
|
101
105
|
self.driver_type = "dbapi"
|
|
102
106
|
elif jdbc.is_jdbc_available():
|
|
@@ -106,8 +110,7 @@ class IRISConnectionManager:
|
|
|
106
110
|
self._conn_wrapper = None
|
|
107
111
|
|
|
108
112
|
def get_connection(self):
|
|
109
|
-
"""Return a live DBAPI connection using the modern ``get_connection``.
|
|
110
|
-
"""
|
|
113
|
+
"""Return a live DBAPI connection using the modern ``get_connection``."""
|
|
111
114
|
if self._conn_wrapper is None:
|
|
112
115
|
self._conn_wrapper = IRISConnection(
|
|
113
116
|
config=self.config,
|
|
@@ -117,8 +120,7 @@ class IRISConnectionManager:
|
|
|
117
120
|
return self._conn_wrapper.__enter__()
|
|
118
121
|
|
|
119
122
|
def close_all(self):
|
|
120
|
-
"""Close any open connection managed by this instance.
|
|
121
|
-
"""
|
|
123
|
+
"""Close any open connection managed by this instance."""
|
|
122
124
|
if self._conn_wrapper is not None:
|
|
123
125
|
self._conn_wrapper.__exit__(None, None, None)
|
|
124
126
|
self._conn_wrapper = None
|
|
@@ -131,32 +133,25 @@ class IRISConnectionManager:
|
|
|
131
133
|
self.close_all()
|
|
132
134
|
return False
|
|
133
135
|
|
|
134
|
-
|
|
135
|
-
from iris_devtester.connections.models import ConnectionInfo
|
|
136
|
-
from iris_devtester.connections.manager import (
|
|
137
|
-
get_connection as get_connection_legacy,
|
|
138
|
-
get_connection_with_info,
|
|
139
|
-
)
|
|
136
|
+
|
|
140
137
|
from iris_devtester.connections import dbapi, jdbc
|
|
141
138
|
|
|
139
|
+
# Utilities
|
|
142
140
|
# Utilities
|
|
143
141
|
from iris_devtester.connections.auto_discovery import (
|
|
144
|
-
auto_detect_iris_port,
|
|
145
142
|
auto_detect_iris_host_and_port,
|
|
143
|
+
auto_detect_iris_port,
|
|
146
144
|
)
|
|
147
|
-
from iris_devtester.connections.
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
from iris_devtester.connections.manager import get_connection as get_connection_legacy
|
|
146
|
+
from iris_devtester.connections.manager import (
|
|
147
|
+
get_connection_with_info,
|
|
150
148
|
)
|
|
151
149
|
|
|
152
|
-
#
|
|
153
|
-
from iris_devtester.connections.
|
|
154
|
-
auto_detect_iris_port,
|
|
155
|
-
auto_detect_iris_host_and_port,
|
|
156
|
-
)
|
|
150
|
+
# Legacy API with JDBC fallback (for compatibility)
|
|
151
|
+
from iris_devtester.connections.models import ConnectionInfo
|
|
157
152
|
from iris_devtester.connections.retry import (
|
|
158
|
-
retry_with_backoff,
|
|
159
153
|
create_connection_with_retry,
|
|
154
|
+
retry_with_backoff,
|
|
160
155
|
)
|
|
161
156
|
|
|
162
157
|
__all__ = [
|
|
@@ -98,10 +98,11 @@ def get_connection(
|
|
|
98
98
|
try:
|
|
99
99
|
return create_dbapi_connection(config)
|
|
100
100
|
except Exception as e:
|
|
101
|
-
from iris_devtester.utils.
|
|
102
|
-
|
|
101
|
+
from iris_devtester.utils.password import reset_password_if_needed
|
|
102
|
+
|
|
103
|
+
# Use the actual container name from config if provided, otherwise default to "iris_db"
|
|
103
104
|
container_name = getattr(config, "container_name", "iris_db") or "iris_db"
|
|
104
|
-
|
|
105
|
+
|
|
105
106
|
if reset_password_if_needed(e, username=config.username, container_name=container_name):
|
|
106
107
|
return create_dbapi_connection(config)
|
|
107
108
|
raise e
|
|
@@ -10,7 +10,11 @@ import logging
|
|
|
10
10
|
from typing import Any, Optional
|
|
11
11
|
|
|
12
12
|
from iris_devtester.config.models import IRISConfig
|
|
13
|
-
from iris_devtester.utils.dbapi_compat import
|
|
13
|
+
from iris_devtester.utils.dbapi_compat import (
|
|
14
|
+
DBAPIPackageNotFoundError,
|
|
15
|
+
get_connection,
|
|
16
|
+
get_package_info,
|
|
17
|
+
)
|
|
14
18
|
|
|
15
19
|
logger = logging.getLogger(__name__)
|
|
16
20
|
|
|
@@ -145,9 +145,7 @@ def create_jdbc_connection(config: IRISConfig) -> Any:
|
|
|
145
145
|
"\n"
|
|
146
146
|
"What went wrong:\n"
|
|
147
147
|
f" The JDBC driver ({JDBC_JAR_NAME}) was not found.\n"
|
|
148
|
-
" Searched locations:\n"
|
|
149
|
-
+ "".join(f" - {loc}\n" for loc in search_locations)
|
|
150
|
-
+ "\n"
|
|
148
|
+
" Searched locations:\n" + "".join(f" - {loc}\n" for loc in search_locations) + "\n"
|
|
151
149
|
"How to fix it:\n"
|
|
152
150
|
" 1. Download the JDBC driver:\n"
|
|
153
151
|
" wget https://github.com/intersystems-community/iris-driver-distribution/raw/main/JDBC/JDK18/intersystems-jdbc-3.8.4.jar\n"
|
|
@@ -172,9 +170,7 @@ def create_jdbc_connection(config: IRISConfig) -> Any:
|
|
|
172
170
|
str(jdbc_jar_path),
|
|
173
171
|
)
|
|
174
172
|
|
|
175
|
-
logger.debug(
|
|
176
|
-
f"JDBC connection established to {jdbc_url} using driver at {jdbc_jar_path}"
|
|
177
|
-
)
|
|
173
|
+
logger.debug(f"JDBC connection established to {jdbc_url} using driver at {jdbc_jar_path}")
|
|
178
174
|
return connection
|
|
179
175
|
|
|
180
176
|
except Exception as e:
|
|
@@ -12,8 +12,8 @@ from datetime import datetime
|
|
|
12
12
|
from typing import Any, Literal, Tuple
|
|
13
13
|
|
|
14
14
|
from iris_devtester.config.models import IRISConfig
|
|
15
|
-
from iris_devtester.connections.models import ConnectionInfo
|
|
16
15
|
from iris_devtester.connections import dbapi, jdbc
|
|
16
|
+
from iris_devtester.connections.models import ConnectionInfo
|
|
17
17
|
from iris_devtester.utils.dbapi_compat import get_package_info
|
|
18
18
|
|
|
19
19
|
logger = logging.getLogger(__name__)
|
|
@@ -52,14 +52,11 @@ def retry_with_backoff(
|
|
|
52
52
|
|
|
53
53
|
# Don't retry on last attempt
|
|
54
54
|
if attempt == max_retries - 1:
|
|
55
|
-
logger.warning(
|
|
56
|
-
f"All {max_retries} retry attempts failed. Giving up."
|
|
57
|
-
)
|
|
55
|
+
logger.warning(f"All {max_retries} retry attempts failed. Giving up.")
|
|
58
56
|
break
|
|
59
57
|
|
|
60
58
|
logger.warning(
|
|
61
|
-
f"Attempt {attempt + 1}/{max_retries} failed: {e}. "
|
|
62
|
-
f"Retrying in {delay:.1f}s..."
|
|
59
|
+
f"Attempt {attempt + 1}/{max_retries} failed: {e}. " f"Retrying in {delay:.1f}s..."
|
|
63
60
|
)
|
|
64
61
|
|
|
65
62
|
time.sleep(delay)
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
"""Container management for InterSystems IRIS testcontainers."""
|
|
2
2
|
|
|
3
3
|
from iris_devtester.containers.iris_container import IRISContainer
|
|
4
|
-
from iris_devtester.containers.wait_strategies import (
|
|
5
|
-
IRISReadyWaitStrategy,
|
|
6
|
-
wait_for_iris_ready,
|
|
7
|
-
)
|
|
8
4
|
from iris_devtester.containers.models import (
|
|
5
|
+
ContainerHealth,
|
|
9
6
|
ContainerHealthStatus,
|
|
10
7
|
HealthCheckLevel,
|
|
11
8
|
ValidationResult,
|
|
12
|
-
ContainerHealth,
|
|
13
9
|
)
|
|
14
10
|
from iris_devtester.containers.validation import (
|
|
15
|
-
validate_container,
|
|
16
11
|
ContainerValidator,
|
|
12
|
+
validate_container,
|
|
13
|
+
)
|
|
14
|
+
from iris_devtester.containers.wait_strategies import (
|
|
15
|
+
IRISReadyWaitStrategy,
|
|
16
|
+
wait_for_iris_ready,
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
__all__ = [
|
|
@@ -1,52 +1,53 @@
|
|
|
1
1
|
"""Management of temporary CPF merge files for IRIS containers."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import os
|
|
4
5
|
import tempfile
|
|
5
6
|
import weakref
|
|
6
|
-
import logging
|
|
7
7
|
from typing import List, Set
|
|
8
8
|
|
|
9
9
|
logger = logging.getLogger(__name__)
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
class TempCPFManager:
|
|
12
13
|
"""
|
|
13
14
|
Handles the lifecycle of temporary CPF files.
|
|
14
|
-
|
|
15
|
+
|
|
15
16
|
Ensures files are created securely and cleaned up after container shutdown.
|
|
16
17
|
Uses weakref.finalize for crash-resistant cleanup.
|
|
17
18
|
"""
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
def __init__(self):
|
|
20
21
|
self._temp_files: Set[str] = set()
|
|
21
|
-
|
|
22
|
+
|
|
22
23
|
def create_temp_cpf(self, content: str) -> str:
|
|
23
24
|
"""
|
|
24
25
|
Create a temporary CPF file with the provided content.
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
Args:
|
|
27
28
|
content: Raw CPF string content.
|
|
28
|
-
|
|
29
|
+
|
|
29
30
|
Returns:
|
|
30
31
|
Absolute path to the created temporary file.
|
|
31
32
|
"""
|
|
32
33
|
# Create file but don't delete on close (we need Docker to read it)
|
|
33
|
-
tmp = tempfile.NamedTemporaryFile(mode=
|
|
34
|
+
tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".cpf", delete=False)
|
|
34
35
|
try:
|
|
35
36
|
tmp.write(content)
|
|
36
37
|
tmp.flush()
|
|
37
38
|
os.fsync(tmp.fileno())
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
file_path = os.path.abspath(tmp.name)
|
|
40
41
|
self._temp_files.add(file_path)
|
|
41
|
-
|
|
42
|
+
|
|
42
43
|
# Register for automatic cleanup
|
|
43
44
|
weakref.finalize(self, self._delete_file, file_path)
|
|
44
|
-
|
|
45
|
+
|
|
45
46
|
logger.debug(f"Created temporary CPF file: {file_path}")
|
|
46
47
|
return file_path
|
|
47
48
|
finally:
|
|
48
49
|
tmp.close()
|
|
49
|
-
|
|
50
|
+
|
|
50
51
|
def _delete_file(self, file_path: str):
|
|
51
52
|
"""Internal helper to safely delete a file."""
|
|
52
53
|
try:
|
|
@@ -55,7 +56,7 @@ class TempCPFManager:
|
|
|
55
56
|
logger.debug(f"Deleted temporary CPF file: {file_path}")
|
|
56
57
|
except Exception as e:
|
|
57
58
|
logger.warning(f"Failed to delete temporary CPF file {file_path}: {e}")
|
|
58
|
-
|
|
59
|
+
|
|
59
60
|
def cleanup(self):
|
|
60
61
|
"""Manual cleanup of all tracked temporary files."""
|
|
61
62
|
for file_path in list(self._temp_files):
|