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 +26 -0
- lfx_ibm/components/ibm/__init__.py +9 -0
- lfx_ibm/components/ibm/db2_security.py +272 -0
- lfx_ibm/components/ibm/db2_vector.py +458 -0
- lfx_ibm/components/ibm/db2vs.py +1041 -0
- lfx_ibm/components/ibm/watsonx.py +224 -0
- lfx_ibm/components/ibm/watsonx_embeddings.py +143 -0
- lfx_ibm/extension.json +16 -0
- lfx_ibm_nightly-0.1.0.dev57.dist-info/METADATA +83 -0
- lfx_ibm_nightly-0.1.0.dev57.dist-info/RECORD +12 -0
- lfx_ibm_nightly-0.1.0.dev57.dist-info/WHEEL +4 -0
- lfx_ibm_nightly-0.1.0.dev57.dist-info/entry_points.txt +2 -0
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,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
|