aissemble-inference-deploy 1.5.0rc3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. aissemble_inference_deploy/__init__.py +38 -0
  2. aissemble_inference_deploy/cli.py +278 -0
  3. aissemble_inference_deploy/config.py +182 -0
  4. aissemble_inference_deploy/generators/__init__.py +36 -0
  5. aissemble_inference_deploy/generators/base.py +239 -0
  6. aissemble_inference_deploy/generators/docker.py +307 -0
  7. aissemble_inference_deploy/generators/kserve.py +89 -0
  8. aissemble_inference_deploy/generators/kubernetes.py +119 -0
  9. aissemble_inference_deploy/generators/local.py +162 -0
  10. aissemble_inference_deploy/registry.py +158 -0
  11. aissemble_inference_deploy/templates/docker/.dockerignore.j2 +47 -0
  12. aissemble_inference_deploy/templates/docker/Dockerfile.j2 +59 -0
  13. aissemble_inference_deploy/templates/docker/README.md.j2 +163 -0
  14. aissemble_inference_deploy/templates/docker/docker-compose.yml.j2 +22 -0
  15. aissemble_inference_deploy/templates/kserve/README.md.j2 +278 -0
  16. aissemble_inference_deploy/templates/kserve/inference-service.yaml.j2 +14 -0
  17. aissemble_inference_deploy/templates/kserve/serving-runtime.yaml.j2 +35 -0
  18. aissemble_inference_deploy/templates/kubernetes/README.md.j2 +164 -0
  19. aissemble_inference_deploy/templates/kubernetes/deployment.yaml.j2 +50 -0
  20. aissemble_inference_deploy/templates/kubernetes/kustomization.yaml.j2 +11 -0
  21. aissemble_inference_deploy/templates/kubernetes/overlays/dev/kustomization.yaml.j2 +52 -0
  22. aissemble_inference_deploy/templates/kubernetes/overlays/prod/kustomization.yaml.j2 +36 -0
  23. aissemble_inference_deploy/templates/kubernetes/service.yaml.j2 +19 -0
  24. aissemble_inference_deploy/templates/local/run-mlserver.sh.j2 +47 -0
  25. aissemble_inference_deploy-1.5.0rc3.dist-info/METADATA +248 -0
  26. aissemble_inference_deploy-1.5.0rc3.dist-info/RECORD +29 -0
  27. aissemble_inference_deploy-1.5.0rc3.dist-info/WHEEL +4 -0
  28. aissemble_inference_deploy-1.5.0rc3.dist-info/entry_points.txt +8 -0
  29. aissemble_inference_deploy-1.5.0rc3.dist-info/licenses/LICENSE.txt +201 -0
@@ -0,0 +1,38 @@
1
+ ###
2
+ # #%L
3
+ # aiSSEMBLE::Open Inference Protocol::Modules::deploy
4
+ # %%
5
+ # Copyright (C) 2024 Booz Allen Hamilton Inc.
6
+ # %%
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ # #L%
19
+ ###
20
+ """
21
+ aiSSEMBLE OIP Deploy - Deployment tooling for OIP-compatible models.
22
+
23
+ This module provides CLI tooling to generate deployment configurations
24
+ for any OIP-compatible model across multiple deployment targets:
25
+ - Local (MLServer)
26
+ - Docker
27
+ - Kubernetes (vanilla)
28
+ - KServe (serverless)
29
+
30
+ Custom generators can be added via the 'inference.generators' entry point group.
31
+ """
32
+
33
+ __version__ = "1.5.0.dev"
34
+
35
+ from .generators.base import Generator, ModelInfo
36
+ from .registry import GeneratorRegistry
37
+
38
+ __all__ = ["Generator", "GeneratorRegistry", "ModelInfo", "__version__"]
@@ -0,0 +1,278 @@
1
+ ###
2
+ # #%L
3
+ # aiSSEMBLE::Open Inference Protocol::Deploy
4
+ # %%
5
+ # Copyright (C) 2024 Booz Allen Hamilton Inc.
6
+ # %%
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ # #L%
19
+ ###
20
+ """
21
+ CLI commands for inference-deploy.
22
+
23
+ Provides the `inference deploy` command group for generating deployment configurations.
24
+ Generators are discovered via entry points, allowing custom generators to be
25
+ installed as separate packages.
26
+ """
27
+
28
+ from datetime import datetime, timezone
29
+ from pathlib import Path
30
+
31
+ import click
32
+
33
+ from . import __version__
34
+ from .config import DeployConfig
35
+ from .registry import GeneratorRegistry
36
+
37
+
38
+ def get_registry() -> GeneratorRegistry:
39
+ """Get the generator registry instance."""
40
+ return GeneratorRegistry.instance()
41
+
42
+
43
+ @click.group()
44
+ @click.version_option(version=__version__)
45
+ def main():
46
+ """Inference deployment tooling - generate deployment configs for OIP-compatible models."""
47
+ pass
48
+
49
+
50
+ @main.group()
51
+ def deploy():
52
+ """Generate and manage deployment configurations."""
53
+ pass
54
+
55
+
56
+ @deploy.command("init")
57
+ @click.option(
58
+ "--target",
59
+ "-t",
60
+ multiple=True,
61
+ default=None,
62
+ help="Deployment target(s) to generate configs for. Use 'all' for all targets.",
63
+ )
64
+ @click.option(
65
+ "--model-dir",
66
+ "-m",
67
+ type=click.Path(exists=True, path_type=Path),
68
+ default=None,
69
+ help="Path to models directory (default: ./models)",
70
+ )
71
+ @click.option(
72
+ "--output-dir",
73
+ "-o",
74
+ type=click.Path(path_type=Path),
75
+ default=None,
76
+ help="Output directory for generated configs (default: ./deploy)",
77
+ )
78
+ @click.option(
79
+ "--project-dir",
80
+ "-p",
81
+ type=click.Path(exists=True, path_type=Path),
82
+ default=None,
83
+ help="Project root directory (default: current directory)",
84
+ )
85
+ def init(
86
+ target: tuple[str, ...] | None,
87
+ model_dir: Path | None,
88
+ output_dir: Path | None,
89
+ project_dir: Path | None,
90
+ ):
91
+ """Initialize deployment configurations for your models.
92
+
93
+ Generates deployment configs in the deploy/ directory for the specified
94
+ target(s). Use --target all to generate for all available targets.
95
+
96
+ Generators are discovered via entry points, so custom generators can be
97
+ installed as separate packages.
98
+
99
+ Examples:
100
+
101
+ inference deploy init --target local
102
+
103
+ inference deploy init --target docker --target kubernetes
104
+
105
+ inference deploy init --target all
106
+ """
107
+ registry = get_registry()
108
+ available = registry.list_available()
109
+
110
+ if not available:
111
+ click.echo("Error: No generators found. Install a generator package or check")
112
+ click.echo("that aissemble-inference-deploy is installed correctly.")
113
+ raise SystemExit(1)
114
+
115
+ # Default to 'local' if available, otherwise first available
116
+ if target is None or len(target) == 0:
117
+ if "local" in available:
118
+ targets = ["local"]
119
+ else:
120
+ targets = [available[0]]
121
+ else:
122
+ targets = list(target)
123
+
124
+ # Expand 'all' to all available targets
125
+ if "all" in targets:
126
+ targets = available
127
+
128
+ # Kubernetes depends on Docker - auto-include if not present
129
+ if "kubernetes" in targets and "docker" not in targets:
130
+ click.echo("Note: Adding 'docker' target (required by kubernetes)")
131
+ targets.insert(0, "docker")
132
+
133
+ # KServe depends on Docker - auto-include if not present
134
+ if "kserve" in targets and "docker" not in targets:
135
+ click.echo("Note: Adding 'docker' target (required by kserve)")
136
+ targets.insert(0, "docker")
137
+
138
+ # Validate targets
139
+ for t in targets:
140
+ if t not in available and t != "all":
141
+ click.echo(f"Error: Unknown target '{t}'")
142
+ click.echo(f"Available targets: {', '.join(available)}")
143
+ raise SystemExit(1)
144
+
145
+ project_dir = project_dir or Path.cwd()
146
+ output_dir = output_dir or project_dir / "deploy"
147
+
148
+ # Check if running from wrong directory (inside deploy/)
149
+ cwd = Path.cwd()
150
+ if cwd.name == "deploy" and project_dir == cwd:
151
+ click.echo(
152
+ "Error: It looks like you're running from inside a deploy/ directory."
153
+ )
154
+ click.echo(
155
+ "Please run from your project root (where pyproject.toml and models/ are)."
156
+ )
157
+ click.echo()
158
+ click.echo("Example:")
159
+ click.echo(" cd /path/to/your-project")
160
+ click.echo(" inference deploy init --target docker")
161
+ raise SystemExit(1)
162
+
163
+ # Check for project root indicators
164
+ has_pyproject = (project_dir / "pyproject.toml").exists()
165
+ has_models = (project_dir / "models").exists()
166
+ if not has_pyproject and not has_models:
167
+ click.echo(
168
+ f"Warning: No pyproject.toml or models/ directory found in {project_dir}"
169
+ )
170
+ click.echo("Are you running from your project root?")
171
+ click.echo()
172
+ if not click.confirm("Continue anyway?"):
173
+ raise SystemExit(1)
174
+
175
+ # Load or create config
176
+ config_path = output_dir / ".inference-deploy.yaml"
177
+ config = DeployConfig.load(config_path)
178
+ config.generator_version = __version__
179
+ config.generated_at = datetime.now(timezone.utc).isoformat()
180
+
181
+ click.echo(f"Generating deployment configs in {output_dir}")
182
+ click.echo(f"Targets: {', '.join(targets)}")
183
+ click.echo()
184
+
185
+ all_generated_files = []
186
+
187
+ for target_name in targets:
188
+ generator_cls = registry.get(target_name)
189
+ if generator_cls is None:
190
+ click.echo(f" [{target_name}] Error: Generator not found")
191
+ continue
192
+
193
+ generator = generator_cls(project_dir, output_dir)
194
+
195
+ # Detect models
196
+ models_path = model_dir or project_dir / "models"
197
+ models = generator.detect_models(models_path)
198
+
199
+ if not models:
200
+ click.echo(f" [{target_name}] Warning: No models found in {models_path}")
201
+ else:
202
+ model_names = ", ".join(m.name for m in models)
203
+ click.echo(f" [{target_name}] Found models: {model_names}")
204
+
205
+ # Generate configs
206
+ generated_files = generator.generate(models)
207
+ all_generated_files.extend(generated_files)
208
+
209
+ for file_path in generated_files:
210
+ config.add_file(file_path, target_name, output_dir)
211
+ rel_path = file_path.relative_to(output_dir)
212
+ click.echo(f" [{target_name}] Generated: {rel_path}")
213
+
214
+ if target_name not in config.targets:
215
+ config.targets.append(target_name)
216
+
217
+ # Save config
218
+ config.save(config_path)
219
+ click.echo()
220
+ click.echo(f"Config saved to {config_path.relative_to(project_dir)}")
221
+
222
+ # Print next steps
223
+ click.echo()
224
+ click.echo("Next steps:")
225
+ if "local" in targets:
226
+ click.echo(" Local: cd deploy/local && ./run-mlserver.sh")
227
+ if "kubernetes" in targets:
228
+ # Kubernetes workflow: build image, then deploy
229
+ click.echo(" K8s: cd deploy/docker && docker-compose build")
230
+ click.echo(" kubectl apply -k deploy/kubernetes/overlays/dev")
231
+ elif "docker" in targets:
232
+ # Docker-only workflow: build and run
233
+ click.echo(" Docker: cd deploy/docker && docker-compose up --build")
234
+ if "kserve" in targets:
235
+ click.echo(" KServe: kubectl apply -f deploy/kserve/serving-runtime.yaml")
236
+ click.echo(" kubectl apply -f deploy/kserve/inference-service.yaml")
237
+
238
+
239
+ @deploy.command("list-targets")
240
+ def list_targets():
241
+ """List available deployment targets.
242
+
243
+ Generators are discovered via entry points. Install additional generator
244
+ packages to add more targets.
245
+ """
246
+ registry = get_registry()
247
+ available = registry.list_available()
248
+
249
+ if not available:
250
+ click.echo("No generators found.")
251
+ click.echo()
252
+ click.echo(
253
+ "Install a generator package or check that aissemble-inference-deploy"
254
+ )
255
+ click.echo("is installed correctly.")
256
+ return
257
+
258
+ click.echo("Available deployment targets:")
259
+ click.echo()
260
+ for name in available:
261
+ generator_cls = registry.get(name)
262
+ # Get description from docstring if available
263
+ doc = generator_cls.__doc__ if generator_cls else None
264
+ if doc:
265
+ # Get first line of docstring
266
+ desc = doc.strip().split("\n")[0]
267
+ click.echo(f" {name:15} - {desc}")
268
+ else:
269
+ click.echo(f" {name}")
270
+
271
+ click.echo()
272
+ click.echo(
273
+ "Custom generators can be added via the 'inference.generators' entry point."
274
+ )
275
+
276
+
277
+ if __name__ == "__main__":
278
+ main()
@@ -0,0 +1,182 @@
1
+ ###
2
+ # #%L
3
+ # aiSSEMBLE::Open Inference Protocol::Deploy
4
+ # %%
5
+ # Copyright (C) 2024 Booz Allen Hamilton Inc.
6
+ # %%
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ # #L%
19
+ ###
20
+ """
21
+ Configuration management for oip-deploy.
22
+
23
+ Manages the .inference-deploy.yaml file that tracks generated configs,
24
+ versions, and checksums for update/merge functionality.
25
+ """
26
+
27
+ import hashlib
28
+ import re
29
+ import sys
30
+ from dataclasses import dataclass, field
31
+ from datetime import datetime, timezone
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ import yaml
36
+
37
+
38
+ @dataclass
39
+ class GeneratedFile:
40
+ """Tracks a single generated file."""
41
+
42
+ path: str
43
+ checksum: str
44
+ generator: str
45
+ generated_at: str
46
+
47
+ def __post_init__(self):
48
+ """Validate checksum format."""
49
+ if not re.match(r"^[a-f0-9]{64}$", self.checksum):
50
+ raise ValueError(
51
+ f"Invalid checksum format (expected SHA256 hex): {self.checksum}"
52
+ )
53
+
54
+
55
+ @dataclass
56
+ class DeployConfig:
57
+ """Configuration stored in .inference-deploy.yaml."""
58
+
59
+ version: str = "1.0"
60
+ generator_version: str = ""
61
+ generated_at: str = ""
62
+ targets: list[str] = field(default_factory=list)
63
+ files: list[GeneratedFile] = field(default_factory=list)
64
+
65
+ @classmethod
66
+ def load(cls, path: Path) -> "DeployConfig":
67
+ """
68
+ Load config from a YAML file.
69
+
70
+ Args:
71
+ path: Path to .inference-deploy.yaml file
72
+
73
+ Returns:
74
+ DeployConfig instance
75
+
76
+ Raises:
77
+ ValueError: If YAML is malformed
78
+ """
79
+ if not path.exists():
80
+ return cls()
81
+
82
+ try:
83
+ content = path.read_text(encoding="utf-8")
84
+ data = yaml.safe_load(content)
85
+ except yaml.YAMLError as e:
86
+ raise ValueError(f"Invalid YAML in {path}: {e}")
87
+ except OSError as e:
88
+ raise ValueError(f"Cannot read {path}: {e}")
89
+
90
+ if data is None:
91
+ return cls()
92
+
93
+ if not isinstance(data, dict):
94
+ raise ValueError(f"Expected dict in {path}, got {type(data).__name__}")
95
+
96
+ # Validate and load files with proper error handling
97
+ files = []
98
+ for file_data in data.get("files", []):
99
+ if not isinstance(file_data, dict):
100
+ print(
101
+ f"Warning: Skipping invalid file entry (not a dict): {file_data}",
102
+ file=sys.stderr,
103
+ )
104
+ continue
105
+
106
+ try:
107
+ files.append(GeneratedFile(**file_data))
108
+ except TypeError as e:
109
+ print(
110
+ f"Warning: Skipping invalid file entry: {e}",
111
+ file=sys.stderr,
112
+ )
113
+ except ValueError as e:
114
+ print(
115
+ f"Warning: Skipping file entry with invalid checksum: {e}",
116
+ file=sys.stderr,
117
+ )
118
+
119
+ return cls(
120
+ version=str(data.get("version", "1.0")),
121
+ generator_version=str(data.get("generator_version", "")),
122
+ generated_at=str(data.get("generated_at", "")),
123
+ targets=list(data.get("targets", [])),
124
+ files=files,
125
+ )
126
+
127
+ def save(self, path: Path) -> None:
128
+ """Save config to a YAML file."""
129
+ data: dict[str, Any] = {
130
+ "version": self.version,
131
+ "generator_version": self.generator_version,
132
+ "generated_at": self.generated_at,
133
+ "targets": self.targets,
134
+ "files": [
135
+ {
136
+ "path": f.path,
137
+ "checksum": f.checksum,
138
+ "generator": f.generator,
139
+ "generated_at": f.generated_at,
140
+ }
141
+ for f in self.files
142
+ ],
143
+ }
144
+ path.parent.mkdir(parents=True, exist_ok=True)
145
+ path.write_text(
146
+ yaml.dump(data, default_flow_style=False, sort_keys=False),
147
+ encoding="utf-8",
148
+ )
149
+
150
+ def add_file(self, path: Path, generator: str, base_dir: Path) -> None:
151
+ """Add or update a tracked file."""
152
+ rel_path = str(path.relative_to(base_dir))
153
+ checksum = compute_checksum(path)
154
+ now = datetime.now(timezone.utc).isoformat()
155
+
156
+ # Update existing or add new
157
+ for f in self.files:
158
+ if f.path == rel_path:
159
+ f.checksum = checksum
160
+ f.generated_at = now
161
+ return
162
+
163
+ self.files.append(
164
+ GeneratedFile(
165
+ path=rel_path,
166
+ checksum=checksum,
167
+ generator=generator,
168
+ generated_at=now,
169
+ )
170
+ )
171
+
172
+ def get_file(self, rel_path: str) -> GeneratedFile | None:
173
+ """Get a tracked file by its relative path."""
174
+ for f in self.files:
175
+ if f.path == rel_path:
176
+ return f
177
+ return None
178
+
179
+
180
+ def compute_checksum(path: Path) -> str:
181
+ """Compute SHA256 checksum of a file."""
182
+ return hashlib.sha256(path.read_bytes()).hexdigest()
@@ -0,0 +1,36 @@
1
+ ###
2
+ # #%L
3
+ # aiSSEMBLE::Open Inference Protocol::Modules::deploy
4
+ # %%
5
+ # Copyright (C) 2024 Booz Allen Hamilton Inc.
6
+ # %%
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ # #L%
19
+ ###
20
+ """
21
+ Deployment config generators for different targets.
22
+ """
23
+
24
+ from .base import Generator
25
+ from .docker import DockerGenerator
26
+ from .kserve import KServeGenerator
27
+ from .kubernetes import KubernetesGenerator
28
+ from .local import LocalGenerator
29
+
30
+ __all__ = [
31
+ "Generator",
32
+ "DockerGenerator",
33
+ "KServeGenerator",
34
+ "KubernetesGenerator",
35
+ "LocalGenerator",
36
+ ]