vlmparse 0.1.8__py3-none-any.whl → 0.1.9__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.
@@ -0,0 +1,226 @@
1
+ import getpass
2
+ import time
3
+ from contextlib import contextmanager
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ import docker
8
+ from loguru import logger
9
+
10
+ if TYPE_CHECKING:
11
+ from .base_server import BaseServerConfig
12
+
13
+
14
+ def _ensure_image_exists(
15
+ client: docker.DockerClient,
16
+ image: str,
17
+ dockerfile_path: Path,
18
+ ):
19
+ """Check if image exists, build it if not."""
20
+ try:
21
+ client.images.get(image)
22
+ logger.info(f"Docker image {image} found")
23
+ return
24
+ except docker.errors.ImageNotFound:
25
+ logger.info(f"Docker image {image} not found, building...")
26
+
27
+ if not dockerfile_path.exists():
28
+ raise FileNotFoundError(
29
+ f"Dockerfile directory not found at {dockerfile_path}"
30
+ ) from None
31
+
32
+ logger.info(f"Building image from {dockerfile_path}")
33
+
34
+ # Use low-level API for real-time streaming
35
+ api_client = docker.APIClient(base_url="unix://var/run/docker.sock")
36
+
37
+ # Build the image with streaming
38
+ build_stream = api_client.build(
39
+ path=str(dockerfile_path),
40
+ tag=image,
41
+ rm=True,
42
+ decode=True, # Automatically decode JSON responses to dict
43
+ )
44
+
45
+ # Stream build logs in real-time
46
+ for chunk in build_stream:
47
+ if "stream" in chunk:
48
+ for line in chunk["stream"].splitlines():
49
+ logger.info(line)
50
+ elif "error" in chunk:
51
+ logger.error(chunk["error"])
52
+ raise docker.errors.BuildError(chunk["error"], build_stream) from None
53
+ elif "status" in chunk:
54
+ # Handle status updates (e.g., downloading layers)
55
+ logger.debug(chunk["status"])
56
+
57
+ logger.info(f"Successfully built image {image}")
58
+
59
+
60
+ @contextmanager
61
+ def docker_server(
62
+ config: "BaseServerConfig",
63
+ timeout: int = 1000,
64
+ cleanup: bool = True,
65
+ ):
66
+ """Generic context manager for Docker server deployment.
67
+
68
+ Args:
69
+ config: BaseServerConfig (can be any subclass like DockerServerConfig or VLLMDockerServerConfig)
70
+ timeout: Timeout in seconds to wait for server to be ready
71
+ cleanup: If True, stop and remove container on exit. If False, leave container running
72
+
73
+ Yields:
74
+ tuple: (base_url, container) - The base URL of the server and the Docker container object
75
+ """
76
+
77
+ client = docker.from_env()
78
+ container = None
79
+
80
+ try:
81
+ # Ensure image exists
82
+ logger.info(f"Checking for Docker image {config.docker_image}...")
83
+
84
+ if config.dockerfile_dir is not None:
85
+ _ensure_image_exists(
86
+ client, config.docker_image, Path(config.dockerfile_dir)
87
+ )
88
+ else:
89
+ # Pull pre-built image
90
+ try:
91
+ client.images.get(config.docker_image)
92
+ logger.info(f"Docker image {config.docker_image} found locally")
93
+ except docker.errors.ImageNotFound:
94
+ logger.info(
95
+ f"Docker image {config.docker_image} not found locally, pulling..."
96
+ )
97
+ client.images.pull(config.docker_image)
98
+ logger.info(f"Successfully pulled {config.docker_image}")
99
+
100
+ logger.info(
101
+ f"Starting Docker container for {config.model_name} on port {config.docker_port}"
102
+ )
103
+
104
+ # Configure GPU access
105
+ device_requests = None
106
+
107
+ if config.gpu_device_ids is None:
108
+ # Default: Try to use all GPUs if available
109
+ device_requests = [
110
+ docker.types.DeviceRequest(count=-1, capabilities=[["gpu"]])
111
+ ]
112
+ elif len(config.gpu_device_ids) > 0 and config.gpu_device_ids[0] != "":
113
+ # Use specific GPU devices
114
+ device_requests = [
115
+ docker.types.DeviceRequest(
116
+ device_ids=config.gpu_device_ids, capabilities=[["gpu"]]
117
+ )
118
+ ]
119
+ else:
120
+ # Empty list means CPU-only, no GPU
121
+ device_requests = None
122
+
123
+ # Use generic methods from config
124
+ command = config.get_command()
125
+ volumes = config.get_volumes()
126
+ environment = config.get_environment()
127
+ container_port = config.container_port
128
+ log_prefix = config.model_name
129
+
130
+ # Construct URI for label
131
+ uri = f"http://localhost:{config.docker_port}{config.get_base_url_suffix()}"
132
+
133
+ # Determine GPU label
134
+ if config.gpu_device_ids is None:
135
+ gpu_label = "0"
136
+ elif len(config.gpu_device_ids) == 0 or (
137
+ len(config.gpu_device_ids) == 1 and config.gpu_device_ids[0] == ""
138
+ ):
139
+ gpu_label = "cpu"
140
+ else:
141
+ gpu_label = ",".join(config.gpu_device_ids)
142
+
143
+ # Start container
144
+ container_kwargs = {
145
+ "image": config.docker_image,
146
+ "ports": {f"{container_port}/tcp": config.docker_port},
147
+ "detach": True,
148
+ "remove": True,
149
+ "name": f"vlmparse-{config.model_name.replace('/', '-')}-{getpass.getuser()}",
150
+ "labels": {
151
+ "vlmparse_model_name": config.model_name,
152
+ "vlmparse_uri": uri,
153
+ "vlmparse_gpus": gpu_label,
154
+ },
155
+ }
156
+
157
+ if device_requests is not None:
158
+ container_kwargs["device_requests"] = device_requests
159
+ if command:
160
+ container_kwargs["command"] = command
161
+ if environment:
162
+ container_kwargs["environment"] = environment
163
+ if volumes:
164
+ container_kwargs["volumes"] = volumes
165
+ if config.entrypoint:
166
+ container_kwargs["entrypoint"] = config.entrypoint
167
+
168
+ container = client.containers.run(**container_kwargs)
169
+
170
+ logger.info(
171
+ f"Container {container.short_id} started, waiting for server to be ready..."
172
+ )
173
+
174
+ # Wait for server to be ready
175
+ start_time = time.time()
176
+ server_ready = False
177
+ last_log_position = 0
178
+
179
+ while time.time() - start_time < timeout:
180
+ try:
181
+ container.reload()
182
+ except docker.errors.NotFound as e:
183
+ logger.error("Container stopped unexpectedly during startup")
184
+ raise RuntimeError(
185
+ "Container crashed during initialization. Check Docker logs for details."
186
+ ) from e
187
+
188
+ if container.status == "running":
189
+ # Get all logs and display new ones
190
+ all_logs = container.logs().decode("utf-8")
191
+
192
+ # Display new log lines
193
+ if len(all_logs) > last_log_position:
194
+ new_logs = all_logs[last_log_position:]
195
+ for line in new_logs.splitlines():
196
+ if line.strip(): # Only print non-empty lines
197
+ logger.info(f"[{log_prefix}] {line}")
198
+ last_log_position = len(all_logs)
199
+
200
+ # Check if server is ready
201
+ for indicator in config.server_ready_indicators:
202
+ if indicator in all_logs:
203
+ server_ready = True
204
+ if server_ready:
205
+ logger.info(f"Server ready indicator '{indicator}' found in logs")
206
+ break
207
+
208
+ time.sleep(2)
209
+
210
+ if not server_ready:
211
+ raise TimeoutError(f"Server did not become ready within {timeout} seconds")
212
+
213
+ # Build base URL using config's suffix method
214
+ base_url = (
215
+ f"http://localhost:{config.docker_port}{config.get_base_url_suffix()}"
216
+ )
217
+
218
+ logger.info(f"{log_prefix} server ready at {base_url}")
219
+
220
+ yield base_url, container
221
+
222
+ finally:
223
+ if cleanup and container:
224
+ logger.info(f"Stopping container {container.short_id}")
225
+ container.stop(timeout=10)
226
+ logger.info("Container stopped")
@@ -1,52 +1,28 @@
1
1
  import os
2
- from typing import Callable
3
2
 
4
- from loguru import logger
5
3
  from pydantic import Field
6
4
 
7
- from .model_identity import ModelIdentityMixin
8
- from .utils import docker_server
5
+ from .base_server import BaseServer, BaseServerConfig
6
+ from .docker_run_deployment import docker_server
9
7
 
10
8
 
11
- class DockerServerConfig(ModelIdentityMixin):
12
- """Base configuration for deploying a Docker server.
9
+ class DockerServerConfig(BaseServerConfig):
10
+ """Configuration for deploying a Docker server.
13
11
 
14
- Inherits from ModelIdentityMixin which provides:
15
- - model_name: str
16
- - default_model_name: str | None
17
- - aliases: list[str]
18
- - _create_client_kwargs(base_url): Helper for creating client configs
19
- - get_all_names(): All names this model can be referenced by
12
+ Inherits from BaseServerConfig which provides common server configuration.
20
13
  """
21
14
 
22
15
  docker_image: str
23
16
  dockerfile_dir: str | None = None
24
17
  command_args: list[str] = Field(default_factory=list)
25
- server_ready_indicators: list[str] = Field(
26
- default_factory=lambda: [
27
- "Application startup complete",
28
- "Uvicorn running",
29
- "Starting vLLM API server",
30
- ]
31
- )
32
- docker_port: int = 8056
33
- gpu_device_ids: list[str] | None = None
34
- container_port: int = 8000
35
- environment: dict[str, str] = Field(default_factory=dict)
36
18
  volumes: dict[str, dict] | None = None
37
19
  entrypoint: str | None = None
38
20
 
39
- class Config:
40
- extra = "allow"
41
-
42
21
  @property
43
22
  def client_config(self):
44
23
  """Override in subclasses to return appropriate client config."""
45
24
  raise NotImplementedError
46
25
 
47
- def get_client(self, **kwargs):
48
- return self.client_config.get_client(**kwargs)
49
-
50
26
  def get_server(self, auto_stop: bool = True):
51
27
  return ConverterServer(config=self, auto_stop=auto_stop)
52
28
 
@@ -71,14 +47,6 @@ class DockerServerConfig(ModelIdentityMixin):
71
47
  """Setup volumes for container. Override in subclasses for specific logic."""
72
48
  return self.volumes
73
49
 
74
- def get_environment(self) -> dict | None:
75
- """Setup environment variables. Override in subclasses for specific logic."""
76
- return self.environment if self.environment else None
77
-
78
- def get_base_url_suffix(self) -> str:
79
- """Return URL suffix (e.g., '/v1' for OpenAI-compatible APIs). Override in subclasses."""
80
- return ""
81
-
82
50
 
83
51
  DEFAULT_MODEL_NAME = "vllm-model"
84
52
 
@@ -144,93 +112,9 @@ class VLLMDockerServerConfig(DockerServerConfig):
144
112
  return "/v1"
145
113
 
146
114
 
147
- class ConverterServer:
115
+ class ConverterServer(BaseServer):
148
116
  """Manages Docker server lifecycle with start/stop methods."""
149
117
 
150
- def __init__(self, config: DockerServerConfig, auto_stop: bool = True):
151
- self.config = config
152
- self.auto_stop = auto_stop
153
- self._server_context = None
154
- self._container = None
155
- self.base_url = None
156
-
157
- def start(self):
158
- """Start the Docker server."""
159
- if self._server_context is not None:
160
- logger.warning("Server already started")
161
- return self.base_url, self._container
162
-
163
- # Use the generic docker_server for all server types
164
- self._server_context = docker_server(config=self.config, cleanup=self.auto_stop)
165
-
166
- self.base_url, self._container = self._server_context.__enter__()
167
- logger.info(f"Server started at {self.base_url}")
168
- logger.info(f"Container ID: {self._container.id}")
169
- logger.info(f"Container name: {self._container.name}")
170
- return self.base_url, self._container
171
-
172
- def stop(self):
173
- """Stop the Docker server."""
174
- if self._server_context is not None:
175
- try:
176
- self._server_context.__exit__(None, None, None)
177
- except Exception as e:
178
- logger.warning(f"Error during server cleanup: {e}")
179
- finally:
180
- self._server_context = None
181
- self._container = None
182
- self.base_url = None
183
- logger.info("Server stopped")
184
-
185
- def __del__(self):
186
- """Automatically stop server when object is destroyed if auto_stop is True.
187
-
188
- Note: This is a fallback mechanism. Prefer using the context manager
189
- or explicitly calling stop() for reliable cleanup.
190
- """
191
- try:
192
- if self.auto_stop and self._server_context is not None:
193
- self.stop()
194
- except Exception:
195
- pass # Suppress errors during garbage collection
196
-
197
-
198
- class DockerConfigRegistry:
199
- """Registry for mapping model names to their Docker configurations.
200
-
201
- Thread-safe registry that maps model names to their Docker configuration factories.
202
- """
203
-
204
- def __init__(self):
205
- import threading
206
-
207
- self._registry: dict[str, Callable[[], DockerServerConfig | None]] = {}
208
- self._lock = threading.RLock()
209
-
210
- def register(
211
- self, model_name: str, config_factory: Callable[[], DockerServerConfig | None]
212
- ):
213
- """Register a config factory for a model name (thread-safe)."""
214
- with self._lock:
215
- self._registry[model_name] = config_factory
216
-
217
- def get(self, model_name: str, default=False) -> DockerServerConfig | None:
218
- """Get config for a model name (thread-safe). Returns default if not registered."""
219
- with self._lock:
220
- if model_name not in self._registry:
221
- if default:
222
- return VLLMDockerServerConfig(
223
- model_name=model_name, default_model_name=DEFAULT_MODEL_NAME
224
- )
225
- return None
226
- factory = self._registry[model_name]
227
- return factory()
228
-
229
- def list_models(self) -> list[str]:
230
- """List all registered model names (thread-safe)."""
231
- with self._lock:
232
- return list(self._registry.keys())
233
-
234
-
235
- # Global registry instance
236
- docker_config_registry = DockerConfigRegistry()
118
+ def _create_server_context(self):
119
+ """Create the Docker server context."""
120
+ return docker_server(config=self.config, cleanup=self.auto_stop)
@@ -0,0 +1,42 @@
1
+ from typing import Callable
2
+
3
+ from vlmparse.servers.base_server import BaseServerConfig
4
+
5
+
6
+ class DockerConfigRegistry:
7
+ """Registry for mapping model names to their Docker configurations.
8
+
9
+ Thread-safe registry that maps model names to their Docker configuration factories.
10
+ """
11
+
12
+ def __init__(self):
13
+ import threading
14
+
15
+ self._registry: dict[str, Callable[[], BaseServerConfig | None]] = {}
16
+ self._lock = threading.RLock()
17
+
18
+ def register(
19
+ self,
20
+ model_name: str,
21
+ config_factory: Callable[[], BaseServerConfig | None],
22
+ ):
23
+ """Register a config factory for a model name (thread-safe)."""
24
+ with self._lock:
25
+ self._registry[model_name] = config_factory
26
+
27
+ def get(self, model_name: str) -> BaseServerConfig | None:
28
+ """Get config for a model name (thread-safe). Returns None if not registered."""
29
+ with self._lock:
30
+ if model_name not in self._registry:
31
+ return None
32
+ factory = self._registry[model_name]
33
+ return factory()
34
+
35
+ def list_models(self) -> list[str]:
36
+ """List all registered model names (thread-safe)."""
37
+ with self._lock:
38
+ return list(self._registry.keys())
39
+
40
+
41
+ # Global registry instance
42
+ docker_config_registry = DockerConfigRegistry()