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
@@ -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 = [f"{container_port}->{host_port}"
278
- for container_port, host_port in sorted(self.ports.items())]
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
- DEFAULT_DRIVER,
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() -> dict:
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() -> dict:
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():
@@ -1,14 +1,7 @@
1
1
  class CPFPreset:
2
- ENABLE_CALLIN = (
3
- "[Actions]\n"
4
- "ModifyService:Name=%Service_CallIn,Enabled=1,AutheEnabled=48"
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"
@@ -1,8 +1,9 @@
1
1
  """YAML configuration file loader for IRIS container management."""
2
2
 
3
- import yaml
4
3
  from pathlib import Path
5
- from typing import Dict, Any
4
+ from typing import Any, Dict
5
+
6
+ import yaml
6
7
 
7
8
 
8
9
  def load_yaml(file_path: Path) -> Dict[str, Any]:
@@ -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 get_connection, IRISConnection
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
- if 'pytest' in sys.modules:
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('auto_remediate', True)
40
- max_retries = kwargs.get('retry_attempts', 3)
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.password_verification import PasswordResetResult as ModernResult
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
- success=True,
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
- # Legacy API with JDBC fallback (for compatibility)
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.retry import (
148
- retry_with_backoff,
149
- create_connection_with_retry,
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
- # Utilities
153
- from iris_devtester.connections.auto_discovery import (
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.password_reset import reset_password_if_needed
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 get_connection, get_package_info, DBAPIPackageNotFoundError
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='w', suffix='.cpf', delete=False)
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):