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.
- iris_devtester/__init__.py +1 -1
- iris_devtester/cli/__init__.py +32 -3
- iris_devtester/cli/container.py +213 -20
- iris_devtester/config/container_config.py +10 -4
- iris_devtester/containers/iris_container.py +249 -17
- iris_devtester/fixtures/__init__.py +21 -10
- iris_devtester/fixtures/creator.py +18 -3
- iris_devtester/fixtures/loader.py +5 -2
- iris_devtester/fixtures/manifest.py +13 -4
- iris_devtester/fixtures/validator.py +9 -3
- {iris_devtester-1.9.3.dist-info → iris_devtester-1.10.2.dist-info}/METADATA +42 -3
- {iris_devtester-1.9.3.dist-info → iris_devtester-1.10.2.dist-info}/RECORD +16 -16
- {iris_devtester-1.9.3.dist-info → iris_devtester-1.10.2.dist-info}/WHEEL +0 -0
- {iris_devtester-1.9.3.dist-info → iris_devtester-1.10.2.dist-info}/entry_points.txt +0 -0
- {iris_devtester-1.9.3.dist-info → iris_devtester-1.10.2.dist-info}/licenses/LICENSE +0 -0
- {iris_devtester-1.9.3.dist-info → iris_devtester-1.10.2.dist-info}/top_level.txt +0 -0
iris_devtester/__init__.py
CHANGED
iris_devtester/cli/__init__.py
CHANGED
|
@@ -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 -
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
iris_devtester/cli/container.py
CHANGED
|
@@ -21,8 +21,23 @@ def container_group(ctx):
|
|
|
21
21
|
"""
|
|
22
22
|
Container lifecycle management commands.
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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(
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
110
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
1
|
+
"""IRIS Fixture Management.
|
|
2
2
|
|
|
3
3
|
This module provides tools for creating, loading, and validating IRIS database
|
|
4
|
-
fixtures
|
|
5
|
-
|
|
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 (
|
|
9
|
-
- Load fixtures via
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
77
|
+
Manifest describing an IRIS fixture.
|
|
73
78
|
|
|
74
79
|
A fixture is a directory containing:
|
|
75
80
|
- manifest.json (this schema)
|
|
76
|
-
-
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
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
|
|
110
|
+
## Container Editions
|
|
111
111
|
|
|
112
|
-
|
|
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=
|
|
2
|
-
iris_devtester/cli/__init__.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
35
|
-
iris_devtester/fixtures/creator.py,sha256=
|
|
36
|
-
iris_devtester/fixtures/loader.py,sha256=
|
|
37
|
-
iris_devtester/fixtures/manifest.py,sha256=
|
|
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=
|
|
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.
|
|
62
|
-
iris_devtester-1.
|
|
63
|
-
iris_devtester-1.
|
|
64
|
-
iris_devtester-1.
|
|
65
|
-
iris_devtester-1.
|
|
66
|
-
iris_devtester-1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|