graflo 1.3.3__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.
- graflo/README.md +18 -0
- graflo/__init__.py +70 -0
- graflo/architecture/__init__.py +38 -0
- graflo/architecture/actor.py +1120 -0
- graflo/architecture/actor_util.py +450 -0
- graflo/architecture/edge.py +297 -0
- graflo/architecture/onto.py +374 -0
- graflo/architecture/resource.py +161 -0
- graflo/architecture/schema.py +136 -0
- graflo/architecture/transform.py +292 -0
- graflo/architecture/util.py +93 -0
- graflo/architecture/vertex.py +586 -0
- graflo/caster.py +655 -0
- graflo/cli/__init__.py +14 -0
- graflo/cli/ingest.py +194 -0
- graflo/cli/manage_dbs.py +197 -0
- graflo/cli/plot_schema.py +132 -0
- graflo/cli/xml2json.py +93 -0
- graflo/data_source/__init__.py +48 -0
- graflo/data_source/api.py +339 -0
- graflo/data_source/base.py +97 -0
- graflo/data_source/factory.py +298 -0
- graflo/data_source/file.py +133 -0
- graflo/data_source/memory.py +72 -0
- graflo/data_source/registry.py +82 -0
- graflo/data_source/sql.py +185 -0
- graflo/db/__init__.py +44 -0
- graflo/db/arango/__init__.py +22 -0
- graflo/db/arango/conn.py +1026 -0
- graflo/db/arango/query.py +180 -0
- graflo/db/arango/util.py +88 -0
- graflo/db/conn.py +377 -0
- graflo/db/connection/__init__.py +6 -0
- graflo/db/connection/config_mapping.py +18 -0
- graflo/db/connection/onto.py +688 -0
- graflo/db/connection/wsgi.py +29 -0
- graflo/db/manager.py +119 -0
- graflo/db/neo4j/__init__.py +16 -0
- graflo/db/neo4j/conn.py +639 -0
- graflo/db/postgres/__init__.py +156 -0
- graflo/db/postgres/conn.py +425 -0
- graflo/db/postgres/resource_mapping.py +139 -0
- graflo/db/postgres/schema_inference.py +245 -0
- graflo/db/postgres/types.py +148 -0
- graflo/db/tigergraph/__init__.py +9 -0
- graflo/db/tigergraph/conn.py +2212 -0
- graflo/db/util.py +49 -0
- graflo/filter/__init__.py +21 -0
- graflo/filter/onto.py +525 -0
- graflo/logging.conf +22 -0
- graflo/onto.py +190 -0
- graflo/plot/__init__.py +17 -0
- graflo/plot/plotter.py +556 -0
- graflo/util/__init__.py +23 -0
- graflo/util/chunker.py +751 -0
- graflo/util/merge.py +150 -0
- graflo/util/misc.py +37 -0
- graflo/util/onto.py +332 -0
- graflo/util/transform.py +448 -0
- graflo-1.3.3.dist-info/METADATA +190 -0
- graflo-1.3.3.dist-info/RECORD +64 -0
- graflo-1.3.3.dist-info/WHEEL +4 -0
- graflo-1.3.3.dist-info/entry_points.txt +5 -0
- graflo-1.3.3.dist-info/licenses/LICENSE +126 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from enum import EnumMeta
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from strenum import StrEnum
|
|
5
|
+
from typing import Any, Dict, Type, TypeVar
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
from pydantic import Field, model_validator
|
|
9
|
+
from pydantic import AliasChoices
|
|
10
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EnumMetaWithContains(EnumMeta):
|
|
14
|
+
"""Enhanced EnumMeta that supports 'in' operator checks."""
|
|
15
|
+
|
|
16
|
+
def __contains__(cls, item, **kwargs):
|
|
17
|
+
try:
|
|
18
|
+
cls(item, **kwargs)
|
|
19
|
+
except ValueError:
|
|
20
|
+
return False
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Type variable for DBConfig subclasses
|
|
25
|
+
T = TypeVar("T", bound="DBConfig")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DBType(StrEnum, metaclass=EnumMetaWithContains):
|
|
29
|
+
"""Enum representing different types of databases.
|
|
30
|
+
|
|
31
|
+
Includes both graph databases and source databases (SQL, NoSQL, etc.).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Graph databases
|
|
35
|
+
ARANGO = "arango"
|
|
36
|
+
NEO4J = "neo4j"
|
|
37
|
+
TIGERGRAPH = "tigergraph"
|
|
38
|
+
|
|
39
|
+
# Source databases (SQL, NoSQL)
|
|
40
|
+
POSTGRES = "postgres"
|
|
41
|
+
MYSQL = "mysql"
|
|
42
|
+
MONGODB = "mongodb"
|
|
43
|
+
SQLITE = "sqlite"
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def config_class(self) -> Type["DBConfig"]:
|
|
47
|
+
"""Get the appropriate config class for this database type."""
|
|
48
|
+
from .config_mapping import DB_TYPE_MAPPING
|
|
49
|
+
|
|
50
|
+
return DB_TYPE_MAPPING[self]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Databases that can be used as sources (INPUT)
|
|
54
|
+
SOURCE_DATABASES: set[DBType] = {
|
|
55
|
+
DBType.ARANGO, # Graph DBs can be sources
|
|
56
|
+
DBType.NEO4J, # Graph DBs can be sources
|
|
57
|
+
DBType.TIGERGRAPH, # Graph DBs can be sources
|
|
58
|
+
DBType.POSTGRES, # SQL DBs
|
|
59
|
+
DBType.MYSQL,
|
|
60
|
+
DBType.MONGODB,
|
|
61
|
+
DBType.SQLITE,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Databases that can be used as targets (OUTPUT)
|
|
65
|
+
TARGET_DATABASES: set[DBType] = {
|
|
66
|
+
DBType.ARANGO,
|
|
67
|
+
DBType.NEO4J,
|
|
68
|
+
DBType.TIGERGRAPH,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class DBConfig(BaseSettings, abc.ABC):
|
|
73
|
+
"""Abstract base class for all database connection configurations using Pydantic BaseSettings."""
|
|
74
|
+
|
|
75
|
+
uri: str | None = Field(default=None, description="Backend URI")
|
|
76
|
+
username: str | None = Field(default=None, description="Authentication username")
|
|
77
|
+
password: str | None = Field(default=None, description="Authentication Password")
|
|
78
|
+
database: str | None = Field(
|
|
79
|
+
default=None,
|
|
80
|
+
description="Database name (backward compatibility, DB-specific mapping)",
|
|
81
|
+
)
|
|
82
|
+
schema_name: str | None = Field(
|
|
83
|
+
default=None,
|
|
84
|
+
validation_alias=AliasChoices("schema", "schema_name"),
|
|
85
|
+
description="Schema/graph name (unified internal structure)",
|
|
86
|
+
)
|
|
87
|
+
request_timeout: float = Field(
|
|
88
|
+
default=60.0, description="Request timeout in seconds"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@abc.abstractmethod
|
|
92
|
+
def _get_default_port(self) -> int:
|
|
93
|
+
"""Get the default port for this db type."""
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
@abc.abstractmethod
|
|
97
|
+
def _get_effective_database(self) -> str | None:
|
|
98
|
+
"""Get the effective database name based on DB type.
|
|
99
|
+
|
|
100
|
+
For SQL databases: returns the database name
|
|
101
|
+
For graph databases: returns None (they don't have a database level)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Database name or None
|
|
105
|
+
"""
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
@abc.abstractmethod
|
|
109
|
+
def _get_effective_schema(self) -> str | None:
|
|
110
|
+
"""Get the effective schema/graph name based on DB type.
|
|
111
|
+
|
|
112
|
+
For SQL databases: returns the schema name
|
|
113
|
+
For graph databases: returns the graph/database name (mapped from user-facing field)
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Schema/graph name or None
|
|
117
|
+
"""
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def effective_database(self) -> str | None:
|
|
122
|
+
"""Get the effective database name (delegates to concrete class)."""
|
|
123
|
+
return self._get_effective_database()
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def effective_schema(self) -> str | None:
|
|
127
|
+
"""Get the effective schema/graph name (delegates to concrete class)."""
|
|
128
|
+
return self._get_effective_schema()
|
|
129
|
+
|
|
130
|
+
@model_validator(mode="after")
|
|
131
|
+
def _add_default_port_to_uri(self):
|
|
132
|
+
"""Add default port to URI if missing."""
|
|
133
|
+
if self.uri is None:
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
parsed = urlparse(self.uri)
|
|
137
|
+
if parsed.port is not None:
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
# Add default port
|
|
141
|
+
default_port = self._get_default_port()
|
|
142
|
+
if parsed.scheme and parsed.hostname:
|
|
143
|
+
# Reconstruct URI with port
|
|
144
|
+
port_part = f":{default_port}" if default_port else ""
|
|
145
|
+
path_part = parsed.path or ""
|
|
146
|
+
query_part = f"?{parsed.query}" if parsed.query else ""
|
|
147
|
+
fragment_part = f"#{parsed.fragment}" if parsed.fragment else ""
|
|
148
|
+
self.uri = f"{parsed.scheme}://{parsed.hostname}{port_part}{path_part}{query_part}{fragment_part}"
|
|
149
|
+
|
|
150
|
+
return self
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def url(self) -> str | None:
|
|
154
|
+
"""Backward compatibility property: alias for uri."""
|
|
155
|
+
return self.uri
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def url_without_port(self) -> str:
|
|
159
|
+
"""Get URL without port."""
|
|
160
|
+
if self.uri is None:
|
|
161
|
+
raise ValueError("URI is not set")
|
|
162
|
+
parsed = urlparse(self.uri)
|
|
163
|
+
return f"{parsed.scheme}://{parsed.hostname}"
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def port(self) -> str | None:
|
|
167
|
+
"""Get port from URI."""
|
|
168
|
+
if self.uri is None:
|
|
169
|
+
return None
|
|
170
|
+
parsed = urlparse(self.uri)
|
|
171
|
+
return str(parsed.port) if parsed.port else None
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def protocol(self) -> str:
|
|
175
|
+
"""Get protocol/scheme from URI."""
|
|
176
|
+
if self.uri is None:
|
|
177
|
+
return "http"
|
|
178
|
+
parsed = urlparse(self.uri)
|
|
179
|
+
return parsed.scheme or "http"
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def hostname(self) -> str | None:
|
|
183
|
+
"""Get hostname from URI."""
|
|
184
|
+
if self.uri is None:
|
|
185
|
+
return None
|
|
186
|
+
parsed = urlparse(self.uri)
|
|
187
|
+
return parsed.hostname
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def connection_type(self) -> "DBType":
|
|
191
|
+
"""Get database type from class."""
|
|
192
|
+
# Map class to DBType - need to import here to avoid circular import
|
|
193
|
+
from .config_mapping import DB_TYPE_MAPPING
|
|
194
|
+
|
|
195
|
+
# Reverse lookup: find DBType for this class
|
|
196
|
+
for db_type, config_class in DB_TYPE_MAPPING.items():
|
|
197
|
+
if type(self) is config_class:
|
|
198
|
+
return db_type
|
|
199
|
+
|
|
200
|
+
# Fallback (shouldn't happen)
|
|
201
|
+
return DBType.ARANGO
|
|
202
|
+
|
|
203
|
+
def can_be_source(self) -> bool:
|
|
204
|
+
"""Check if this database type can be used as a source."""
|
|
205
|
+
return self.connection_type in SOURCE_DATABASES
|
|
206
|
+
|
|
207
|
+
def can_be_target(self) -> bool:
|
|
208
|
+
"""Check if this database type can be used as a target."""
|
|
209
|
+
return self.connection_type in TARGET_DATABASES
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def from_dict(cls, data: Dict[str, Any]) -> "DBConfig":
|
|
213
|
+
"""Create a connection config from a dictionary."""
|
|
214
|
+
if not isinstance(data, dict):
|
|
215
|
+
raise TypeError(f"Expected dict, got {type(data)}")
|
|
216
|
+
|
|
217
|
+
# Copy the data to avoid modifying the original
|
|
218
|
+
config_data = data.copy()
|
|
219
|
+
|
|
220
|
+
db_type = config_data.pop("db_type", None) or config_data.pop(
|
|
221
|
+
"connection_type", None
|
|
222
|
+
)
|
|
223
|
+
if not db_type:
|
|
224
|
+
raise ValueError("Missing 'db_type' or 'connection_type' in configuration")
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
conn_type = DBType(db_type)
|
|
228
|
+
except ValueError:
|
|
229
|
+
raise ValueError(
|
|
230
|
+
f"Database type '{db_type}' not supported. "
|
|
231
|
+
f"Should be one of: {list(DBType)}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Map old 'url' field to 'uri' for backward compatibility
|
|
235
|
+
if "url" in config_data and "uri" not in config_data:
|
|
236
|
+
config_data["uri"] = config_data.pop("url")
|
|
237
|
+
|
|
238
|
+
# Map old credential fields
|
|
239
|
+
if "cred_name" in config_data and "username" not in config_data:
|
|
240
|
+
config_data["username"] = config_data.pop("cred_name")
|
|
241
|
+
if "cred_pass" in config_data and "password" not in config_data:
|
|
242
|
+
config_data["password"] = config_data.pop("cred_pass")
|
|
243
|
+
|
|
244
|
+
# Construct URI from protocol/hostname/port if uri is not provided
|
|
245
|
+
if "uri" not in config_data:
|
|
246
|
+
protocol = config_data.pop("protocol", "http")
|
|
247
|
+
hostname = config_data.pop("hostname", None)
|
|
248
|
+
port = config_data.pop("port", None)
|
|
249
|
+
hosts = config_data.pop("hosts", None)
|
|
250
|
+
|
|
251
|
+
if hosts:
|
|
252
|
+
# Use hosts as URI
|
|
253
|
+
config_data["uri"] = hosts
|
|
254
|
+
elif hostname:
|
|
255
|
+
# Construct URI from components
|
|
256
|
+
if port:
|
|
257
|
+
config_data["uri"] = f"{protocol}://{hostname}:{port}"
|
|
258
|
+
else:
|
|
259
|
+
config_data["uri"] = f"{protocol}://{hostname}"
|
|
260
|
+
|
|
261
|
+
# Get the appropriate config class and initialize it
|
|
262
|
+
config_class = conn_type.config_class
|
|
263
|
+
return config_class(**config_data)
|
|
264
|
+
|
|
265
|
+
@classmethod
|
|
266
|
+
def from_docker_env(cls, docker_dir: str | Path | None = None) -> "DBConfig":
|
|
267
|
+
"""Load config from docker .env file.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
docker_dir: Path to docker directory. If None, uses default based on db type.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
DBConfig instance loaded from .env file
|
|
274
|
+
"""
|
|
275
|
+
raise NotImplementedError("Subclasses must implement from_docker_env")
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
def from_env(cls: Type[T], prefix: str | None = None) -> T:
|
|
279
|
+
"""Load config from environment variables using Pydantic BaseSettings.
|
|
280
|
+
|
|
281
|
+
Supports custom prefixes for multiple configs:
|
|
282
|
+
- Default (prefix=None): Uses {BASE_PREFIX}URI, {BASE_PREFIX}USERNAME, etc.
|
|
283
|
+
- With prefix (prefix="USER"): Uses USER_{BASE_PREFIX}URI, USER_{BASE_PREFIX}USERNAME, etc.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
prefix: Optional prefix for environment variables (e.g., "USER", "LAKE", "KG").
|
|
287
|
+
If None, uses default {BASE_PREFIX}* variables.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
DBConfig instance loaded from environment variables using Pydantic BaseSettings
|
|
291
|
+
|
|
292
|
+
Examples:
|
|
293
|
+
# Load default config (ARANGO_URI, ARANGO_USERNAME, etc.)
|
|
294
|
+
config = ArangoConfig.from_env()
|
|
295
|
+
|
|
296
|
+
# Load config with prefix (USER_ARANGO_URI, USER_ARANGO_USERNAME, etc.)
|
|
297
|
+
user_config = ArangoConfig.from_env(prefix="USER")
|
|
298
|
+
"""
|
|
299
|
+
if prefix:
|
|
300
|
+
# Get the base prefix from the class's model_config
|
|
301
|
+
base_prefix = cls.model_config.get("env_prefix")
|
|
302
|
+
if not base_prefix:
|
|
303
|
+
raise ValueError(
|
|
304
|
+
f"Class {cls.__name__} does not have env_prefix configured in model_config"
|
|
305
|
+
)
|
|
306
|
+
# Create a new model class with modified env_prefix
|
|
307
|
+
new_prefix = f"{prefix.upper()}_{base_prefix}"
|
|
308
|
+
case_sensitive = cls.model_config.get("case_sensitive", False)
|
|
309
|
+
model_config = SettingsConfigDict(
|
|
310
|
+
env_prefix=new_prefix,
|
|
311
|
+
case_sensitive=case_sensitive,
|
|
312
|
+
)
|
|
313
|
+
# Create a new class dynamically with the modified prefix
|
|
314
|
+
temp_class = type(
|
|
315
|
+
f"{cls.__name__}WithPrefix", (cls,), {"model_config": model_config}
|
|
316
|
+
)
|
|
317
|
+
return temp_class()
|
|
318
|
+
else:
|
|
319
|
+
# Use default prefix - Pydantic will read from environment automatically
|
|
320
|
+
return cls()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class ArangoConfig(DBConfig):
|
|
324
|
+
"""Configuration for ArangoDB connections."""
|
|
325
|
+
|
|
326
|
+
model_config = SettingsConfigDict(
|
|
327
|
+
env_prefix="ARANGO_",
|
|
328
|
+
case_sensitive=False,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def _get_default_port(self) -> int:
|
|
332
|
+
"""Get default ArangoDB port."""
|
|
333
|
+
return 8529
|
|
334
|
+
|
|
335
|
+
def _get_effective_database(self) -> str | None:
|
|
336
|
+
"""ArangoDB doesn't have a database level (connection -> database/graph -> collections)."""
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
def _get_effective_schema(self) -> str | None:
|
|
340
|
+
"""For ArangoDB, 'database' field maps to schema (graph) in unified model.
|
|
341
|
+
|
|
342
|
+
ArangoDB structure: connection -> database (graph) -> collections
|
|
343
|
+
Unified model: connection -> schema -> entities
|
|
344
|
+
"""
|
|
345
|
+
return self.database
|
|
346
|
+
|
|
347
|
+
@classmethod
|
|
348
|
+
def from_docker_env(cls, docker_dir: str | Path | None = None) -> "ArangoConfig":
|
|
349
|
+
"""Load ArangoDB config from docker/arango/.env file.
|
|
350
|
+
|
|
351
|
+
The .env file structure is minimal and may contain:
|
|
352
|
+
- ARANGO_PORT: Port number (defaults hostname to localhost, protocol to http)
|
|
353
|
+
- ARANGO_URI: Full URI (alternative to ARANGO_PORT)
|
|
354
|
+
- ARANGO_HOSTNAME: Hostname (defaults to localhost)
|
|
355
|
+
- ARANGO_PROTOCOL: Protocol (defaults to http)
|
|
356
|
+
- ARANGO_USERNAME: Username (defaults to root)
|
|
357
|
+
- ARANGO_PASSWORD: Password (or read from secret file if PATH_TO_SECRET is set)
|
|
358
|
+
- ARANGO_DATABASE: Database name (optional, can be set later)
|
|
359
|
+
- PATH_TO_SECRET: Path to secret file containing password (relative to docker_dir)
|
|
360
|
+
"""
|
|
361
|
+
if docker_dir is None:
|
|
362
|
+
docker_dir = (
|
|
363
|
+
Path(__file__).parent.parent.parent.parent / "docker" / "arango"
|
|
364
|
+
)
|
|
365
|
+
else:
|
|
366
|
+
docker_dir = Path(docker_dir)
|
|
367
|
+
|
|
368
|
+
env_file = docker_dir / ".env"
|
|
369
|
+
if not env_file.exists():
|
|
370
|
+
raise FileNotFoundError(f"Environment file not found: {env_file}")
|
|
371
|
+
|
|
372
|
+
# Load .env file manually with simple variable expansion
|
|
373
|
+
env_vars: Dict[str, str] = {}
|
|
374
|
+
with open(env_file, "r") as f:
|
|
375
|
+
for line in f:
|
|
376
|
+
line = line.strip()
|
|
377
|
+
if line and not line.startswith("#") and "=" in line:
|
|
378
|
+
key, value = line.split("=", 1)
|
|
379
|
+
key = key.strip()
|
|
380
|
+
value = value.strip().strip('"').strip("'")
|
|
381
|
+
env_vars[key] = value
|
|
382
|
+
|
|
383
|
+
# Expand variables (simple single-pass expansion)
|
|
384
|
+
# First pass: expand ${SPEC} references
|
|
385
|
+
for key, value in env_vars.items():
|
|
386
|
+
if "${SPEC}" in value and "SPEC" in env_vars:
|
|
387
|
+
env_vars[key] = value.replace("${SPEC}", env_vars["SPEC"])
|
|
388
|
+
|
|
389
|
+
# Second pass: expand other variables (like ${CONTAINER_NAME})
|
|
390
|
+
for key, value in env_vars.items():
|
|
391
|
+
for var_name, var_value in env_vars.items():
|
|
392
|
+
var_ref = f"${{{var_name}}}"
|
|
393
|
+
if var_ref in value:
|
|
394
|
+
env_vars[key] = value.replace(var_ref, var_value)
|
|
395
|
+
|
|
396
|
+
# Map environment variables to config
|
|
397
|
+
config_data: Dict[str, Any] = {}
|
|
398
|
+
|
|
399
|
+
# URI construction
|
|
400
|
+
if "ARANGO_URI" in env_vars:
|
|
401
|
+
config_data["uri"] = env_vars["ARANGO_URI"]
|
|
402
|
+
elif "ARANGO_PORT" in env_vars:
|
|
403
|
+
port = env_vars["ARANGO_PORT"]
|
|
404
|
+
hostname = env_vars.get("ARANGO_HOSTNAME", "localhost")
|
|
405
|
+
protocol = env_vars.get("ARANGO_PROTOCOL", "http")
|
|
406
|
+
config_data["uri"] = f"{protocol}://{hostname}:{port}"
|
|
407
|
+
else:
|
|
408
|
+
# Default to localhost:8529 if nothing is specified
|
|
409
|
+
config_data["uri"] = "http://localhost:8529"
|
|
410
|
+
|
|
411
|
+
# Username (defaults to root for ArangoDB)
|
|
412
|
+
if "ARANGO_USERNAME" in env_vars:
|
|
413
|
+
config_data["username"] = env_vars["ARANGO_USERNAME"]
|
|
414
|
+
else:
|
|
415
|
+
config_data["username"] = "root"
|
|
416
|
+
|
|
417
|
+
# Password: check ARANGO_PASSWORD first, then try secret file
|
|
418
|
+
if "ARANGO_PASSWORD" in env_vars:
|
|
419
|
+
config_data["password"] = env_vars["ARANGO_PASSWORD"]
|
|
420
|
+
elif "PATH_TO_SECRET" in env_vars:
|
|
421
|
+
# Read password from secret file
|
|
422
|
+
secret_path_str = env_vars["PATH_TO_SECRET"]
|
|
423
|
+
# Handle relative paths (relative to docker_dir)
|
|
424
|
+
if secret_path_str.startswith("./"):
|
|
425
|
+
secret_path = docker_dir / secret_path_str[2:]
|
|
426
|
+
else:
|
|
427
|
+
secret_path = Path(secret_path_str)
|
|
428
|
+
|
|
429
|
+
if secret_path.exists():
|
|
430
|
+
with open(secret_path, "r") as f:
|
|
431
|
+
config_data["password"] = f.read().strip()
|
|
432
|
+
else:
|
|
433
|
+
# Secret file not found, password will be None (ArangoDB accepts empty string)
|
|
434
|
+
config_data["password"] = None
|
|
435
|
+
|
|
436
|
+
# Database (optional, can be set later or use Schema.general.name)
|
|
437
|
+
if "ARANGO_DATABASE" in env_vars:
|
|
438
|
+
config_data["database"] = env_vars["ARANGO_DATABASE"]
|
|
439
|
+
|
|
440
|
+
return cls(**config_data)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class Neo4jConfig(DBConfig):
|
|
444
|
+
"""Configuration for Neo4j connections."""
|
|
445
|
+
|
|
446
|
+
model_config = SettingsConfigDict(
|
|
447
|
+
env_prefix="NEO4J_",
|
|
448
|
+
case_sensitive=False,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
bolt_port: int | None = Field(default=None, description="Neo4j bolt protocol port")
|
|
452
|
+
|
|
453
|
+
def _get_default_port(self) -> int:
|
|
454
|
+
"""Get default Neo4j HTTP port."""
|
|
455
|
+
return 7474
|
|
456
|
+
|
|
457
|
+
def _get_effective_database(self) -> str | None:
|
|
458
|
+
"""Neo4j doesn't have a database level (connection -> database -> nodes/relationships)."""
|
|
459
|
+
return None
|
|
460
|
+
|
|
461
|
+
def _get_effective_schema(self) -> str | None:
|
|
462
|
+
"""For Neo4j, 'database' field maps to schema (database) in unified model.
|
|
463
|
+
|
|
464
|
+
Neo4j structure: connection -> database -> nodes/relationships
|
|
465
|
+
Unified model: connection -> schema -> entities
|
|
466
|
+
"""
|
|
467
|
+
return self.database
|
|
468
|
+
|
|
469
|
+
def __init__(self, **data):
|
|
470
|
+
"""Initialize Neo4j config."""
|
|
471
|
+
super().__init__(**data)
|
|
472
|
+
# Set default bolt_port if not provided
|
|
473
|
+
if self.bolt_port is None:
|
|
474
|
+
self.bolt_port = 7687
|
|
475
|
+
|
|
476
|
+
@classmethod
|
|
477
|
+
def from_docker_env(cls, docker_dir: str | Path | None = None) -> "Neo4jConfig":
|
|
478
|
+
"""Load Neo4j config from docker/neo4j/.env file."""
|
|
479
|
+
if docker_dir is None:
|
|
480
|
+
docker_dir = Path(__file__).parent.parent.parent.parent / "docker" / "neo4j"
|
|
481
|
+
else:
|
|
482
|
+
docker_dir = Path(docker_dir)
|
|
483
|
+
|
|
484
|
+
env_file = docker_dir / ".env"
|
|
485
|
+
if not env_file.exists():
|
|
486
|
+
raise FileNotFoundError(f"Environment file not found: {env_file}")
|
|
487
|
+
|
|
488
|
+
# Load .env file manually
|
|
489
|
+
env_vars: Dict[str, str] = {}
|
|
490
|
+
with open(env_file, "r") as f:
|
|
491
|
+
for line in f:
|
|
492
|
+
line = line.strip()
|
|
493
|
+
if line and not line.startswith("#") and "=" in line:
|
|
494
|
+
key, value = line.split("=", 1)
|
|
495
|
+
env_vars[key.strip()] = value.strip().strip('"').strip("'")
|
|
496
|
+
|
|
497
|
+
# Map environment variables to config
|
|
498
|
+
config_data: Dict[str, Any] = {}
|
|
499
|
+
# Neo4j typically uses bolt protocol
|
|
500
|
+
if "NEO4J_BOLT_PORT" in env_vars:
|
|
501
|
+
port = env_vars["NEO4J_BOLT_PORT"]
|
|
502
|
+
hostname = env_vars.get("NEO4J_HOSTNAME", "localhost")
|
|
503
|
+
config_data["uri"] = f"bolt://{hostname}:{port}"
|
|
504
|
+
config_data["bolt_port"] = int(port)
|
|
505
|
+
elif "NEO4J_URI" in env_vars:
|
|
506
|
+
config_data["uri"] = env_vars["NEO4J_URI"]
|
|
507
|
+
|
|
508
|
+
if "NEO4J_USERNAME" in env_vars:
|
|
509
|
+
config_data["username"] = env_vars["NEO4J_USERNAME"]
|
|
510
|
+
elif "NEO4J_AUTH" in env_vars:
|
|
511
|
+
# Parse NEO4J_AUTH format: username/password
|
|
512
|
+
auth = env_vars["NEO4J_AUTH"].split("/")
|
|
513
|
+
if len(auth) == 2:
|
|
514
|
+
config_data["username"] = auth[0]
|
|
515
|
+
config_data["password"] = auth[1]
|
|
516
|
+
|
|
517
|
+
if "NEO4J_PASSWORD" in env_vars:
|
|
518
|
+
config_data["password"] = env_vars["NEO4J_PASSWORD"]
|
|
519
|
+
if "NEO4J_DATABASE" in env_vars:
|
|
520
|
+
config_data["database"] = env_vars["NEO4J_DATABASE"]
|
|
521
|
+
|
|
522
|
+
return cls(**config_data)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
class TigergraphConfig(DBConfig):
|
|
526
|
+
"""Configuration for TigerGraph connections."""
|
|
527
|
+
|
|
528
|
+
model_config = SettingsConfigDict(
|
|
529
|
+
env_prefix="TIGERGRAPH_",
|
|
530
|
+
case_sensitive=False,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
gs_port: int | None = Field(default=None, description="TigerGraph GSQL port")
|
|
534
|
+
secret: str | None = Field(
|
|
535
|
+
default=None, description="TigerGraph secret for token authentication"
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def _get_default_port(self) -> int:
|
|
539
|
+
"""Get default TigerGraph REST++ port."""
|
|
540
|
+
return 9000
|
|
541
|
+
|
|
542
|
+
def _get_effective_database(self) -> str | None:
|
|
543
|
+
"""TigerGraph doesn't have a database level (connection -> schema -> vertices/edges)."""
|
|
544
|
+
return None
|
|
545
|
+
|
|
546
|
+
def _get_effective_schema(self) -> str | None:
|
|
547
|
+
"""For TigerGraph, 'schema_name' field maps to schema (graph) in unified model.
|
|
548
|
+
|
|
549
|
+
TigerGraph structure: connection -> schema -> vertices/edges
|
|
550
|
+
Unified model: connection -> schema -> entities
|
|
551
|
+
"""
|
|
552
|
+
return self.schema_name
|
|
553
|
+
|
|
554
|
+
def __init__(self, **data):
|
|
555
|
+
"""Initialize TigerGraph config."""
|
|
556
|
+
super().__init__(**data)
|
|
557
|
+
# Set default gs_port if not provided
|
|
558
|
+
if self.gs_port is None:
|
|
559
|
+
self.gs_port = 14240
|
|
560
|
+
|
|
561
|
+
@classmethod
|
|
562
|
+
def from_docker_env(
|
|
563
|
+
cls, docker_dir: str | Path | None = None
|
|
564
|
+
) -> "TigergraphConfig":
|
|
565
|
+
"""Load TigerGraph config from docker/tigergraph/.env file."""
|
|
566
|
+
if docker_dir is None:
|
|
567
|
+
docker_dir = (
|
|
568
|
+
Path(__file__).parent.parent.parent.parent / "docker" / "tigergraph"
|
|
569
|
+
)
|
|
570
|
+
else:
|
|
571
|
+
docker_dir = Path(docker_dir)
|
|
572
|
+
|
|
573
|
+
env_file = docker_dir / ".env"
|
|
574
|
+
if not env_file.exists():
|
|
575
|
+
raise FileNotFoundError(f"Environment file not found: {env_file}")
|
|
576
|
+
|
|
577
|
+
# Load .env file manually
|
|
578
|
+
env_vars: Dict[str, str] = {}
|
|
579
|
+
with open(env_file, "r") as f:
|
|
580
|
+
for line in f:
|
|
581
|
+
line = line.strip()
|
|
582
|
+
if line and not line.startswith("#") and "=" in line:
|
|
583
|
+
key, value = line.split("=", 1)
|
|
584
|
+
env_vars[key.strip()] = value.strip().strip('"').strip("'")
|
|
585
|
+
|
|
586
|
+
# Map environment variables to config
|
|
587
|
+
config_data: Dict[str, Any] = {}
|
|
588
|
+
if "TG_REST" in env_vars or "TIGERGRAPH_PORT" in env_vars:
|
|
589
|
+
port = env_vars.get("TG_REST") or env_vars.get("TIGERGRAPH_PORT")
|
|
590
|
+
hostname = env_vars.get("TIGERGRAPH_HOSTNAME", "localhost")
|
|
591
|
+
protocol = env_vars.get("TIGERGRAPH_PROTOCOL", "http")
|
|
592
|
+
config_data["uri"] = f"{protocol}://{hostname}:{port}"
|
|
593
|
+
|
|
594
|
+
if "TG_WEB" in env_vars or "TIGERGRAPH_GS_PORT" in env_vars:
|
|
595
|
+
gs_port = env_vars.get("TG_WEB") or env_vars.get("TIGERGRAPH_GS_PORT")
|
|
596
|
+
config_data["gs_port"] = int(gs_port) if gs_port else None
|
|
597
|
+
|
|
598
|
+
if "TIGERGRAPH_USERNAME" in env_vars:
|
|
599
|
+
config_data["username"] = env_vars["TIGERGRAPH_USERNAME"]
|
|
600
|
+
if "TIGERGRAPH_PASSWORD" in env_vars or "GSQL_PASSWORD" in env_vars:
|
|
601
|
+
config_data["password"] = env_vars.get(
|
|
602
|
+
"TIGERGRAPH_PASSWORD"
|
|
603
|
+
) or env_vars.get("GSQL_PASSWORD")
|
|
604
|
+
if "TIGERGRAPH_DATABASE" in env_vars:
|
|
605
|
+
config_data["database"] = env_vars["TIGERGRAPH_DATABASE"]
|
|
606
|
+
|
|
607
|
+
return cls(**config_data)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
class PostgresConfig(DBConfig):
|
|
611
|
+
"""Configuration for PostgreSQL connections."""
|
|
612
|
+
|
|
613
|
+
model_config = SettingsConfigDict(
|
|
614
|
+
env_prefix="POSTGRES_",
|
|
615
|
+
case_sensitive=False,
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
def _get_default_port(self) -> int:
|
|
619
|
+
"""Get default PostgreSQL port."""
|
|
620
|
+
return 5432
|
|
621
|
+
|
|
622
|
+
def _get_effective_database(self) -> str | None:
|
|
623
|
+
"""For PostgreSQL, 'database' field is the actual database name.
|
|
624
|
+
|
|
625
|
+
PostgreSQL structure: connection -> database -> schema -> table
|
|
626
|
+
Unified model: connection -> database -> schema -> entity
|
|
627
|
+
"""
|
|
628
|
+
return self.database
|
|
629
|
+
|
|
630
|
+
def _get_effective_schema(self) -> str | None:
|
|
631
|
+
"""For PostgreSQL, 'schema_name' field is the schema name.
|
|
632
|
+
|
|
633
|
+
PostgreSQL structure: connection -> database -> schema -> table
|
|
634
|
+
Unified model: connection -> database -> schema -> entity
|
|
635
|
+
"""
|
|
636
|
+
return self.schema_name
|
|
637
|
+
|
|
638
|
+
@classmethod
|
|
639
|
+
def from_docker_env(cls, docker_dir: str | Path | None = None) -> "PostgresConfig":
|
|
640
|
+
"""Load PostgreSQL config from docker/postgres/.env file."""
|
|
641
|
+
if docker_dir is None:
|
|
642
|
+
docker_dir = (
|
|
643
|
+
Path(__file__).parent.parent.parent.parent / "docker" / "postgres"
|
|
644
|
+
)
|
|
645
|
+
else:
|
|
646
|
+
docker_dir = Path(docker_dir)
|
|
647
|
+
|
|
648
|
+
env_file = docker_dir / ".env"
|
|
649
|
+
if not env_file.exists():
|
|
650
|
+
raise FileNotFoundError(f"Environment file not found: {env_file}")
|
|
651
|
+
|
|
652
|
+
# Load .env file manually
|
|
653
|
+
env_vars: Dict[str, str] = {}
|
|
654
|
+
with open(env_file, "r") as f:
|
|
655
|
+
for line in f:
|
|
656
|
+
line = line.strip()
|
|
657
|
+
if line and not line.startswith("#") and "=" in line:
|
|
658
|
+
key, value = line.split("=", 1)
|
|
659
|
+
env_vars[key.strip()] = value.strip().strip('"').strip("'")
|
|
660
|
+
|
|
661
|
+
# Map environment variables to config
|
|
662
|
+
config_data: Dict[str, Any] = {}
|
|
663
|
+
if "POSTGRES_URI" in env_vars:
|
|
664
|
+
config_data["uri"] = env_vars["POSTGRES_URI"]
|
|
665
|
+
elif "POSTGRES_PORT" in env_vars:
|
|
666
|
+
port = env_vars["POSTGRES_PORT"]
|
|
667
|
+
hostname = env_vars.get("POSTGRES_HOSTNAME", "localhost")
|
|
668
|
+
protocol = env_vars.get("POSTGRES_PROTOCOL", "postgresql")
|
|
669
|
+
config_data["uri"] = f"{protocol}://{hostname}:{port}"
|
|
670
|
+
elif "POSTGRES_HOST" in env_vars:
|
|
671
|
+
# PostgreSQL often uses POSTGRES_HOST instead of POSTGRES_HOSTNAME
|
|
672
|
+
port = env_vars.get("POSTGRES_PORT", "5432")
|
|
673
|
+
hostname = env_vars["POSTGRES_HOST"]
|
|
674
|
+
protocol = env_vars.get("POSTGRES_PROTOCOL", "postgresql")
|
|
675
|
+
config_data["uri"] = f"{protocol}://{hostname}:{port}"
|
|
676
|
+
|
|
677
|
+
if "POSTGRES_USER" in env_vars or "POSTGRES_USERNAME" in env_vars:
|
|
678
|
+
config_data["username"] = env_vars.get("POSTGRES_USER") or env_vars.get(
|
|
679
|
+
"POSTGRES_USERNAME"
|
|
680
|
+
)
|
|
681
|
+
if "POSTGRES_PASSWORD" in env_vars:
|
|
682
|
+
config_data["password"] = env_vars["POSTGRES_PASSWORD"]
|
|
683
|
+
if "POSTGRES_DB" in env_vars or "POSTGRES_DATABASE" in env_vars:
|
|
684
|
+
config_data["database"] = env_vars.get("POSTGRES_DB") or env_vars.get(
|
|
685
|
+
"POSTGRES_DATABASE"
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
return cls(**config_data)
|