iris-devtester 1.9.2__py3-none-any.whl → 1.10.0__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.
@@ -26,7 +26,7 @@ LangChain Integration:
26
26
  ... # Build your RAG app...
27
27
  """
28
28
 
29
- __version__ = "1.9.2"
29
+ __version__ = "1.10.0"
30
30
  __author__ = "InterSystems Community"
31
31
  __license__ = "MIT"
32
32
 
@@ -31,6 +31,25 @@ def container_group(ctx):
31
31
  @click.option(
32
32
  "--config", type=click.Path(exists=True), help="Path to iris-config.yml configuration file"
33
33
  )
34
+ @click.option(
35
+ "--name",
36
+ type=str,
37
+ default=None,
38
+ help="Container name (default: iris_db)",
39
+ )
40
+ @click.option(
41
+ "--edition",
42
+ type=click.Choice(["community", "enterprise", "light"], case_sensitive=False),
43
+ default="community",
44
+ help="IRIS edition: community (default), enterprise (requires license), light (minimal for CI/CD)",
45
+ )
46
+ @click.option(
47
+ "--license",
48
+ "license_key",
49
+ type=click.Path(exists=True),
50
+ default=None,
51
+ help="Path to iris.key license file (required for enterprise edition)",
52
+ )
34
53
  @click.option(
35
54
  "--detach/--no-detach",
36
55
  default=True,
@@ -41,7 +60,7 @@ def container_group(ctx):
41
60
  )
42
61
  @click.option("--cpf", help="Path to CPF merge file or raw CPF content")
43
62
  @click.pass_context
44
- def up(ctx, config, detach, timeout, cpf):
63
+ def up(ctx, config, name, edition, license_key, detach, timeout, cpf):
45
64
  """
46
65
  Create and start IRIS container from configuration.
47
66
 
@@ -58,15 +77,18 @@ def up(ctx, config, detach, timeout, cpf):
58
77
  # Zero-config (uses Community edition defaults)
59
78
  iris-devtester container up
60
79
 
80
+ # Light edition for CI/CD (85% smaller, faster startup)
81
+ iris-devtester container up --edition light
82
+
83
+ # Enterprise edition with license
84
+ iris-devtester container up --edition enterprise --license /path/to/iris.key
85
+
86
+ # With custom container name
87
+ iris-devtester container up --name my-test-db
88
+
61
89
  # With custom configuration including volumes
62
90
  iris-devtester container up --config iris-config.yml
63
91
 
64
- # Example iris-config.yml with volumes:
65
- # edition: community
66
- # volumes:
67
- # - ./workspace:/external/workspace
68
- # - ./config:/opt/config:ro
69
-
70
92
  # Foreground mode (see logs)
71
93
  iris-devtester container up --no-detach
72
94
  """
@@ -85,6 +107,37 @@ def up(ctx, config, detach, timeout, cpf):
85
107
  container_config = ContainerConfig.default()
86
108
  click.echo("⚡ Creating container from zero-config defaults")
87
109
 
110
+ # Override container name if provided via --name
111
+ if name:
112
+ container_config.container_name = name
113
+ click.echo(f" → Container name: {name}")
114
+
115
+ # Override edition if provided via --edition
116
+ if edition:
117
+ edition_lower = edition.lower()
118
+ container_config.edition = edition_lower
119
+
120
+ # Set appropriate image based on edition
121
+ if edition_lower == "light":
122
+ # Light edition: caretdev/iris-community-light (85% smaller)
123
+ container_config.image_tag = "latest-em"
124
+ click.echo(
125
+ click.style(f" → Edition: light", fg="cyan")
126
+ + " (minimal for CI/CD, ~580MB vs ~3.5GB)"
127
+ )
128
+ elif edition_lower == "enterprise":
129
+ if not license_key:
130
+ raise click.ClickException(
131
+ "Enterprise edition requires --license option.\n"
132
+ "\n"
133
+ "Usage: iris-devtester container up --edition enterprise --license /path/to/iris.key"
134
+ )
135
+ container_config.license_key = license_key
136
+ click.echo(f" → Edition: enterprise")
137
+ click.echo(f" → License: {license_key}")
138
+ else:
139
+ click.echo(f" → Edition: community")
140
+
88
141
  if cpf:
89
142
  container_config.cpf_merge = cpf
90
143
  click.echo(f" → CPF Merge: {cpf[:50]}...")
@@ -95,6 +148,36 @@ def up(ctx, config, detach, timeout, cpf):
95
148
  if existing_container:
96
149
  # Container exists - check if running
97
150
  existing_container.reload()
151
+
152
+ # Warn if using default name and container already exists
153
+ # (user might be connecting to wrong container from different project)
154
+ if container_config.container_name == "iris_db" and not name:
155
+ existing_image = (
156
+ existing_container.image.tags[0] if existing_container.image.tags else "unknown"
157
+ )
158
+ click.echo("")
159
+ click.echo(
160
+ click.style(
161
+ "⚠️ WARNING: Using default container name 'iris_db'", fg="yellow", bold=True
162
+ )
163
+ )
164
+ click.echo(
165
+ click.style(
166
+ f" A container with this name already exists (image: {existing_image})",
167
+ fg="yellow",
168
+ )
169
+ )
170
+ click.echo(
171
+ click.style(
172
+ " If this is from a different project, use --name to avoid conflicts:",
173
+ fg="yellow",
174
+ )
175
+ )
176
+ click.echo(
177
+ click.style(" iris-devtester container up --name my-project-db", fg="cyan")
178
+ )
179
+ click.echo("")
180
+
98
181
  if existing_container.status == "running":
99
182
  click.echo(f"✓ Container '{container_config.container_name}' is already running")
100
183
 
@@ -228,6 +311,141 @@ def up(ctx, config, detach, timeout, cpf):
228
311
  ctx.exit(1)
229
312
 
230
313
 
314
+ @container_group.command(name="list")
315
+ @click.option(
316
+ "--all", "-a", "show_all", is_flag=True, help="Show all containers (including stopped)"
317
+ )
318
+ @click.option(
319
+ "--format",
320
+ "output_format",
321
+ type=click.Choice(["table", "json"], case_sensitive=False),
322
+ default="table",
323
+ help="Output format (default: table)",
324
+ )
325
+ @click.pass_context
326
+ def list_containers(ctx, show_all, output_format):
327
+ """
328
+ List IRIS containers.
329
+
330
+ Shows all IRIS containers managed by iris-devtester, with their status,
331
+ edition, ports, and age.
332
+
333
+ \b
334
+ Examples:
335
+ # List running containers
336
+ iris-devtester container list
337
+
338
+ # List all containers (including stopped)
339
+ iris-devtester container list --all
340
+
341
+ # JSON output for scripting
342
+ iris-devtester container list --format json
343
+ """
344
+ from datetime import datetime
345
+
346
+ import docker
347
+
348
+ try:
349
+ client = docker.from_env()
350
+
351
+ # Find IRIS containers (by image name patterns)
352
+ iris_patterns = [
353
+ "iris-community",
354
+ "intersystems/iris",
355
+ "caretdev/iris",
356
+ "intersystemsdc/iris",
357
+ ]
358
+
359
+ containers = client.containers.list(all=show_all)
360
+ iris_containers = []
361
+
362
+ for container in containers:
363
+ image_name = (
364
+ container.image.tags[0] if container.image.tags else str(container.image.id)[:12]
365
+ )
366
+
367
+ # Check if this is an IRIS container
368
+ is_iris = any(pattern in image_name.lower() for pattern in iris_patterns)
369
+ if not is_iris:
370
+ continue
371
+
372
+ # Determine edition from image name
373
+ if "light" in image_name.lower():
374
+ edition = "light"
375
+ elif "community" in image_name.lower():
376
+ edition = "community"
377
+ else:
378
+ edition = "enterprise"
379
+
380
+ # Get port mappings
381
+ ports = container.attrs.get("NetworkSettings", {}).get("Ports", {})
382
+ port_str = "-"
383
+ if ports and ports.get("1972/tcp"):
384
+ host_port = ports["1972/tcp"][0]["HostPort"]
385
+ port_str = f"{host_port}->1972"
386
+
387
+ # Calculate age
388
+ created = container.attrs.get("Created", "")
389
+ age_str = "unknown"
390
+ if created:
391
+ try:
392
+ # Parse ISO format timestamp
393
+ created_dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
394
+ now = datetime.now(created_dt.tzinfo)
395
+ delta = now - created_dt
396
+ if delta.days > 0:
397
+ age_str = f"{delta.days}d"
398
+ elif delta.seconds > 3600:
399
+ age_str = f"{delta.seconds // 3600}h"
400
+ else:
401
+ age_str = f"{delta.seconds // 60}m"
402
+ except Exception:
403
+ pass
404
+
405
+ iris_containers.append(
406
+ {
407
+ "name": container.name,
408
+ "edition": edition,
409
+ "status": container.status,
410
+ "ports": port_str,
411
+ "age": age_str,
412
+ "image": image_name,
413
+ }
414
+ )
415
+
416
+ if output_format == "json":
417
+ import json as json_module
418
+
419
+ click.echo(json_module.dumps(iris_containers, indent=2))
420
+ else:
421
+ # Table format
422
+ if not iris_containers:
423
+ click.echo("No IRIS containers found.")
424
+ if not show_all:
425
+ click.echo("Use --all to include stopped containers.")
426
+ else:
427
+ # Print header
428
+ click.echo(f"{'NAME':<20} {'EDITION':<12} {'STATUS':<10} {'PORTS':<15} {'AGE':<6}")
429
+ click.echo("-" * 65)
430
+
431
+ for c in iris_containers:
432
+ status_color = "green" if c["status"] == "running" else "yellow"
433
+ click.echo(
434
+ f"{c['name']:<20} "
435
+ f"{c['edition']:<12} "
436
+ f"{click.style(c['status'], fg=status_color):<19} "
437
+ f"{c['ports']:<15} "
438
+ f"{c['age']:<6}"
439
+ )
440
+
441
+ except docker.errors.DockerException as e:
442
+ progress.print_error(f"Docker error: {e}")
443
+ ctx.exit(1)
444
+ except Exception as e:
445
+ progress.print_error(f"Error listing containers: {e}")
446
+ ctx.exit(1)
447
+
448
+
231
449
  @container_group.command(name="start")
232
450
  @click.argument("container_name", required=False, default="iris_db")
233
451
  @click.option(
@@ -613,7 +831,7 @@ def remove(ctx, container_name, force, volumes):
613
831
 
614
832
 
615
833
  @container_group.command(name="reset-password")
616
- @click.argument("container_name")
834
+ @click.argument("container_name", required=False, default="iris_db")
617
835
  @click.option("--user", default="_SYSTEM", help="Username to reset password for (default: _SYSTEM)")
618
836
  @click.option("--password", default="SYS", help="New password (default: SYS)")
619
837
  @click.option(
@@ -679,7 +897,7 @@ def reset_password_cmd(ctx, container_name, user, password, port):
679
897
 
680
898
 
681
899
  @container_group.command(name="test-connection")
682
- @click.argument("container_name")
900
+ @click.argument("container_name", required=False, default="iris_db")
683
901
  @click.option(
684
902
  "--namespace", default="USER", help="IRIS namespace to test connection to (default: USER)"
685
903
  )
@@ -768,7 +986,7 @@ def test_connection_cmd(ctx, container_name, namespace, username, password):
768
986
 
769
987
 
770
988
  @container_group.command(name="enable-callin")
771
- @click.argument("container_name")
989
+ @click.argument("container_name", required=False, default="iris_db")
772
990
  @click.option(
773
991
  "--timeout", type=int, default=30, help="Timeout in seconds for docker commands (default: 30)"
774
992
  )
@@ -808,9 +1026,6 @@ def enable_callin(ctx, container_name, timeout):
808
1026
  except (ImportError, ModuleNotFoundError) as e:
809
1027
  progress.print_error(f"enable_callin utility not available: {e}")
810
1028
  ctx.exit(1)
811
- except (click.exceptions.Exit, SystemExit, KeyboardInterrupt):
812
- # Let Click handle these - don't catch them
813
- raise
814
1029
  except Exception as e:
815
1030
  progress.print_error(f"Failed to enable CallIn: {e}")
816
1031
  ctx.exit(1)
@@ -3,9 +3,9 @@
3
3
  import os
4
4
  import re
5
5
  from pathlib import Path
6
- from typing import List, Literal, Optional
6
+ from typing import List, Literal, Optional, Union
7
7
 
8
- from pydantic import BaseModel, Field, field_validator, model_validator
8
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
9
9
 
10
10
  from iris_devtester.config.yaml_loader import load_yaml
11
11
 
@@ -51,7 +51,7 @@ class ContainerConfig(BaseModel):
51
51
  ... )
52
52
  """
53
53
 
54
- edition: Literal["community", "enterprise"] = Field(
54
+ edition: Literal["community", "enterprise", "light"] = Field(
55
55
  default="community", description="IRIS edition to use"
56
56
  )
57
57
  container_name: str = Field(
@@ -122,7 +122,7 @@ class ContainerConfig(BaseModel):
122
122
  return self
123
123
 
124
124
  @classmethod
125
- def from_yaml(cls, file_path: str | Path) -> "ContainerConfig":
125
+ def from_yaml(cls, file_path: Union[str, Path]) -> "ContainerConfig":
126
126
  """
127
127
  Load configuration from YAML file.
128
128
 
@@ -245,15 +245,21 @@ class ContainerConfig(BaseModel):
245
245
  return cls()
246
246
 
247
247
  def get_image_name(self) -> str:
248
+ """Get the Docker image name based on edition and configuration."""
248
249
  if self.image:
249
250
  return self.image
250
251
 
251
252
  if self.edition == "community":
252
-
253
- # Bug Fix #1: Community images use 'intersystemsdc/' prefix on Docker Hub
253
+ # Community images use 'intersystemsdc/' prefix on Docker Hub
254
254
  return f"intersystemsdc/iris-community:{self.image_tag}"
255
+ elif self.edition == "light":
256
+ # Light edition: caretdev/iris-community-light (85% smaller)
257
+ # Use latest-em for LTS stability
258
+ tag = self.image_tag if self.image_tag != "latest" else "latest-em"
259
+ return f"caretdev/iris-community-light:{tag}"
255
260
  else:
256
- return f"intersystems/iris:{self.image_tag}"
261
+ # Enterprise edition
262
+ return f"containers.intersystems.com/intersystems/iris:{self.image_tag}"
257
263
 
258
264
  def validate_volume_paths(self) -> List[str]:
259
265
  """
@@ -290,10 +296,8 @@ class ContainerConfig(BaseModel):
290
296
 
291
297
  return errors
292
298
 
293
- class Config:
294
- """Pydantic model configuration."""
295
-
296
- json_schema_extra = {
299
+ model_config = ConfigDict(
300
+ json_schema_extra={
297
301
  "example": {
298
302
  "edition": "community",
299
303
  "container_name": "iris_db",
@@ -306,3 +310,4 @@ class ContainerConfig(BaseModel):
306
310
  "image_tag": "latest",
307
311
  }
308
312
  }
313
+ )
@@ -3,9 +3,9 @@
3
3
  from datetime import datetime
4
4
  from enum import Enum
5
5
  from pathlib import Path
6
- from typing import Dict, Literal, Optional
6
+ from typing import Dict, Optional
7
7
 
8
- from pydantic import BaseModel, Field, field_validator
8
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
9
9
 
10
10
 
11
11
  class ContainerStatus(str, Enum):
@@ -304,10 +304,8 @@ class ContainerState(BaseModel):
304
304
  "config_source": str(self.config_source) if self.config_source else None,
305
305
  }
306
306
 
307
- class Config:
308
- """Pydantic model configuration."""
309
-
310
- json_schema_extra = {
307
+ model_config = ConfigDict(
308
+ json_schema_extra={
311
309
  "example": {
312
310
  "container_id": "a1b2c3d4e5f6" + "0" * 52, # 64 chars
313
311
  "container_name": "iris_db",
@@ -316,8 +314,9 @@ class ContainerState(BaseModel):
316
314
  "created_at": "2025-01-10T14:30:00Z",
317
315
  "started_at": "2025-01-10T14:30:15Z",
318
316
  "finished_at": None,
319
- "ports": {1972: 1972, 52773: 52773},
317
+ "ports": {"1972": 1972, "52773": 52773},
320
318
  "image": "intersystems/iris-community:latest",
321
319
  "config_source": None,
322
320
  }
323
321
  }
322
+ )
@@ -28,7 +28,7 @@ def load_yaml(file_path: Path) -> Dict[str, Any]:
28
28
  config = yaml.safe_load(f)
29
29
  if config is None:
30
30
  return {}
31
- return config
31
+ return dict(config)
32
32
  except yaml.YAMLError as e:
33
33
  raise yaml.YAMLError(f"Invalid YAML syntax in {file_path}: {e}")
34
34
 
@@ -52,8 +52,8 @@ def reset_password_if_needed(config_or_error, **kwargs):
52
52
  If first arg is a config, attempts remediation and returns result object.
53
53
  """
54
54
  from iris_devtester.testing.models import PasswordResetResult as ContractResult
55
- from iris_devtester.utils.password import reset_password_if_needed as modern_reset
56
55
  from iris_devtester.utils.password import PasswordResetResult as ModernResult
56
+ from iris_devtester.utils.password import reset_password_if_needed as modern_reset
57
57
 
58
58
  if isinstance(config_or_error, Exception):
59
59
  return modern_reset(config_or_error, **kwargs)
@@ -1,10 +1,9 @@
1
1
  import logging
2
- import os
3
2
  import subprocess
4
3
  import time
5
- from typing import Any, Dict, List, Literal, Optional, Tuple, Type, Union
4
+ from typing import Any, Optional
6
5
 
7
- from iris_devtester.config import IRISConfig, discover_config
6
+ from iris_devtester.config import IRISConfig
8
7
  from iris_devtester.connections import get_connection
9
8
 
10
9
  logger = logging.getLogger(__name__)
@@ -95,25 +94,107 @@ class IRISContainer(IRISBase):
95
94
  self._preconfigure_username: Optional[str] = None
96
95
 
97
96
  @classmethod
98
- def community(cls, image: Optional[str] = None, **kwargs) -> "IRISContainer":
99
- """Create a Community Edition container."""
97
+ def community(
98
+ cls, image: Optional[str] = None, version: str = "latest", **kwargs
99
+ ) -> "IRISContainer":
100
+ """
101
+ Create a Community Edition container.
102
+
103
+ Auto-detects architecture (ARM64 vs x86) and pulls the appropriate image.
104
+
105
+ Args:
106
+ image: Docker image to use. If None, auto-detects based on architecture.
107
+ version: Image version tag. Options: 'latest', '2025.1', '2025.2', etc.
108
+ """
100
109
  if image is None:
101
110
  import platform as platform_module
102
111
 
103
112
  if platform_module.machine() == "arm64":
104
- image = "containers.intersystems.com/intersystems/iris-community:2025.1"
113
+ # ARM64 (Apple Silicon) - use official InterSystems registry
114
+ tag = version if version != "latest" else "2025.1"
115
+ image = f"containers.intersystems.com/intersystems/iris-community:{tag}"
105
116
  else:
106
- image = "intersystemsdc/iris-community:latest"
117
+ # x86_64 - use Docker Hub community image
118
+ image = f"intersystemsdc/iris-community:{version}"
107
119
  return cls(image=image, **kwargs)
108
120
 
109
121
  @classmethod
110
- def enterprise(cls, license_key: str, image: Optional[str] = None, **kwargs) -> "IRISContainer":
111
- """Create an Enterprise Edition container."""
122
+ def enterprise(
123
+ cls, license_key: Optional[str] = None, image: Optional[str] = None, **kwargs
124
+ ) -> "IRISContainer":
125
+ """
126
+ Create an Enterprise Edition container.
127
+
128
+ Args:
129
+ license_key: Path to iris.key file. If None, checks IRIS_LICENSE_KEY env var.
130
+ image: Docker image to use. Defaults to containers.intersystems.com/intersystems/iris:latest
131
+
132
+ Raises:
133
+ ValueError: If no license key is provided or found in environment.
134
+ """
135
+ import os
136
+
137
+ if license_key is None:
138
+ license_key = os.environ.get("IRIS_LICENSE_KEY")
139
+
140
+ if license_key is None:
141
+ raise ValueError(
142
+ "Enterprise edition requires a license key.\n"
143
+ "\n"
144
+ "Provide license_key parameter or set IRIS_LICENSE_KEY environment variable:\n"
145
+ " IRISContainer.enterprise(license_key='/path/to/iris.key')\n"
146
+ " # or\n"
147
+ " export IRIS_LICENSE_KEY=/path/to/iris.key"
148
+ )
149
+
150
+ if not os.path.exists(license_key):
151
+ raise ValueError(
152
+ f"License key file not found: {license_key}\n"
153
+ "\n"
154
+ "Verify the license key path exists and is readable."
155
+ )
156
+
112
157
  if image is None:
113
158
  image = "containers.intersystems.com/intersystems/iris:latest"
159
+
114
160
  container = cls(image=image, **kwargs)
161
+ # Mount license key into container
162
+ container._license_key_path = license_key
115
163
  return container
116
164
 
165
+ @classmethod
166
+ def light(
167
+ cls, image: Optional[str] = None, version: str = "latest", **kwargs
168
+ ) -> "IRISContainer":
169
+ """
170
+ Create a Light Edition container optimized for CI/CD.
171
+
172
+ Light edition is ~85% smaller than full Community edition (~580MB vs ~3.5GB).
173
+ It removes Interoperability, Management Portal, DeepSee, and web components.
174
+ DBAPI, JDBC, and ODBC connectivity are fully supported.
175
+
176
+ Args:
177
+ image: Docker image to use. Defaults to caretdev/iris-community-light.
178
+ version: Image version tag. Options: 'latest', 'latest-em' (LTS), '2025.1', etc.
179
+
180
+ Best for:
181
+ - CI/CD pipelines
182
+ - Microservices
183
+ - Automated testing
184
+ - SQL-only workloads
185
+
186
+ Not supported:
187
+ - Interoperability/Ensemble
188
+ - Management Portal
189
+ - DeepSee/BI
190
+ - CSP/REST web framework
191
+ """
192
+ if image is None:
193
+ # Use latest-em for LTS stability, or allow version override
194
+ tag = version if version != "latest" else "latest-em"
195
+ image = f"caretdev/iris-community-light:{tag}"
196
+ return cls(image=image, **kwargs)
197
+
117
198
  def with_name(self, name: str) -> "IRISContainer":
118
199
  """Set the container name."""
119
200
  self._container_name = name
@@ -185,7 +266,7 @@ class IRISContainer(IRISBase):
185
266
  if is_enabled:
186
267
  self._callin_enabled = True
187
268
  return is_enabled
188
- except:
269
+ except Exception:
189
270
  return False
190
271
 
191
272
  def get_test_namespace(self, prefix: str = "TEST") -> str:
@@ -238,9 +319,7 @@ class IRISContainer(IRISBase):
238
319
  self.host = self.get_container_host_ip()
239
320
  self._mapped_port = int(self.get_exposed_port(1972)) # Use internal port to get mapping
240
321
  config.host = self.host
241
- config.port = (
242
- self._mapped_port
243
- ) # Config uses the host-mapped port for connections
322
+ config.port = self._mapped_port # Config uses the host-mapped port for connections
244
323
  except Exception:
245
324
  pass
246
325
  return config
@@ -7,7 +7,7 @@ Provides atomic file-based persistence with file locking for concurrent safety.
7
7
  import json
8
8
  from datetime import datetime
9
9
  from pathlib import Path
10
- from typing import List, Optional
10
+ from typing import List, Literal, Optional
11
11
 
12
12
  from filelock import FileLock, Timeout
13
13
 
@@ -89,7 +89,7 @@ class PortRegistry:
89
89
  # Manual port assignment
90
90
  self._validate_port_available(assignments, preferred_port, project_path)
91
91
  port = preferred_port
92
- assignment_type = "manual"
92
+ assignment_type: Literal["auto", "manual"] = "manual"
93
93
  else:
94
94
  # Auto-assignment
95
95
  port = self._find_available_port(assignments)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris-devtester
3
- Version: 1.9.2
3
+ Version: 1.10.0
4
4
  Summary: Battle-tested InterSystems IRIS infrastructure utilities for Python development
5
5
  Author-email: InterSystems Community <community@intersystems.com>
6
6
  Maintainer-email: Thomas Dyar <thomas.dyar@intersystems.com>
@@ -107,6 +107,89 @@ def test_connection():
107
107
  assert cursor.fetchone()[0] == 1
108
108
  ```
109
109
 
110
+ ## Container Editions
111
+
112
+ Three canonical container editions are available:
113
+
114
+ | Edition | Size | Use Case | Image |
115
+ |---------|------|----------|-------|
116
+ | **Community** | ~972MB | Development, testing | `intersystemsdc/iris-community` |
117
+ | **Enterprise** | ~1GB+ | Production testing | `containers.intersystems.com/intersystems/iris` |
118
+ | **Light** | **~580MB** | CI/CD pipelines | `caretdev/iris-community-light` |
119
+
120
+ ### Python API
121
+ ```python
122
+ from iris_devtester.containers import IRISContainer
123
+
124
+ # Community Edition (auto-detects ARM64 vs x86)
125
+ with IRISContainer.community() as iris:
126
+ conn = iris.get_connection()
127
+
128
+ # Light Edition (85% smaller, for CI/CD)
129
+ with IRISContainer.light() as iris:
130
+ conn = iris.get_connection()
131
+
132
+ # Enterprise Edition (requires license)
133
+ with IRISContainer.enterprise(license_key="/path/to/iris.key") as iris:
134
+ conn = iris.get_connection()
135
+
136
+ # Specify version
137
+ with IRISContainer.community(version="2025.1") as iris:
138
+ conn = iris.get_connection()
139
+ ```
140
+
141
+ ### CLI Usage
142
+ ```bash
143
+ # Community (default)
144
+ iris-devtester container up
145
+
146
+ # Light edition for CI/CD
147
+ iris-devtester container up --edition light
148
+
149
+ # Enterprise edition with license
150
+ iris-devtester container up --edition enterprise --license /path/to/iris.key
151
+
152
+ # List running IRIS containers
153
+ iris-devtester container list
154
+ ```
155
+
156
+ ### Light Edition Details
157
+
158
+ The Light edition removes components unnecessary for SQL-only workloads:
159
+ - **Removed**: Interoperability/Ensemble, Management Portal, DeepSee/BI, CSP/REST
160
+ - **Kept**: SQL engine, DBAPI, JDBC, ODBC, SQLAlchemy-IRIS support
161
+
162
+ Perfect for microservices, automated testing, and Python/SQL pipelines.
163
+
164
+ ### Builder Methods
165
+ ```python
166
+ # Set a custom container name (for debugging, logs, multiple containers)
167
+ iris = IRISContainer.community().with_name("my-test-db")
168
+
169
+ # Set credentials
170
+ iris = IRISContainer.community().with_credentials("_SYSTEM", "MyPassword")
171
+
172
+ # Pre-configure password (set via IRIS_PASSWORD env var at startup)
173
+ iris = IRISContainer.community().with_preconfigured_password("MyPassword")
174
+
175
+ # Chain multiple options
176
+ with IRISContainer.community() \
177
+ .with_name("integration-test-db") \
178
+ .with_credentials("_SYSTEM", "TestPass123") as iris:
179
+ conn = iris.get_connection()
180
+ ```
181
+
182
+ ### Constructor Parameters
183
+ ```python
184
+ IRISContainer(
185
+ image="intersystemsdc/iris-community:latest", # Docker image
186
+ username="SuperUser", # Default username
187
+ password="SYS", # Default password
188
+ namespace="USER", # Default namespace
189
+ name="my-container", # Container name (alternative to with_name)
190
+ )
191
+ ```
192
+
110
193
  ## Key Features
111
194
 
112
195
  - **🔐 Automatic Password Management**: Remediates security flags using official system APIs.
@@ -1,20 +1,20 @@
1
- iris_devtester/__init__.py,sha256=psFgHhr57q7wwSsFeofHyfp0p-z_J-Hquq7yiPgID2U,1848
1
+ iris_devtester/__init__.py,sha256=9a7_AhJCQogvs4DlktrA6mAPrgXskkpfNN2ZunkT2GY,1849
2
2
  iris_devtester/cli/__init__.py,sha256=yakDJH-EsnTBmKl_K7r2n47EDiWvfpwLVcY11KriQQQ,691
3
3
  iris_devtester/cli/__main__.py,sha256=YgFAn0Q02AXgXRwAg70MR3U0IIjFHUSz38h5tPFJnGg,125
4
4
  iris_devtester/cli/connection_commands.py,sha256=cabAufSsrrhwpxlPZFlsE-5Zi54176eGdJOApPLdZXA,11467
5
- iris_devtester/cli/container.py,sha256=TROSWehO02J8FNdUaoMooasCZUhox_jzi05z5RasS-8,29211
5
+ iris_devtester/cli/container.py,sha256=VvxDghoCuMVtqdw9XdTOD8LNYP-MmA0IQR04wgGytKg,37040
6
6
  iris_devtester/cli/container_commands.py,sha256=lOxa2TXnBK1ATCIzbbXexk-p0lItStUwU_gKVnv_p1w,4451
7
7
  iris_devtester/cli/fixture_commands.py,sha256=w1n6MIujQsY46ZKG_L0aRF4lhohWh5dOrF0lgH_K3r0,17493
8
8
  iris_devtester/config/__init__.py,sha256=acRVXpdKAp7lkZzrpKGnOYLgSfkxN3kpL1RU0b0hhlM,486
9
9
  iris_devtester/config/auto_discovery.py,sha256=y1BD26rfEut1vjw77IXh0yel95BaUvMfMxzskIPPQec,10638
10
- iris_devtester/config/container_config.py,sha256=oO44vKUImSXbSj9X1ka4g2gRJLyRqLv6mv7HVYzCU8w,10928
11
- iris_devtester/config/container_state.py,sha256=qIgSNQlpM6z417drSICbPfCWEvtb-L3YoB6JvkPl4T4,11239
10
+ iris_devtester/config/container_config.py,sha256=RsxfMIi9SF5tnmIAl_srASZrZrFSaV8BCxlFfMusYcQ,11353
11
+ iris_devtester/config/container_state.py,sha256=O2i69oVj_B1NQRZqmPKPdjmYwonhPD0QLe1UZmGupI4,11218
12
12
  iris_devtester/config/defaults.py,sha256=RgQal0IeDr215KIy6ZToDrsOdV4DwVJvMJE2SgAzVA4,1846
13
13
  iris_devtester/config/discovery.py,sha256=7R64ykibns6N2W9hNZlg_8CQqpNwiWSITl6G72VnNws,5879
14
14
  iris_devtester/config/models.py,sha256=aa2kd8t_UNawiA2BbU8GHlhVhVjqvEwLEmE75tewOo4,2706
15
15
  iris_devtester/config/presets.py,sha256=yXRiSfOUtUUUDQTW1wx-BHrUDzXhjYTCF8iiJ4kwyrU,453
16
- iris_devtester/config/yaml_loader.py,sha256=WF48AYDiuAMrI2XJpNkqfURvr3figvNcCrNInmvw0lI,1314
17
- iris_devtester/connections/__init__.py,sha256=sc2YFupRKBLUdiK3NFj3yhAsn5UfOABOvWRLvIJ2Xms,6018
16
+ iris_devtester/config/yaml_loader.py,sha256=3OahoOeLtRtBAxo3pH0-a1RUdXwFB5wc2_jtF2WielQ,1320
17
+ iris_devtester/connections/__init__.py,sha256=0ci2QuyQPJxdtoJzeyd8CSw_0I4C9MB6UnsVzztdh5U,6018
18
18
  iris_devtester/connections/auto_discovery.py,sha256=LCLdP90w7hfJrNj2oe6ueiSdmMDR5IdjztJ1wn9pvQc,5197
19
19
  iris_devtester/connections/connection.py,sha256=4QPV_D4kNCw61qg7XT9rgJOYxu5ZmOcw2km8LiuQ2Nc,5536
20
20
  iris_devtester/connections/dbapi.py,sha256=NmXwrgvfnN6K3051XenNFaqcbAipBNBvh7y3O2C4XUM,4767
@@ -24,7 +24,7 @@ iris_devtester/connections/models.py,sha256=rfKCFtm86wQZAv_cLOVS6N7P5vGlfkInSHbs
24
24
  iris_devtester/connections/retry.py,sha256=FxNhlTA2RHI2ayvEF5w7P_EjO7k6xKgeA3mplJd3-co,2893
25
25
  iris_devtester/containers/__init__.py,sha256=quWN4CtxRUMje8QkpZWMY79XlVOSM79HF5ewHLNz5YA,729
26
26
  iris_devtester/containers/cpf_manager.py,sha256=GBkuM5mPw85lo7djINlTNkrQ8bGgRe2QEWv7YQQL6KU,1969
27
- iris_devtester/containers/iris_container.py,sha256=2SEdxJ2-Sx0QqbZGCLnUvzjWtPfRh84uk-fvtsrQjaA,11173
27
+ iris_devtester/containers/iris_container.py,sha256=NgZs86FjaPtEsVc1P471jK1VrI8FNX7_PZIcdGcxr78,14058
28
28
  iris_devtester/containers/models.py,sha256=f66fwQjlmw1Sl4M33QlglCv4ewNtk4TJLXXcmhNsbDA,13693
29
29
  iris_devtester/containers/monitor_utils.py,sha256=cCsLp1cZYAbMmj7o6tUYhVurMEE7GOOe9Y5FLAxl6hM,4917
30
30
  iris_devtester/containers/monitoring.py,sha256=E4v-5VW-_W7CVSRm_ryzpOfLQgP9NZ6WN7GYglsAFro,41172
@@ -42,7 +42,7 @@ iris_devtester/integrations/langchain.py,sha256=cIV8bq_wDKMOXLFQ1P-wdSFsJdxIV7o0
42
42
  iris_devtester/ports/__init__.py,sha256=uLdbV7J7UJxJsoDqoyySTabIle72WltYDFKlN2-f7bI,506
43
43
  iris_devtester/ports/assignment.py,sha256=7ayWfeqLfHDICLAmr3LaJAGs8Uz-Ehm3dHX-V44Frj0,1954
44
44
  iris_devtester/ports/exceptions.py,sha256=mJLJAeZUmiBAfv0ubr6nh0K1iK-uk4bAX1gupyw5lzg,4967
45
- iris_devtester/ports/registry.py,sha256=f-UT2NyN2dNan9-5DfIlL-xRpBE_6w8KQKMCeHPXQeU,12880
45
+ iris_devtester/ports/registry.py,sha256=vf-vlPKNJCPGCCijz_ioioO5qjjMII2WndUlfDkgyxc,12916
46
46
  iris_devtester/testing/__init__.py,sha256=VD02sIjDQ1HUFIRFrd1vQUwoExE2yRkeFJ1Goz4cCDs,1559
47
47
  iris_devtester/testing/fixtures.py,sha256=Rh7NFlhKrGWy0YOHP1Bwyj1-E_99OJz5-TcG-3BhI6w,1139
48
48
  iris_devtester/testing/helpers.py,sha256=fLJbs7A5Ay_TQ7sMimdtOpKONN4AdnFEzEhOsR28iBs,5021
@@ -58,9 +58,9 @@ iris_devtester/utils/iris_container_adapter.py,sha256=0Vc55gDsVAxHZzk4wvZySGUJKR
58
58
  iris_devtester/utils/password.py,sha256=OqS6LNXMDopSoYGiodM2V9clghD4qcbIzftH4TdrMSU,21968
59
59
  iris_devtester/utils/progress.py,sha256=5OmuntItqCgUFXSmaQjHwILrTnVLYECZ5glpSbvOTOU,7795
60
60
  iris_devtester/utils/test_connection.py,sha256=cyEzxSbvxBxz44xLb7Jbz8OhHT8UgCZLO0lT-qShPEo,6950
61
- iris_devtester-1.9.2.dist-info/licenses/LICENSE,sha256=dISbikDYS2uP710ZFzSzaSmKzIBRyi_6YwuwO97bT94,1083
62
- iris_devtester-1.9.2.dist-info/METADATA,sha256=jgKwbs0Hg9T72Ob1WkO8zBYTGxhpDuu7iN_8wza7WRQ,6286
63
- iris_devtester-1.9.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
64
- iris_devtester-1.9.2.dist-info/entry_points.txt,sha256=svOltni3vX3ul_qxbWl8fOA6ZrJeFASuN29pON6ftqw,59
65
- iris_devtester-1.9.2.dist-info/top_level.txt,sha256=DnrLJ3laB5x_gTkmATDEg1v1lTOQxgmHgNd66bdXaoU,15
66
- iris_devtester-1.9.2.dist-info/RECORD,,
61
+ iris_devtester-1.10.0.dist-info/licenses/LICENSE,sha256=dISbikDYS2uP710ZFzSzaSmKzIBRyi_6YwuwO97bT94,1083
62
+ iris_devtester-1.10.0.dist-info/METADATA,sha256=na0Agz_JlzoL95dJWzkAcV8HIH8caNSI7H4tVrTwA8w,8940
63
+ iris_devtester-1.10.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
64
+ iris_devtester-1.10.0.dist-info/entry_points.txt,sha256=svOltni3vX3ul_qxbWl8fOA6ZrJeFASuN29pON6ftqw,59
65
+ iris_devtester-1.10.0.dist-info/top_level.txt,sha256=DnrLJ3laB5x_gTkmATDEg1v1lTOQxgmHgNd66bdXaoU,15
66
+ iris_devtester-1.10.0.dist-info/RECORD,,