iris-devtester 1.9.3__py3-none-any.whl → 1.10.2__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.3"
29
+ __version__ = "1.10.1"
30
30
  __author__ = "InterSystems Community"
31
31
  __license__ = "MIT"
32
32
 
@@ -13,9 +13,38 @@ from .fixture_commands import fixture
13
13
  @click.version_option(version=__version__, prog_name="iris-devtester")
14
14
  def main():
15
15
  """
16
- iris-devtester - Battle-tested InterSystems IRIS infrastructure utilities.
17
-
18
- Provides tools for container management, fixture handling, and testing.
16
+ iris-devtester - Python testing toolkit for InterSystems IRIS databases.
17
+
18
+ \b
19
+ WHAT THIS TOOL DOES:
20
+ Manages IRIS database containers, fixtures, and connections for testing.
21
+ Designed for CI/CD pipelines, local development, and AI agent automation.
22
+
23
+ \b
24
+ CONTAINER EDITIONS:
25
+ community Full IRIS Community (~3.5GB) - default, all features
26
+ light Minimal for CI/CD (~580MB) - SQL/DBAPI only, 6x smaller
27
+ enterprise Licensed IRIS - requires iris.key file
28
+
29
+ \b
30
+ QUICK START:
31
+ iris-devtester container up # Start community
32
+ iris-devtester container up --edition light # Start light (CI/CD)
33
+ iris-devtester container list # List containers
34
+ iris-devtester test-connection # Verify connectivity
35
+ iris-devtester container status # Check health
36
+
37
+ \b
38
+ COMMON WORKFLOWS:
39
+ Local dev: container up → test-connection → (your tests)
40
+ CI/CD: container up --edition light → test-connection → pytest
41
+ Fixtures: fixture load --fixture ./data → (verify data)
42
+
43
+ \b
44
+ FOR AI AGENTS:
45
+ All commands support --help for detailed options. Commands return
46
+ structured exit codes (0=success, 1=error, 2=not found, 5=timeout).
47
+ Use 'container list --format json' for machine-readable output.
19
48
  """
20
49
  pass
21
50
 
@@ -21,8 +21,23 @@ def container_group(ctx):
21
21
  """
22
22
  Container lifecycle management commands.
23
23
 
24
- Manage IRIS containers from the command line with zero-config support.
25
- Supports both Community and Enterprise editions.
24
+ \b
25
+ Manage IRIS database containers for testing and development.
26
+ Supports Community, Enterprise, and Light editions.
27
+
28
+ \b
29
+ EDITIONS:
30
+ community Full IRIS Community Edition (~3.5GB, all features)
31
+ light Minimal CI/CD image (~580MB, SQL/DBAPI only)
32
+ enterprise Licensed IRIS (requires --license iris.key)
33
+
34
+ \b
35
+ QUICK START:
36
+ up Create and start a container
37
+ list Show running containers
38
+ status Check container health
39
+ test-connection Verify database connectivity
40
+ stop/remove Clean up containers
26
41
  """
27
42
  pass
28
43
 
@@ -37,6 +52,25 @@ def container_group(ctx):
37
52
  default=None,
38
53
  help="Container name (default: iris_db)",
39
54
  )
55
+ @click.option(
56
+ "--edition",
57
+ type=click.Choice(["community", "enterprise", "light"], case_sensitive=False),
58
+ default="community",
59
+ help="IRIS edition: community (default), enterprise (requires license), light (minimal for CI/CD)",
60
+ )
61
+ @click.option(
62
+ "--image",
63
+ type=str,
64
+ default=None,
65
+ help="Custom Docker image (overrides --edition). Example: myregistry/iris:2024.1",
66
+ )
67
+ @click.option(
68
+ "--license",
69
+ "license_key",
70
+ type=click.Path(exists=True),
71
+ default=None,
72
+ help="Path to iris.key license file (required for enterprise edition)",
73
+ )
40
74
  @click.option(
41
75
  "--detach/--no-detach",
42
76
  default=True,
@@ -47,37 +81,31 @@ def container_group(ctx):
47
81
  )
48
82
  @click.option("--cpf", help="Path to CPF merge file or raw CPF content")
49
83
  @click.pass_context
50
- def up(ctx, config, name, detach, timeout, cpf):
84
+ def up(ctx, config, name, edition, image, license_key, detach, timeout, cpf):
51
85
  """
52
86
  Create and start IRIS container from configuration.
53
87
 
54
88
  Similar to docker-compose up. Creates a new container or starts existing one.
55
89
  Supports zero-config mode - works without any configuration file.
56
90
 
57
- Container Lifecycle (Feature 011):
91
+ \b
92
+ Container Lifecycle:
58
93
  - Containers persist until explicitly removed with 'container remove'
59
94
  - Volume mounts are verified during creation
60
95
  - No automatic cleanup when CLI exits
61
96
 
62
97
  \b
63
98
  Examples:
64
- # Zero-config (uses Community edition defaults)
65
99
  iris-devtester container up
66
-
67
- # With custom container name
100
+ iris-devtester container up --edition light
101
+ iris-devtester container up --edition enterprise --license ./iris.key
102
+ iris-devtester container up --image myregistry/iris:2024.1
68
103
  iris-devtester container up --name my-test-db
69
-
70
- # With custom configuration including volumes
71
104
  iris-devtester container up --config iris-config.yml
72
105
 
73
- # Example iris-config.yml with volumes:
74
- # edition: community
75
- # volumes:
76
- # - ./workspace:/external/workspace
77
- # - ./config:/opt/config:ro
78
-
79
- # Foreground mode (see logs)
80
- iris-devtester container up --no-detach
106
+ \b
107
+ NOTE: For multi-container setups (sharding, mirroring, clusters),
108
+ use docker-compose instead. This tool manages single containers.
81
109
  """
82
110
  try:
83
111
  # Load configuration
@@ -99,6 +127,36 @@ def up(ctx, config, name, detach, timeout, cpf):
99
127
  container_config.container_name = name
100
128
  click.echo(f" → Container name: {name}")
101
129
 
130
+ # Override image if provided via --image (takes precedence over --edition)
131
+ if image:
132
+ container_config.image = image
133
+ click.echo(f" → Image: {image} (custom)")
134
+ # Override edition if provided via --edition (only if --image not set)
135
+ elif edition:
136
+ edition_lower = edition.lower()
137
+ container_config.edition = edition_lower
138
+
139
+ # Set appropriate image based on edition
140
+ if edition_lower == "light":
141
+ # Light edition: caretdev/iris-community-light (85% smaller)
142
+ container_config.image_tag = "latest-em"
143
+ click.echo(
144
+ click.style(f" → Edition: light", fg="cyan")
145
+ + " (minimal for CI/CD, ~580MB vs ~3.5GB)"
146
+ )
147
+ elif edition_lower == "enterprise":
148
+ if not license_key:
149
+ raise click.ClickException(
150
+ "Enterprise edition requires --license option.\n"
151
+ "\n"
152
+ "Usage: iris-devtester container up --edition enterprise --license /path/to/iris.key"
153
+ )
154
+ container_config.license_key = license_key
155
+ click.echo(f" → Edition: enterprise")
156
+ click.echo(f" → License: {license_key}")
157
+ else:
158
+ click.echo(f" → Edition: community")
159
+
102
160
  if cpf:
103
161
  container_config.cpf_merge = cpf
104
162
  click.echo(f" → CPF Merge: {cpf[:50]}...")
@@ -272,6 +330,141 @@ def up(ctx, config, name, detach, timeout, cpf):
272
330
  ctx.exit(1)
273
331
 
274
332
 
333
+ @container_group.command(name="list")
334
+ @click.option(
335
+ "--all", "-a", "show_all", is_flag=True, help="Show all containers (including stopped)"
336
+ )
337
+ @click.option(
338
+ "--format",
339
+ "output_format",
340
+ type=click.Choice(["table", "json"], case_sensitive=False),
341
+ default="table",
342
+ help="Output format (default: table)",
343
+ )
344
+ @click.pass_context
345
+ def list_containers(ctx, show_all, output_format):
346
+ """
347
+ List IRIS containers.
348
+
349
+ Shows all IRIS containers managed by iris-devtester, with their status,
350
+ edition, ports, and age.
351
+
352
+ \b
353
+ Examples:
354
+ # List running containers
355
+ iris-devtester container list
356
+
357
+ # List all containers (including stopped)
358
+ iris-devtester container list --all
359
+
360
+ # JSON output for scripting
361
+ iris-devtester container list --format json
362
+ """
363
+ from datetime import datetime
364
+
365
+ import docker
366
+
367
+ try:
368
+ client = docker.from_env()
369
+
370
+ # Find IRIS containers (by image name patterns)
371
+ iris_patterns = [
372
+ "iris-community",
373
+ "intersystems/iris",
374
+ "caretdev/iris",
375
+ "intersystemsdc/iris",
376
+ ]
377
+
378
+ containers = client.containers.list(all=show_all)
379
+ iris_containers = []
380
+
381
+ for container in containers:
382
+ image_name = (
383
+ container.image.tags[0] if container.image.tags else str(container.image.id)[:12]
384
+ )
385
+
386
+ # Check if this is an IRIS container
387
+ is_iris = any(pattern in image_name.lower() for pattern in iris_patterns)
388
+ if not is_iris:
389
+ continue
390
+
391
+ # Determine edition from image name
392
+ if "light" in image_name.lower():
393
+ edition = "light"
394
+ elif "community" in image_name.lower():
395
+ edition = "community"
396
+ else:
397
+ edition = "enterprise"
398
+
399
+ # Get port mappings
400
+ ports = container.attrs.get("NetworkSettings", {}).get("Ports", {})
401
+ port_str = "-"
402
+ if ports and ports.get("1972/tcp"):
403
+ host_port = ports["1972/tcp"][0]["HostPort"]
404
+ port_str = f"{host_port}->1972"
405
+
406
+ # Calculate age
407
+ created = container.attrs.get("Created", "")
408
+ age_str = "unknown"
409
+ if created:
410
+ try:
411
+ # Parse ISO format timestamp
412
+ created_dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
413
+ now = datetime.now(created_dt.tzinfo)
414
+ delta = now - created_dt
415
+ if delta.days > 0:
416
+ age_str = f"{delta.days}d"
417
+ elif delta.seconds > 3600:
418
+ age_str = f"{delta.seconds // 3600}h"
419
+ else:
420
+ age_str = f"{delta.seconds // 60}m"
421
+ except Exception:
422
+ pass
423
+
424
+ iris_containers.append(
425
+ {
426
+ "name": container.name,
427
+ "edition": edition,
428
+ "status": container.status,
429
+ "ports": port_str,
430
+ "age": age_str,
431
+ "image": image_name,
432
+ }
433
+ )
434
+
435
+ if output_format == "json":
436
+ import json as json_module
437
+
438
+ click.echo(json_module.dumps(iris_containers, indent=2))
439
+ else:
440
+ # Table format
441
+ if not iris_containers:
442
+ click.echo("No IRIS containers found.")
443
+ if not show_all:
444
+ click.echo("Use --all to include stopped containers.")
445
+ else:
446
+ # Print header
447
+ click.echo(f"{'NAME':<20} {'EDITION':<12} {'STATUS':<10} {'PORTS':<15} {'AGE':<6}")
448
+ click.echo("-" * 65)
449
+
450
+ for c in iris_containers:
451
+ status_color = "green" if c["status"] == "running" else "yellow"
452
+ click.echo(
453
+ f"{c['name']:<20} "
454
+ f"{c['edition']:<12} "
455
+ f"{click.style(c['status'], fg=status_color):<19} "
456
+ f"{c['ports']:<15} "
457
+ f"{c['age']:<6}"
458
+ )
459
+
460
+ except docker.errors.DockerException as e:
461
+ progress.print_error(f"Docker error: {e}")
462
+ ctx.exit(1)
463
+ except Exception as e:
464
+ progress.print_error(f"Error listing containers: {e}")
465
+ ctx.exit(1)
466
+
467
+
275
468
  @container_group.command(name="start")
276
469
  @click.argument("container_name", required=False, default="iris_db")
277
470
  @click.option(
@@ -657,7 +850,7 @@ def remove(ctx, container_name, force, volumes):
657
850
 
658
851
 
659
852
  @container_group.command(name="reset-password")
660
- @click.argument("container_name")
853
+ @click.argument("container_name", required=False, default="iris_db")
661
854
  @click.option("--user", default="_SYSTEM", help="Username to reset password for (default: _SYSTEM)")
662
855
  @click.option("--password", default="SYS", help="New password (default: SYS)")
663
856
  @click.option(
@@ -723,7 +916,7 @@ def reset_password_cmd(ctx, container_name, user, password, port):
723
916
 
724
917
 
725
918
  @container_group.command(name="test-connection")
726
- @click.argument("container_name")
919
+ @click.argument("container_name", required=False, default="iris_db")
727
920
  @click.option(
728
921
  "--namespace", default="USER", help="IRIS namespace to test connection to (default: USER)"
729
922
  )
@@ -812,7 +1005,7 @@ def test_connection_cmd(ctx, container_name, namespace, username, password):
812
1005
 
813
1006
 
814
1007
  @container_group.command(name="enable-callin")
815
- @click.argument("container_name")
1008
+ @click.argument("container_name", required=False, default="iris_db")
816
1009
  @click.option(
817
1010
  "--timeout", type=int, default=30, help="Timeout in seconds for docker commands (default: 30)"
818
1011
  )
@@ -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(
@@ -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
  """
@@ -5,6 +5,7 @@ from typing import Any, Optional
5
5
 
6
6
  from iris_devtester.config import IRISConfig
7
7
  from iris_devtester.connections import get_connection
8
+ from iris_devtester.containers.models import HealthCheckLevel, ValidationResult
8
9
 
9
10
  logger = logging.getLogger(__name__)
10
11
 
@@ -59,6 +60,9 @@ class IRISContainer(IRISBase):
59
60
  Enhanced IRIS container with automatic connection and password management.
60
61
  """
61
62
 
63
+ # Custom kwargs that should NOT be passed to parent/Docker SDK
64
+ _CUSTOM_KWARGS = {"port_registry", "preferred_port", "project_path"}
65
+
62
66
  def __init__(
63
67
  self,
64
68
  image: str = "intersystemsdc/iris-community:latest",
@@ -70,6 +74,12 @@ class IRISContainer(IRISBase):
70
74
  if not HAS_TESTCONTAINERS:
71
75
  logger.warning("testcontainers not installed. Functionality will be limited.")
72
76
 
77
+ # Extract custom kwargs before passing to parent
78
+ self._port_registry = kwargs.pop("port_registry", None)
79
+ self._preferred_port = kwargs.pop("preferred_port", None)
80
+ self._project_path = kwargs.pop("project_path", None)
81
+ self._port_assignment = None # Will be set in start() if port_registry is used
82
+
73
83
  super().__init__(image=image, **kwargs)
74
84
  self._username = username
75
85
  self._password = password
@@ -94,30 +104,176 @@ class IRISContainer(IRISBase):
94
104
  self._preconfigure_username: Optional[str] = None
95
105
 
96
106
  @classmethod
97
- def community(cls, image: Optional[str] = None, **kwargs) -> "IRISContainer":
98
- """Create a Community Edition container."""
107
+ def community(
108
+ cls, image: Optional[str] = None, version: str = "latest", **kwargs
109
+ ) -> "IRISContainer":
110
+ """
111
+ Create a Community Edition container.
112
+
113
+ Auto-detects architecture (ARM64 vs x86) and pulls the appropriate image.
114
+
115
+ Args:
116
+ image: Docker image to use. If None, auto-detects based on architecture.
117
+ version: Image version tag. Options: 'latest', '2025.1', '2025.2', etc.
118
+ """
99
119
  if image is None:
100
120
  import platform as platform_module
101
121
 
102
122
  if platform_module.machine() == "arm64":
103
- image = "containers.intersystems.com/intersystems/iris-community:2025.1"
123
+ # ARM64 (Apple Silicon) - use official InterSystems registry
124
+ tag = version if version != "latest" else "2025.1"
125
+ image = f"containers.intersystems.com/intersystems/iris-community:{tag}"
104
126
  else:
105
- image = "intersystemsdc/iris-community:latest"
127
+ # x86_64 - use Docker Hub community image
128
+ image = f"intersystemsdc/iris-community:{version}"
106
129
  return cls(image=image, **kwargs)
107
130
 
108
131
  @classmethod
109
- def enterprise(cls, license_key: str, image: Optional[str] = None, **kwargs) -> "IRISContainer":
110
- """Create an Enterprise Edition container."""
132
+ def enterprise(
133
+ cls, license_key: Optional[str] = None, image: Optional[str] = None, **kwargs
134
+ ) -> "IRISContainer":
135
+ """
136
+ Create an Enterprise Edition container.
137
+
138
+ Args:
139
+ license_key: Path to iris.key file. If None, checks IRIS_LICENSE_KEY env var.
140
+ image: Docker image to use. Defaults to containers.intersystems.com/intersystems/iris:latest
141
+
142
+ Raises:
143
+ ValueError: If no license key is provided or found in environment.
144
+ """
145
+ import os
146
+
147
+ if license_key is None:
148
+ license_key = os.environ.get("IRIS_LICENSE_KEY")
149
+
150
+ if license_key is None:
151
+ raise ValueError(
152
+ "Enterprise edition requires a license key.\n"
153
+ "\n"
154
+ "Provide license_key parameter or set IRIS_LICENSE_KEY environment variable:\n"
155
+ " IRISContainer.enterprise(license_key='/path/to/iris.key')\n"
156
+ " # or\n"
157
+ " export IRIS_LICENSE_KEY=/path/to/iris.key"
158
+ )
159
+
160
+ if not os.path.exists(license_key):
161
+ raise ValueError(
162
+ f"License key file not found: {license_key}\n"
163
+ "\n"
164
+ "Verify the license key path exists and is readable."
165
+ )
166
+
111
167
  if image is None:
112
168
  image = "containers.intersystems.com/intersystems/iris:latest"
169
+
113
170
  container = cls(image=image, **kwargs)
171
+ # Mount license key into container
172
+ container._license_key_path = license_key
114
173
  return container
115
174
 
175
+ @classmethod
176
+ def light(
177
+ cls, image: Optional[str] = None, version: str = "latest", **kwargs
178
+ ) -> "IRISContainer":
179
+ """
180
+ Create a Light Edition container optimized for CI/CD.
181
+
182
+ Light edition is ~85% smaller than full Community edition (~580MB vs ~3.5GB).
183
+ It removes Interoperability, Management Portal, DeepSee, and web components.
184
+ DBAPI, JDBC, and ODBC connectivity are fully supported.
185
+
186
+ Args:
187
+ image: Docker image to use. Defaults to caretdev/iris-community-light.
188
+ version: Image version tag. Options: 'latest', 'latest-em' (LTS), '2025.1', etc.
189
+
190
+ Best for:
191
+ - CI/CD pipelines
192
+ - Microservices
193
+ - Automated testing
194
+ - SQL-only workloads
195
+
196
+ Not supported:
197
+ - Interoperability/Ensemble
198
+ - Management Portal
199
+ - DeepSee/BI
200
+ - CSP/REST web framework
201
+ """
202
+ if image is None:
203
+ # Use latest-em for LTS stability, or allow version override
204
+ tag = version if version != "latest" else "latest-em"
205
+ image = f"caretdev/iris-community-light:{tag}"
206
+ return cls(image=image, **kwargs)
207
+
116
208
  def with_name(self, name: str) -> "IRISContainer":
117
209
  """Set the container name."""
118
210
  self._container_name = name
119
- if hasattr(self, "with_kwargs"):
120
- self.with_kwargs(name=name)
211
+ # Use parent's _name attribute directly - do NOT use with_kwargs(name=...)
212
+ # as that causes duplicate 'name' kwarg in Docker's run() call
213
+ # (parent passes both name=self._name and **self._kwargs to run())
214
+ self._name = name
215
+ return self
216
+
217
+ def with_cpf_merge(self, cpf_content_or_path: str) -> "IRISContainer":
218
+ """Configure CPF merge for IRIS startup customization.
219
+
220
+ CPF merge allows customizing IRIS configuration at startup time
221
+ using a merge file that is applied during container initialization.
222
+ This enables features like:
223
+ - Enabling CallIn service automatically
224
+ - Setting memory configuration
225
+ - Pre-configuring users and security settings
226
+
227
+ Args:
228
+ cpf_content_or_path: Either a CPF merge content string or a
229
+ path to a CPF merge file. If the string contains newlines
230
+ or CPF section markers like "[Actions]", it's treated as
231
+ content. Otherwise, it's treated as a file path.
232
+
233
+ Returns:
234
+ Self for method chaining.
235
+
236
+ Examples:
237
+ >>> # From preset content
238
+ >>> iris = IRISContainer.community().with_cpf_merge(CPFPreset.ENABLE_CALLIN)
239
+
240
+ >>> # From file path
241
+ >>> iris = IRISContainer.community().with_cpf_merge("/path/to/merge.cpf")
242
+ """
243
+ import os
244
+ import tempfile
245
+
246
+ # Determine if it's content or a file path
247
+ is_content = "\n" in cpf_content_or_path or "[" in cpf_content_or_path
248
+
249
+ if is_content:
250
+ # Write content to a temporary file
251
+ # Note: The temp file needs to persist until container is started
252
+ if not hasattr(self, "_cpf_temp_files"):
253
+ self._cpf_temp_files = []
254
+
255
+ fd, temp_path = tempfile.mkstemp(suffix=".cpf", prefix="iris_merge_")
256
+ os.write(fd, cpf_content_or_path.encode("utf-8"))
257
+ os.close(fd)
258
+ self._cpf_temp_files.append(temp_path)
259
+ host_path = temp_path
260
+ else:
261
+ # Treat as file path
262
+ host_path = os.path.abspath(cpf_content_or_path)
263
+ if not os.path.exists(host_path):
264
+ raise FileNotFoundError(f"CPF merge file not found: {host_path}")
265
+
266
+ # Container path for the merge file
267
+ container_path = "/tmp/merge.cpf"
268
+
269
+ # Mount the CPF file into the container
270
+ if hasattr(self, "with_volume_mapping"):
271
+ self.with_volume_mapping(host_path, container_path, "ro")
272
+
273
+ # Set the environment variable to tell IRIS to use the merge file
274
+ if hasattr(self, "with_env"):
275
+ self.with_env("ISC_CPF_MERGE_FILE", container_path)
276
+
121
277
  return self
122
278
 
123
279
  def get_container_name(self) -> str:
@@ -222,14 +378,19 @@ class IRISContainer(IRISBase):
222
378
  self.execute_objectscript(script, namespace="%SYS")
223
379
 
224
380
  def get_config(self) -> IRISConfig:
225
- """Get connection configuration."""
226
- if self._config is None:
227
- self._config = IRISConfig(
228
- username=self._username,
229
- password=self._password,
230
- namespace=self._namespace,
231
- container_name=self.get_container_name(),
232
- )
381
+ """Get connection configuration.
382
+
383
+ Note: Credentials are always read fresh from _username/_password
384
+ to support credential updates after container start.
385
+ """
386
+ # Always create fresh config to pick up any credential changes
387
+ # (e.g., conftest may update _username/_password after start())
388
+ self._config = IRISConfig(
389
+ username=self._username,
390
+ password=self._password,
391
+ namespace=self._namespace,
392
+ container_name=self.get_container_name(),
393
+ )
233
394
  config = self._config
234
395
  try:
235
396
  # Get host and mapped port from testcontainers
@@ -297,20 +458,91 @@ class IRISContainer(IRISBase):
297
458
  return self
298
459
 
299
460
  def start(self) -> "IRISContainer":
300
- """Start container with pre-config support."""
461
+ """Start container with pre-config support and port registry integration."""
301
462
  if self._preconfigure_password:
302
463
  self.with_env("IRIS_PASSWORD", self._preconfigure_password)
303
464
  if self._preconfigure_username:
304
465
  self.with_env("IRIS_USERNAME", self._preconfigure_username)
305
466
 
467
+ # Port registry integration: assign port before starting
468
+ if self._port_registry is not None and self._project_path is not None:
469
+ from iris_devtester.ports import PortAssignment
470
+
471
+ self._port_assignment = self._port_registry.assign_port(
472
+ project_path=self._project_path,
473
+ preferred_port=self._preferred_port,
474
+ )
475
+ # Configure container to use the assigned port
476
+ # Note: testcontainers uses with_bind_ports() for port mapping
477
+ if hasattr(self, "with_bind_ports"):
478
+ self.with_bind_ports(1972, self._port_assignment.port)
479
+
306
480
  super().start()
307
481
  # Ensure host/port are updated after start
308
482
  self.get_config()
309
483
  self._password_preconfigured = True
310
484
  return self
311
485
 
486
+ def get_assigned_port(self) -> Optional[int]:
487
+ """
488
+ Get the port assigned by the port registry.
489
+
490
+ Returns:
491
+ The assigned port number, or None if no port registry was used.
492
+ """
493
+ if hasattr(self, "_port_assignment") and self._port_assignment is not None:
494
+ return self._port_assignment.port
495
+ return None
496
+
497
+ def get_project_path(self) -> Optional[str]:
498
+ """
499
+ Get the project path associated with this container.
500
+
501
+ Returns:
502
+ The project path, or None if no port registry was used.
503
+ """
504
+ return self._project_path
505
+
312
506
  def wait_for_ready(self, timeout: int = 60) -> bool:
313
507
  """Wait for IRIS to be ready."""
314
508
  # Simple wait for prototype
315
509
  time.sleep(15)
316
510
  return True
511
+
512
+ def validate(self, level: HealthCheckLevel = HealthCheckLevel.STANDARD) -> ValidationResult:
513
+ """Validate this container's health status.
514
+
515
+ Args:
516
+ level: Validation depth level (MINIMAL, STANDARD, or FULL).
517
+
518
+ Returns:
519
+ ValidationResult with success status, details, and remediation steps.
520
+
521
+ Examples:
522
+ >>> with IRISContainer.community() as iris:
523
+ ... result = iris.validate()
524
+ ... assert result.success is True
525
+ """
526
+ # Import here to avoid circular import
527
+ from iris_devtester.containers.validation import validate_container
528
+
529
+ container_name = self.get_container_name()
530
+ return validate_container(container_name=container_name, level=level)
531
+
532
+ def assert_healthy(self, level: HealthCheckLevel = HealthCheckLevel.STANDARD) -> None:
533
+ """Assert that this container is healthy, raising RuntimeError if not.
534
+
535
+ Args:
536
+ level: Validation depth level (MINIMAL, STANDARD, or FULL).
537
+
538
+ Raises:
539
+ RuntimeError: If container validation fails, with structured error
540
+ message including "What went wrong" and "How to fix it" sections.
541
+
542
+ Examples:
543
+ >>> with IRISContainer.community() as iris:
544
+ ... iris.assert_healthy() # No exception = healthy
545
+ """
546
+ result = self.validate(level=level)
547
+ if not result.success:
548
+ raise RuntimeError(result.format_message())
@@ -1,17 +1,23 @@
1
- """IRIS .DAT Fixture Management.
1
+ """IRIS Fixture Management.
2
2
 
3
3
  This module provides tools for creating, loading, and validating IRIS database
4
- fixtures stored as .DAT files. Fixtures enable fast, reproducible test data
5
- setup by exporting database namespaces to version-controlled files.
4
+ fixtures. Fixtures enable fast, reproducible test data setup by exporting
5
+ database namespaces to version-controlled files.
6
6
 
7
7
  Key Features:
8
- - Create fixtures from IRIS namespaces (entire database backup)
9
- - Load fixtures via namespace mounting (<1 second)
8
+ - Create fixtures from IRIS namespaces (globals + class definitions)
9
+ - Load fixtures via global import (<1 second for most fixtures)
10
10
  - Validate fixture integrity with SHA256 checksums
11
11
  - CLI commands for fixture management
12
12
 
13
+ Fixture Format:
14
+ Fixtures are stored as directories containing:
15
+ - manifest.json: Metadata, checksums, and table information
16
+ - globals.gof: Global data in IRIS %GOF format
17
+ - classes.xml: Class definitions (optional, for SQL tables)
18
+
13
19
  Example:
14
- >>> from iris_devtester.fixtures import DATFixtureLoader, FixtureCreator
20
+ >>> from iris_devtester.fixtures import FixtureLoader, FixtureCreator
15
21
  >>>
16
22
  >>> # Create fixture from existing namespace
17
23
  >>> creator = FixtureCreator(container=iris_container)
@@ -22,7 +28,7 @@ Example:
22
28
  ... )
23
29
  >>>
24
30
  >>> # Load fixture into new namespace
25
- >>> loader = DATFixtureLoader(container=iris_container)
31
+ >>> loader = FixtureLoader(container=iris_container)
26
32
  >>> target_ns = iris_container.get_test_namespace(prefix="LOADED")
27
33
  >>> result = loader.load_fixture(
28
34
  ... fixture_path="./fixtures/test-data",
@@ -35,7 +41,7 @@ pytest Integration:
35
41
 
36
42
  @pytest.fixture
37
43
  def loaded_fixture(iris_container):
38
- loader = DATFixtureLoader(container=iris_container)
44
+ loader = FixtureLoader(container=iris_container)
39
45
  target_ns = iris_container.get_test_namespace(prefix="TEST")
40
46
  result = loader.load_fixture(
41
47
  fixture_path="./fixtures/test-data",
@@ -53,7 +59,7 @@ pytest Integration:
53
59
  __version__ = "0.1.0"
54
60
 
55
61
  from .creator import FixtureCreator
56
- from .loader import DATFixtureLoader
62
+ from .loader import FixtureLoader
57
63
 
58
64
  # Import data models and exceptions
59
65
  from .manifest import (
@@ -83,6 +89,9 @@ from .obj_export import (
83
89
  # Import validator, loader, and creator
84
90
  from .validator import FixtureValidator
85
91
 
92
+ # Backward compatibility alias
93
+ DATFixtureLoader = FixtureLoader
94
+
86
95
  # Public API
87
96
  __all__ = [
88
97
  # Data models
@@ -98,8 +107,10 @@ __all__ = [
98
107
  "ChecksumMismatchError",
99
108
  # Classes
100
109
  "FixtureValidator",
101
- "DATFixtureLoader",
110
+ "FixtureLoader",
102
111
  "FixtureCreator",
112
+ # Backward compatibility
113
+ "DATFixtureLoader",
103
114
  # $SYSTEM.OBJ utilities (Feature 017)
104
115
  "ExportResult",
105
116
  "ImportResult",
@@ -17,7 +17,12 @@ from .validator import FixtureValidator
17
17
 
18
18
  class FixtureCreator:
19
19
  """
20
- Creates .DAT fixtures by exporting IRIS namespaces.
20
+ Creates IRIS fixtures by exporting namespace globals and class definitions.
21
+
22
+ Fixture Format:
23
+ - globals.gof: Global data in IRIS %GOF format
24
+ - classes.xml: Class definitions for SQL tables
25
+ - manifest.json: Metadata and checksums
21
26
  """
22
27
 
23
28
  def __init__(
@@ -64,7 +69,12 @@ class FixtureCreator:
64
69
  from iris_devtester.config import discover_config
65
70
  from iris_devtester.connections import get_connection as get_conn_factory
66
71
 
67
- base_config = self.connection_config or discover_config()
72
+ # Use container's config if available, otherwise fall back to discovery
73
+ base_config = self.connection_config
74
+ if base_config is None and self.container is not None:
75
+ base_config = self.container.get_config()
76
+ if base_config is None:
77
+ base_config = discover_config()
68
78
  ns_config = dataclasses.replace(base_config, namespace=namespace)
69
79
 
70
80
  ns_connection = get_conn_factory(ns_config)
@@ -223,7 +233,12 @@ class FixtureCreator:
223
233
  get_connection as get_modern_connection,
224
234
  )
225
235
 
226
- self._connection = get_modern_connection(self.connection_config)
236
+ # If container is provided but no explicit connection_config, use container's config
237
+ config = self.connection_config
238
+ if config is None and self.container is not None:
239
+ config = self.container.get_config()
240
+
241
+ self._connection = get_modern_connection(config)
227
242
  return self._connection
228
243
 
229
244
  def _get_iris_version(self) -> str:
@@ -12,7 +12,7 @@ from .manifest import FixtureLoadError, FixtureManifest, FixtureValidationError,
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
14
 
15
- class DATFixtureLoader:
15
+ class FixtureLoader:
16
16
 
17
17
  def __init__(self, container: Optional[IRISContainer] = None, **kwargs):
18
18
  self.container = container
@@ -45,14 +45,17 @@ class DATFixtureLoader:
45
45
  ) -> LoadResult:
46
46
  start_time = time.time()
47
47
 
48
+ # Validate fixture exists BEFORE starting a container (Fail Fast)
49
+ manifest = self._load_manifest(fixture_path)
50
+
48
51
  if not self.container:
49
52
  self.container = IRISContainer.community()
50
53
  self.container.start()
51
54
  self._owns_container = True
52
55
 
53
56
  try:
54
- manifest = self._load_manifest(fixture_path)
55
57
  namespace = target_namespace or manifest.namespace
58
+ # Note: manifest already loaded above (fail-fast before container start)
56
59
 
57
60
  dat_file_path = Path(fixture_path) / "IRIS.DAT"
58
61
  if not dat_file_path.exists():
@@ -1,7 +1,13 @@
1
1
  """Fixture manifest data models and validation.
2
2
 
3
- This module defines the data structures for IRIS .DAT fixture manifests,
3
+ This module defines the data structures for IRIS fixture manifests,
4
4
  including FixtureManifest, TableInfo, ValidationResult, and LoadResult.
5
+
6
+ Fixture Format:
7
+ Fixtures are stored as directories containing:
8
+ - manifest.json: Metadata, checksums, and table information
9
+ - globals.gof: Global data in IRIS %GOF format
10
+ - classes.xml: Class definitions (optional, for SQL tables)
5
11
  """
6
12
 
7
13
  import json
@@ -46,7 +52,6 @@ class TableInfo:
46
52
  """
47
53
  Information about a single table in a fixture.
48
54
 
49
- Note: All tables are stored in a single IRIS.DAT file.
50
55
  This class tracks which tables are included in the fixture.
51
56
 
52
57
  Attributes:
@@ -69,11 +74,12 @@ class TableInfo:
69
74
  @dataclass
70
75
  class FixtureManifest:
71
76
  """
72
- Manifest describing a .DAT fixture.
77
+ Manifest describing an IRIS fixture.
73
78
 
74
79
  A fixture is a directory containing:
75
80
  - manifest.json (this schema)
76
- - IRIS.DAT (single database file containing all tables)
81
+ - globals.gof (global data in %GOF format)
82
+ - classes.xml (optional, class definitions for SQL tables)
77
83
 
78
84
  Example manifest.json:
79
85
  {
@@ -93,6 +99,9 @@ class FixtureManifest:
93
99
  }
94
100
  ]
95
101
  }
102
+
103
+ Note: The dat_file field is kept for backward compatibility but the actual
104
+ data is stored in globals.gof (GOF format).
96
105
  """
97
106
 
98
107
  # Required fields
@@ -1,7 +1,13 @@
1
- """IRIS .DAT Fixture Validator.
1
+ """IRIS Fixture Validator.
2
2
 
3
3
  This module provides the FixtureValidator class for validating fixture
4
4
  integrity including manifest structure, file existence, and SHA256 checksums.
5
+
6
+ Fixture Format:
7
+ Fixtures are stored as directories containing:
8
+ - manifest.json: Metadata, checksums, and table information
9
+ - globals.gof: Global data in IRIS %GOF format
10
+ - classes.xml: Class definitions (optional, for SQL tables)
5
11
  """
6
12
 
7
13
  import hashlib
@@ -18,11 +24,11 @@ from .manifest import (
18
24
 
19
25
  class FixtureValidator:
20
26
  """
21
- Validates .DAT fixture integrity.
27
+ Validates fixture integrity.
22
28
 
23
29
  This is a stateless validator that checks:
24
30
  - Manifest structure and required fields
25
- - File existence (manifest.json, IRIS.DAT)
31
+ - File existence (manifest.json, globals.gof)
26
32
  - SHA256 checksum matching
27
33
  - Fixture size statistics
28
34
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris-devtester
3
- Version: 1.9.3
3
+ Version: 1.10.2
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,9 +107,17 @@ def test_connection():
107
107
  assert cursor.fetchone()[0] == 1
108
108
  ```
109
109
 
110
- ## Container API
110
+ ## Container Editions
111
111
 
112
- ### Basic Usage
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
113
121
  ```python
114
122
  from iris_devtester.containers import IRISContainer
115
123
 
@@ -117,11 +125,42 @@ from iris_devtester.containers import IRISContainer
117
125
  with IRISContainer.community() as iris:
118
126
  conn = iris.get_connection()
119
127
 
128
+ # Light Edition (85% smaller, for CI/CD)
129
+ with IRISContainer.light() as iris:
130
+ conn = iris.get_connection()
131
+
120
132
  # Enterprise Edition (requires license)
121
133
  with IRISContainer.enterprise(license_key="/path/to/iris.key") as iris:
122
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
123
154
  ```
124
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
+
125
164
  ### Builder Methods
126
165
  ```python
127
166
  # Set a custom container name (for debugging, logs, multiple containers)
@@ -1,13 +1,13 @@
1
- iris_devtester/__init__.py,sha256=HzbADwcjJamdWENJTE4FaervonOKGTfKdxU8VkG_kcM,1848
2
- iris_devtester/cli/__init__.py,sha256=yakDJH-EsnTBmKl_K7r2n47EDiWvfpwLVcY11KriQQQ,691
1
+ iris_devtester/__init__.py,sha256=7Ud7QRVdnZQ205YQ02_a-OFoKidWXBeTS9peUW6u60g,1849
2
+ iris_devtester/cli/__init__.py,sha256=Gs11Sj_v4OaKWBf2-dCaUB7RwXSxwGcuYjn8rHgsnRU,1914
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=FcDb6EiSa8SYvzsntxu5j5q6PCh1jba7wosd_KUqe0U,30736
5
+ iris_devtester/cli/container.py,sha256=ZVWmoxCzZPezjSNPQZRJLdGmeJCJzs7yw_iqK-UVt-Y,37763
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=UNtgjfWlO517KzH0upwpx1xBWpyvhXIZQmxRQ-OKLNI,10925
10
+ iris_devtester/config/container_config.py,sha256=RsxfMIi9SF5tnmIAl_srASZrZrFSaV8BCxlFfMusYcQ,11353
11
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
@@ -24,19 +24,19 @@ 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=GA9fgcjU0nLJnSP-PnZDWVMZcwOR7s45ZHK5pNxgYok,11083
27
+ iris_devtester/containers/iris_container.py,sha256=VdxX88785sY6WEj-NtV-BjTrhVpd2lXvMzzMX8xJORw,20276
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
31
31
  iris_devtester/containers/performance.py,sha256=fNLUqsTDgPrIBb2sP2ubVPM5oVconrXA1cn6Bq_2JvM,11272
32
32
  iris_devtester/containers/validation.py,sha256=SUOsEc2UPHJTYosSweSmF8pcDLYnrvXZNOaApTu7PQc,14564
33
33
  iris_devtester/containers/wait_strategies.py,sha256=cUJNU8eD8EWKSaQvEWqkCKkBPCotnT_NBrw2J70ZXVA,7754
34
- iris_devtester/fixtures/__init__.py,sha256=HwE6nEBqFXMpQNjFZ1EdTp7w8KpdChtLx5hkGzm6tzQ,3149
35
- iris_devtester/fixtures/creator.py,sha256=sA3y_SjF4XmMICQ2V2n1pHrfcYspMqa1SOb_koV1yLM,8019
36
- iris_devtester/fixtures/loader.py,sha256=2o15QC9lHmf7C3Ce9PrPuagQdqo1OKRvoyuN_pKUj3o,12493
37
- iris_devtester/fixtures/manifest.py,sha256=ulLcMw8cKDvQDndIj0izRKOgTNmWI70uNviZCbXeKsc,9962
34
+ iris_devtester/fixtures/__init__.py,sha256=foqK_3CCjvIvEipiD_PmXKLclHXq2qPBbdRpewnxiLQ,3491
35
+ iris_devtester/fixtures/creator.py,sha256=L3mQ_AFw2NumDOS9zoQnuv3t3z6MMAR9_5-nrNi99Kg,8716
36
+ iris_devtester/fixtures/loader.py,sha256=oUQdlCX17Abr4muu8dj_sLVxumQ5-hDwxy7So6A0sw8,12646
37
+ iris_devtester/fixtures/manifest.py,sha256=FZUjdzOZfkBJSncglPhD_Xc29cV1z7tjkQExA7Af8lY,10324
38
38
  iris_devtester/fixtures/obj_export.py,sha256=avJtCtLgs4DW5gFcLb6dzPEH_g5lhbYMnCVvpFW4T2Y,14753
39
- iris_devtester/fixtures/validator.py,sha256=Q4AcucnLX8DxICoS6n7p3Ogm1RB-Mp9IsHqtMXiUVto,11671
39
+ iris_devtester/fixtures/validator.py,sha256=gnYAYVNfH2xaD4gFgbtfeZ4pQuV5x0MpQ4djt_VLOIQ,11911
40
40
  iris_devtester/integrations/__init__.py,sha256=wMduwyadsOOwZ8odfVNZkjeKg-u2APnvMz_jdvWaL0A,368
41
41
  iris_devtester/integrations/langchain.py,sha256=cIV8bq_wDKMOXLFQ1P-wdSFsJdxIV7o0-a5f6-ELbME,11310
42
42
  iris_devtester/ports/__init__.py,sha256=uLdbV7J7UJxJsoDqoyySTabIle72WltYDFKlN2-f7bI,506
@@ -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.3.dist-info/licenses/LICENSE,sha256=dISbikDYS2uP710ZFzSzaSmKzIBRyi_6YwuwO97bT94,1083
62
- iris_devtester-1.9.3.dist-info/METADATA,sha256=It5jOKAoyNkbBiyaYQw3kO5TFRog3I9lKeQl2f0eTIo,7682
63
- iris_devtester-1.9.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
64
- iris_devtester-1.9.3.dist-info/entry_points.txt,sha256=svOltni3vX3ul_qxbWl8fOA6ZrJeFASuN29pON6ftqw,59
65
- iris_devtester-1.9.3.dist-info/top_level.txt,sha256=DnrLJ3laB5x_gTkmATDEg1v1lTOQxgmHgNd66bdXaoU,15
66
- iris_devtester-1.9.3.dist-info/RECORD,,
61
+ iris_devtester-1.10.2.dist-info/licenses/LICENSE,sha256=dISbikDYS2uP710ZFzSzaSmKzIBRyi_6YwuwO97bT94,1083
62
+ iris_devtester-1.10.2.dist-info/METADATA,sha256=sh5xjenarcua9puUzAl1Lpf3hEGpbHpp5npOUXeYPfE,8940
63
+ iris_devtester-1.10.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
64
+ iris_devtester-1.10.2.dist-info/entry_points.txt,sha256=svOltni3vX3ul_qxbWl8fOA6ZrJeFASuN29pON6ftqw,59
65
+ iris_devtester-1.10.2.dist-info/top_level.txt,sha256=DnrLJ3laB5x_gTkmATDEg1v1lTOQxgmHgNd66bdXaoU,15
66
+ iris_devtester-1.10.2.dist-info/RECORD,,