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.
- iris_devtester/__init__.py +1 -1
- iris_devtester/cli/container.py +228 -13
- iris_devtester/config/container_config.py +16 -11
- iris_devtester/config/container_state.py +6 -7
- iris_devtester/config/yaml_loader.py +1 -1
- iris_devtester/connections/__init__.py +1 -1
- iris_devtester/containers/iris_container.py +92 -13
- iris_devtester/ports/registry.py +2 -2
- {iris_devtester-1.9.2.dist-info → iris_devtester-1.10.0.dist-info}/METADATA +84 -1
- {iris_devtester-1.9.2.dist-info → iris_devtester-1.10.0.dist-info}/RECORD +14 -14
- {iris_devtester-1.9.2.dist-info → iris_devtester-1.10.0.dist-info}/WHEEL +0 -0
- {iris_devtester-1.9.2.dist-info → iris_devtester-1.10.0.dist-info}/entry_points.txt +0 -0
- {iris_devtester-1.9.2.dist-info → iris_devtester-1.10.0.dist-info}/licenses/LICENSE +0 -0
- {iris_devtester-1.9.2.dist-info → iris_devtester-1.10.0.dist-info}/top_level.txt +0 -0
iris_devtester/__init__.py
CHANGED
iris_devtester/cli/container.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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,
|
|
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
|
-
|
|
308
|
-
|
|
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
|
+
)
|
|
@@ -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,
|
|
4
|
+
from typing import Any, Optional
|
|
6
5
|
|
|
7
|
-
from iris_devtester.config import IRISConfig
|
|
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(
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
111
|
-
|
|
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
|
iris_devtester/ports/registry.py
CHANGED
|
@@ -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.
|
|
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=
|
|
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=
|
|
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=
|
|
11
|
-
iris_devtester/config/container_state.py,sha256=
|
|
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=
|
|
17
|
-
iris_devtester/connections/__init__.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
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.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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|