lfx-ibm-nightly 0.1.0.dev57__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.
lfx_ibm/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ """lfx-ibm: IBM bundle (Db2 Vector Store + watsonx.ai LLM and embeddings).
2
+
3
+ This package is the distribution unit ``lfx-ibm``. At runtime
4
+ Langflow's loader discovers ``extension.json`` shipped alongside this
5
+ ``__init__.py`` and registers the three IBM components under the
6
+ namespaced IDs:
7
+
8
+ * ``ext:ibm:DB2VectorStoreComponent@official``
9
+ * ``ext:ibm:WatsonxAIComponent@official``
10
+ * ``ext:ibm:WatsonxEmbeddingsComponent@official``
11
+
12
+ Third pilot port (after lfx-duckduckgo and lfx-arxiv) -- exercises the
13
+ same extraction recipe documented in ``src/bundles/PORTING.md`` against
14
+ a multi-component bundle that ships a langchain-community-backed vector
15
+ store, the IBM Db2 vendor SDK, and the langchain-ibm watsonx.ai client.
16
+ """
17
+
18
+ from lfx_ibm.components.ibm.db2_vector import DB2VectorStoreComponent
19
+ from lfx_ibm.components.ibm.watsonx import WatsonxAIComponent
20
+ from lfx_ibm.components.ibm.watsonx_embeddings import WatsonxEmbeddingsComponent
21
+
22
+ __all__ = [
23
+ "DB2VectorStoreComponent",
24
+ "WatsonxAIComponent",
25
+ "WatsonxEmbeddingsComponent",
26
+ ]
@@ -0,0 +1,9 @@
1
+ from .db2_vector import DB2VectorStoreComponent
2
+ from .watsonx import WatsonxAIComponent
3
+ from .watsonx_embeddings import WatsonxEmbeddingsComponent
4
+
5
+ __all__ = [
6
+ "DB2VectorStoreComponent",
7
+ "WatsonxAIComponent",
8
+ "WatsonxEmbeddingsComponent",
9
+ ]
@@ -0,0 +1,272 @@
1
+ """Lightweight validation and safe error helpers for IBM Db2 components."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import tempfile
8
+ from pathlib import Path
9
+ from urllib.parse import urlparse
10
+
11
+ _IDENTIFIER_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9_]{0,127}$")
12
+ _HOSTNAME_PATTERN = re.compile(r"^[A-Za-z0-9.-]{1,253}$")
13
+
14
+
15
+ def _require_string(value: object, field_name: str) -> str:
16
+ """Return a stripped string or raise TypeError."""
17
+ if not isinstance(value, str):
18
+ msg = f"Invalid {field_name}: must be a string"
19
+ raise TypeError(msg)
20
+
21
+ cleaned = value.strip()
22
+ if not cleaned:
23
+ msg = f"Invalid {field_name}: cannot be empty"
24
+ raise ValueError(msg)
25
+ return cleaned
26
+
27
+
28
+ # Maximum length for Db2 database names
29
+ _MAX_DB_NAME_LENGTH = 128
30
+
31
+
32
+ def validate_database_name(value: object) -> str:
33
+ """Validate a Db2 database name."""
34
+ database_name = _require_string(value, "database name")
35
+ if len(database_name) > _MAX_DB_NAME_LENGTH:
36
+ msg = "Invalid database name: exceeds maximum length"
37
+ raise ValueError(msg)
38
+ if any(char in database_name for char in ('"', "'", ";", "\\", "\n", "\r", "\t")):
39
+ msg = "Invalid database name: contains unsafe characters"
40
+ raise ValueError(msg)
41
+ return database_name
42
+
43
+
44
+ def validate_hostname(value: object) -> str:
45
+ """Validate a hostname or IP-like address."""
46
+ hostname = _require_string(value, "hostname")
47
+ if any(char in hostname for char in ('"', "'", ";", "\\", "/", "?", "#", "\n", "\r", "\t", " ")):
48
+ msg = "Invalid hostname: contains unsafe characters"
49
+ raise ValueError(msg)
50
+ if not _HOSTNAME_PATTERN.fullmatch(hostname):
51
+ msg = "Invalid hostname: contains unsupported characters"
52
+ raise ValueError(msg)
53
+ if ".." in hostname or hostname.startswith((".", "-")) or hostname.endswith("."):
54
+ msg = "Invalid hostname: malformed hostname"
55
+ raise ValueError(msg)
56
+ return hostname
57
+
58
+
59
+ # Valid TCP port range
60
+ _MIN_PORT = 1
61
+ _MAX_PORT = 65535
62
+
63
+
64
+ def validate_port(value: object) -> int:
65
+ """Validate a TCP port number."""
66
+ if isinstance(value, bool) or not isinstance(value, int):
67
+ msg = "Invalid port: must be an integer"
68
+ raise TypeError(msg)
69
+ if value < _MIN_PORT or value > _MAX_PORT:
70
+ msg = f"Invalid port: must be between {_MIN_PORT} and {_MAX_PORT}"
71
+ raise ValueError(msg)
72
+ return value
73
+
74
+
75
+ def validate_identifier(value: object, field_name: str = "identifier") -> str:
76
+ """Validate a SQL identifier used for table names."""
77
+ identifier = _require_string(value, field_name)
78
+ if not _IDENTIFIER_PATTERN.fullmatch(identifier):
79
+ msg = f"Invalid {field_name}: use letters, numbers, and underscores only, starting with a letter"
80
+ raise ValueError(msg)
81
+ return identifier
82
+
83
+
84
+ def get_quoted_identifier(identifier: str) -> str:
85
+ """Return a properly quoted SQL identifier for Db2.
86
+
87
+ Args:
88
+ identifier: The identifier to quote (already validated)
89
+
90
+ Returns:
91
+ The quoted identifier safe for SQL queries
92
+ """
93
+ # Db2 uses double quotes for identifiers
94
+ # Escape any existing double quotes by doubling them
95
+ escaped = identifier.replace('"', '""')
96
+ return f'"{escaped}"'
97
+
98
+
99
+ def sanitize_sql_string(value: str) -> str:
100
+ """Sanitize a string value for use in SQL queries.
101
+
102
+ Args:
103
+ value: The string value to sanitize
104
+
105
+ Returns:
106
+ The sanitized string with dangerous characters escaped
107
+ """
108
+ # Escape single quotes by doubling them (SQL standard)
109
+ return value.replace("'", "''")
110
+
111
+
112
+ def create_safe_error_message(error: Exception, context: str | None = None) -> str:
113
+ """Create a sanitized error message without exposing connection details."""
114
+ error_text = str(error).strip() or "Unknown error"
115
+ redacted_text = error_text
116
+
117
+ sensitive_patterns = [
118
+ r"PWD=[^; ]+",
119
+ r"PASSWORD=[^; ]+",
120
+ r"UID=[^; ]+",
121
+ r"USER(ID)?=[^; ]+",
122
+ r"HOSTNAME=[^; ]+",
123
+ r"DATABASE=[^; ]+",
124
+ r"PORT=[^; ]+",
125
+ ]
126
+ for pattern in sensitive_patterns:
127
+ redacted_text = re.sub(pattern, "[REDACTED]", redacted_text, flags=re.IGNORECASE)
128
+
129
+ prefix = "DB2 operation failed"
130
+ if context:
131
+ prefix = f"{prefix} {context}"
132
+
133
+ return f"{prefix}: {redacted_text}"
134
+
135
+
136
+ def validate_ssl_certificate_path(cert_path: str | None) -> tuple[str | None, str | None]:
137
+ """Validate and resolve SSL certificate path.
138
+
139
+ Args:
140
+ cert_path: Path to certificate file (local path or URL), or None
141
+
142
+ Returns:
143
+ Tuple of (resolved_path, error_message)
144
+ - If valid: (resolved_path, None)
145
+ - If invalid: (None, error_message)
146
+ - If None/empty: (None, None) - indicates use system defaults
147
+
148
+ Raises:
149
+ ValueError: If certificate path is invalid or file doesn't exist
150
+ """
151
+ if not cert_path or not cert_path.strip():
152
+ # Empty path means use system defaults
153
+ return None, None
154
+
155
+ cert_path = cert_path.strip()
156
+
157
+ # Check if it's a URL
158
+ parsed = urlparse(cert_path)
159
+ if parsed.scheme in ("http", "https"):
160
+ # URL-based certificate - will be downloaded later
161
+ return cert_path, None
162
+
163
+ # Local file path - validate it exists and is readable
164
+ try:
165
+ # Resolve path (handles relative paths, ~, etc.)
166
+ resolved_path = Path(cert_path).expanduser().resolve()
167
+
168
+ # Check if file exists
169
+ if not resolved_path.exists():
170
+ return None, f"Certificate file not found: {cert_path}"
171
+
172
+ # Check if it's a file (not a directory)
173
+ if not resolved_path.is_file():
174
+ return None, f"Certificate path is not a file: {cert_path}"
175
+
176
+ # Check if file is readable
177
+ if not os.access(resolved_path, os.R_OK):
178
+ return None, f"Certificate file is not readable: {cert_path}"
179
+
180
+ # Validate file extension
181
+ valid_extensions = {".crt", ".pem", ".cer", ".cert"}
182
+ if resolved_path.suffix.lower() not in valid_extensions:
183
+ return (
184
+ None,
185
+ f"Invalid certificate file extension: {resolved_path.suffix}. "
186
+ f"Expected one of: {', '.join(valid_extensions)}",
187
+ )
188
+
189
+ return str(resolved_path), None
190
+
191
+ except (OSError, ValueError) as e:
192
+ return None, f"Error validating certificate path: {e}"
193
+
194
+
195
+ def download_certificate(url: str) -> tuple[str | None, str | None]:
196
+ """Download SSL certificate from URL to temporary file.
197
+
198
+ Args:
199
+ url: URL to download certificate from
200
+
201
+ Returns:
202
+ Tuple of (temp_file_path, error_message)
203
+ - If successful: (temp_file_path, None)
204
+ - If failed: (None, error_message)
205
+ """
206
+ try:
207
+ import urllib.error
208
+ import urllib.request
209
+
210
+ # Create temporary file with appropriate extension
211
+ temp_fd, temp_path = tempfile.mkstemp(suffix=".crt", prefix="db2_ssl_")
212
+
213
+ try:
214
+ # Download certificate
215
+ with urllib.request.urlopen(url, timeout=30) as response: # noqa: S310
216
+ cert_data = response.read()
217
+
218
+ # Write to temporary file
219
+ with os.fdopen(temp_fd, "wb") as f:
220
+ f.write(cert_data)
221
+
222
+ except (OSError, ValueError, urllib.error.URLError) as download_error:
223
+ # Clean up temp file on error
224
+ try:
225
+ os.close(temp_fd)
226
+ Path(temp_path).unlink(missing_ok=True)
227
+ except OSError:
228
+ # Ignore cleanup errors
229
+ pass
230
+ return None, f"Failed to download certificate from {url}: {download_error}"
231
+ else:
232
+ return temp_path, None
233
+
234
+ except (OSError, ValueError) as e:
235
+ return None, f"Error setting up certificate download: {e}"
236
+
237
+
238
+ def validate_and_prepare_ssl_certificate(cert_path: str | None) -> tuple[str | None, bool, str | None]:
239
+ """Validate and prepare SSL certificate for use.
240
+
241
+ Args:
242
+ cert_path: Path to certificate file (local path or URL), or None
243
+
244
+ Returns:
245
+ Tuple of (resolved_path, is_temp_file, error_message)
246
+ - resolved_path: Path to use for SSL connection (None means use system defaults)
247
+ - is_temp_file: True if the file is temporary and should be cleaned up
248
+ - error_message: Error message if validation failed, None otherwise
249
+ """
250
+ if not cert_path or not cert_path.strip():
251
+ # No certificate provided - use system defaults
252
+ return None, False, None
253
+
254
+ # First validate the path/URL
255
+ validated_path, error = validate_ssl_certificate_path(cert_path)
256
+ if error:
257
+ return None, False, error
258
+
259
+ # Check if it's a URL that needs downloading
260
+ parsed = urlparse(cert_path)
261
+ if parsed.scheme in ("http", "https"):
262
+ # Download certificate to temporary file
263
+ temp_path, download_error = download_certificate(cert_path)
264
+ if download_error:
265
+ return None, False, download_error
266
+ return temp_path, True, None
267
+
268
+ # Local file path - already validated
269
+ return validated_path, False, None
270
+
271
+
272
+ # Made with Bob