graphxr-database-proxy 1.0.0__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 (33) hide show
  1. graphxr_database_proxy/__init__.py +16 -0
  2. graphxr_database_proxy/api/__init__.py +1 -0
  3. graphxr_database_proxy/api/database.py +266 -0
  4. graphxr_database_proxy/api/google.py +437 -0
  5. graphxr_database_proxy/api/projects.py +99 -0
  6. graphxr_database_proxy/common/util.py +38 -0
  7. graphxr_database_proxy/drivers/__init__.py +1 -0
  8. graphxr_database_proxy/drivers/base.py +56 -0
  9. graphxr_database_proxy/drivers/factory.py +36 -0
  10. graphxr_database_proxy/drivers/spanner.py +814 -0
  11. graphxr_database_proxy/main.py +110 -0
  12. graphxr_database_proxy/models/__init__.py +1 -0
  13. graphxr_database_proxy/models/google.py +50 -0
  14. graphxr_database_proxy/models/project.py +170 -0
  15. graphxr_database_proxy/proxy.py +495 -0
  16. graphxr_database_proxy/proxyForDev.py +290 -0
  17. graphxr_database_proxy/services/__init__.py +1 -0
  18. graphxr_database_proxy/services/project_service.py +150 -0
  19. graphxr_database_proxy/static/favicon.ico +0 -0
  20. graphxr_database_proxy/static/index.html +1 -0
  21. graphxr_database_proxy/static/main.7391ee9773c403483393.css +175 -0
  22. graphxr_database_proxy/static/main.7391ee9773c403483393.css.map +1 -0
  23. graphxr_database_proxy/static/main.ce3fbb85a7bc9452edb9.js +2 -0
  24. graphxr_database_proxy/static/main.ce3fbb85a7bc9452edb9.js.map +1 -0
  25. graphxr_database_proxy/static/vendors.70542a99f336a8021013.js +3 -0
  26. graphxr_database_proxy/static/vendors.70542a99f336a8021013.js.LICENSE.txt +95 -0
  27. graphxr_database_proxy/static/vendors.70542a99f336a8021013.js.map +1 -0
  28. graphxr_database_proxy-1.0.0.dist-info/METADATA +180 -0
  29. graphxr_database_proxy-1.0.0.dist-info/RECORD +33 -0
  30. graphxr_database_proxy-1.0.0.dist-info/WHEEL +5 -0
  31. graphxr_database_proxy-1.0.0.dist-info/entry_points.txt +2 -0
  32. graphxr_database_proxy-1.0.0.dist-info/licenses/LICENSE +21 -0
  33. graphxr_database_proxy-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,495 @@
1
+ """
2
+ DatabaseProxy - High-level interface for GraphXR Database Proxy
3
+ """
4
+
5
+ import asyncio
6
+ import uvicorn
7
+ import json
8
+ import uuid
9
+ import os
10
+ from typing import Dict, Any, Optional, List
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+
14
+ from .main import app
15
+ from .models.project import Project, DatabaseConfig, DatabaseType, AuthType, OAuthConfig
16
+ from .services.project_service import ProjectService
17
+
18
+
19
+ class DatabaseProxy:
20
+ """
21
+ High-level interface for GraphXR Database Proxy
22
+
23
+ This class provides a simplified Python API for configuring and running
24
+ the GraphXR Database Proxy with Service Account authentication.
25
+ """
26
+
27
+ def __init__(self, config_dir: str = "config"):
28
+ """
29
+ Initialize DatabaseProxy
30
+
31
+ Args:
32
+ config_dir: Directory to store configuration files
33
+ """
34
+ self.project_service = ProjectService(config_dir)
35
+ self.projects: Dict[str, Project] = {}
36
+ self._app = app
37
+ self._load_existing_projects()
38
+
39
+ def _load_existing_projects(self) -> None:
40
+ """Load existing projects from storage"""
41
+ try:
42
+ # This would load from the project service if needed
43
+ pass
44
+ except Exception as e:
45
+ print(f"Warning: Could not load existing projects: {e}")
46
+
47
+ def add_project(
48
+ self,
49
+ project_name: Optional[str] = None,
50
+ database_type: str = "spanner",
51
+ project_id: Optional[str] = None,
52
+ instance_id: Optional[str] = None,
53
+ database_id: Optional[str] = None,
54
+ credentials: Optional[str] = None,
55
+ graph_name: Optional[str] = None,
56
+ **kwargs
57
+ ) -> str:
58
+ """
59
+ Add a database project configuration
60
+
61
+ Args:
62
+ project_name: Name of the project (uses env var PROJECT_NAME if not provided)
63
+ database_type: Database type ("spanner", "neo4j", "postgresql", etc.)
64
+ project_id: Database project ID (for Spanner: GCP Project ID, uses env var SPANNER_PROJECT_ID if not provided)
65
+ instance_id: Database instance ID (for Spanner: uses env var SPANNER_INSTANCE_ID if not provided)
66
+ database_id: Database ID (for Spanner: uses env var SPANNER_DATABASE_ID if not provided)
67
+ credentials: Authentication credentials - either:
68
+ - Path to credential file (e.g., "/path/to/service-account.json")
69
+ - Credential JSON string (e.g., '{"type": "service_account", ...}')
70
+ Uses env var SPANNER_CREDENTIALS_PATH if not provided
71
+ graph_name: Optional graph name (uses env var SPANNER_GRAPH_NAME if not provided)
72
+ **kwargs: Additional database-specific configuration parameters
73
+
74
+ Environment Variables:
75
+ PROJECT_NAME: Default project name
76
+ SPANNER_PROJECT_ID: Default GCP project ID
77
+ SPANNER_INSTANCE_ID: Default Spanner instance ID
78
+ SPANNER_DATABASE_ID: Default Spanner database ID
79
+ SPANNER_CREDENTIALS_PATH: Default path to service account JSON file
80
+ SPANNER_GRAPH_NAME: Default graph name
81
+
82
+ Returns:
83
+ Internal Project ID
84
+
85
+ Example 1 (Spanner with file path):
86
+ proxy.add_project(
87
+ project_name="MySpannerProject",
88
+ database_type="spanner",
89
+ project_id="your-gcp-project-id",
90
+ instance_id="your-spanner-instance-id",
91
+ database_id="your-database-id",
92
+ credentials="/path/to/your/service-account.json"
93
+ )
94
+
95
+ Example 2 (Spanner with JSON string):
96
+ proxy.add_project(
97
+ project_name="MySpannerProject",
98
+ database_type="spanner",
99
+ project_id="your-gcp-project-id",
100
+ instance_id="your-spanner-instance-id",
101
+ database_id="your-database-id",
102
+ credentials='{"type": "service_account"}'
103
+ )
104
+
105
+ Example 3 (Using environment variables):
106
+ # Set environment variables first
107
+ # os.environ['PROJECT_NAME'] = 'MySpannerProject'
108
+ # os.environ['SPANNER_PROJECT_ID'] = 'your-gcp-project-id'
109
+ # os.environ['SPANNER_INSTANCE_ID'] = 'your-spanner-instance-id'
110
+ # os.environ['SPANNER_DATABASE_ID'] = 'your-database-id'
111
+ # os.environ['SPANNER_CREDENTIALS_PATH'] = '/path/to/service-account.json'
112
+ # os.environ['SPANNER_GRAPH_NAME'] = 'graph'
113
+ proxy.add_project() # No parameters needed, all from environment variables
114
+
115
+ Example 4 (Future: Neo4j):
116
+ proxy.add_project(
117
+ project_name="MyNeo4jProject",
118
+ database_type="neo4j",
119
+ credentials="neo4j://username:password@localhost:7687",
120
+ graph_name="graph"
121
+ )
122
+ """
123
+ # Use environment variables as defaults
124
+ project_name = project_name or os.getenv('PROJECT_NAME')
125
+ project_id = project_id or os.getenv('SPANNER_PROJECT_ID')
126
+ instance_id = instance_id or os.getenv('SPANNER_INSTANCE_ID')
127
+ database_id = database_id or os.getenv('SPANNER_DATABASE_ID')
128
+ credentials = credentials or os.getenv('SPANNER_CREDENTIALS_PATH')
129
+ graph_name = graph_name or os.getenv('SPANNER_GRAPH_NAME')
130
+
131
+ # Validate required parameters
132
+ if not project_name:
133
+ raise ValueError("project_name is required (either as parameter or PROJECT_NAME environment variable)")
134
+ if not project_id:
135
+ raise ValueError("project_id is required (either as parameter or SPANNER_PROJECT_ID environment variable)")
136
+
137
+ # Handle different database types
138
+ if database_type.lower() == "spanner":
139
+ return self._add_spanner_project(
140
+ project_name=project_name,
141
+ project_id=project_id,
142
+ instance_id=instance_id,
143
+ database_id=database_id,
144
+ credentials=credentials,
145
+ graph_name=graph_name,
146
+ **kwargs
147
+ )
148
+ else:
149
+ # Future database types can be added here
150
+ raise NotImplementedError(f"Database type '{database_type}' is not yet supported. Currently supported: spanner")
151
+
152
+ def _add_spanner_project(
153
+ self,
154
+ project_name: str,
155
+ project_id: Optional[str],
156
+ instance_id: Optional[str],
157
+ database_id: Optional[str],
158
+ credentials: Optional[str],
159
+ graph_name: Optional[str] = None,
160
+ **kwargs
161
+ ) -> str:
162
+ """Internal method to add Spanner project configuration"""
163
+
164
+ # Validate required Spanner parameters
165
+ if not project_id:
166
+ raise ValueError("project_id is required for Spanner projects")
167
+ if not instance_id:
168
+ raise ValueError("instance_id is required for Spanner projects (use SPANNER_INSTANCE_ID environment variable or parameter)")
169
+ if not database_id:
170
+ raise ValueError("database_id is required for Spanner projects (use SPANNER_DATABASE_ID environment variable or parameter)")
171
+ if not credentials:
172
+ raise ValueError("credentials is required for Spanner projects (use SPANNER_CREDENTIALS_PATH environment variable or parameter)")
173
+
174
+
175
+ # Validate required parameters for Spanner
176
+ if not all([project_id, instance_id, database_id, credentials]):
177
+ raise ValueError("For Spanner projects, project_id, instance_id, database_id, and credentials are required")
178
+
179
+ # Determine if credentials is a file path or JSON string
180
+ service_account_data = None
181
+ credentials_source = None
182
+
183
+ # Try to parse as JSON first
184
+ try:
185
+ service_account_data = json.loads(credentials)
186
+ credentials_source = "json_string"
187
+ print(f"✓ Using Service Account JSON string")
188
+ except json.JSONDecodeError:
189
+ # Not JSON, treat as file path
190
+ creds_path = Path(credentials)
191
+ if not creds_path.exists():
192
+ raise FileNotFoundError(f"Service account file not found: {credentials}")
193
+
194
+ try:
195
+ with open(creds_path, 'r') as f:
196
+ service_account_data = json.load(f)
197
+ credentials_source = "file_path"
198
+ print(f"✓ Using Service Account file: {creds_path.absolute()}")
199
+ except json.JSONDecodeError:
200
+ raise ValueError(f"Invalid JSON in service account file: {credentials}")
201
+
202
+ # Validate required fields
203
+ required_fields = ['type', 'project_id', 'private_key', 'client_email']
204
+ for field in required_fields:
205
+ if field not in service_account_data:
206
+ raise ValueError(f"Missing required field in service account JSON: {field}")
207
+
208
+ # Create OAuth config with service account data
209
+ oauth_config = OAuthConfig(**service_account_data)
210
+
211
+ # Create database configuration
212
+ database_config = DatabaseConfig(
213
+ type=DatabaseType.SPANNER,
214
+ project_id=project_id,
215
+ instance_id=instance_id,
216
+ database_id=database_id,
217
+ graph_name=graph_name,
218
+ auth_type=AuthType.SERVICE_ACCOUNT,
219
+ oauth_config=oauth_config,
220
+ service_account_path=credentials if credentials_source == "file_path" else None
221
+ )
222
+
223
+ # Generate unique project ID
224
+ internal_project_id = str(uuid.uuid4())
225
+
226
+ # Create project
227
+ project = Project(
228
+ id=internal_project_id,
229
+ name=project_name,
230
+ database_type=DatabaseType.SPANNER,
231
+ database_config=database_config
232
+ )
233
+
234
+ # Store project
235
+ self.projects[internal_project_id] = project
236
+
237
+ # Save to persistent storage
238
+ try:
239
+ # Try to create task if there's a running event loop
240
+ loop = asyncio.get_running_loop()
241
+ loop.create_task(self._save_project_async(project))
242
+ except RuntimeError:
243
+ # No running event loop, run synchronously
244
+ asyncio.run(self._save_project_async(project))
245
+
246
+ print(f"✅ Added Spanner project: {project_name}")
247
+ print(f" GCP Project ID: {project_id}")
248
+ print(f" Instance ID: {instance_id}")
249
+ print(f" Database ID: {database_id}")
250
+ print(f" Graph Name: {graph_name or 'default'}")
251
+ print(f" Internal ID: {internal_project_id}")
252
+
253
+ return internal_project_id
254
+
255
+ def add_database(
256
+ self,
257
+ project_name: str,
258
+ project_id: str,
259
+ instance_id: str,
260
+ database_id: str,
261
+ credentials: str,
262
+ graph_name: Optional[str] = None
263
+ ) -> str:
264
+ """
265
+ Add a Spanner database configuration (deprecated, use add_project instead)
266
+
267
+ This method is kept for backward compatibility. Please use add_project() instead.
268
+
269
+ Args:
270
+ project_name: Name of the project
271
+ project_id: Google Cloud Project ID
272
+ instance_id: Spanner Instance ID
273
+ database_id: Spanner Database ID
274
+ credentials: Service Account credentials
275
+ graph_name: Optional graph name
276
+
277
+ Returns:
278
+ Internal Project ID
279
+ """
280
+ import warnings
281
+ warnings.warn(
282
+ "add_database() is deprecated and will be removed in a future version. "
283
+ "Please use add_project() instead.",
284
+ DeprecationWarning,
285
+ stacklevel=2
286
+ )
287
+
288
+ return self.add_project(
289
+ project_name=project_name,
290
+ database_type="spanner",
291
+ project_id=project_id,
292
+ instance_id=instance_id,
293
+ database_id=database_id,
294
+ credentials=credentials,
295
+ graph_name=graph_name
296
+ )
297
+
298
+ async def _save_project_async(self, project: Project) -> None:
299
+ """Save project to persistent storage asynchronously"""
300
+ try:
301
+ from .models.project import ProjectCreate
302
+ project_create = ProjectCreate(
303
+ name=project.name,
304
+ database_type=project.database_type,
305
+ database_config=project.database_config
306
+ )
307
+ await self.project_service.create_project(project_create)
308
+ except Exception as e:
309
+ print(f"Warning: Could not save project to persistent storage: {e}")
310
+
311
+ def remove_project(self, project_id: str) -> bool:
312
+ """
313
+ Remove a project configuration
314
+
315
+ Args:
316
+ project_id: ID of the project to remove
317
+
318
+ Returns:
319
+ True if removed successfully, False if not found
320
+ """
321
+ if project_id in self.projects:
322
+ project = self.projects[project_id]
323
+ del self.projects[project_id]
324
+ print(f"✅ Removed project: {project.name}")
325
+ return True
326
+ print(f"❌ Project not found: {project_id}")
327
+ return False
328
+
329
+ def remove_database(self, project_id: str) -> bool:
330
+ """
331
+ Remove a database configuration (deprecated, use remove_project instead)
332
+
333
+ This method is kept for backward compatibility. Please use remove_project() instead.
334
+ """
335
+ import warnings
336
+ warnings.warn(
337
+ "remove_database() is deprecated and will be removed in a future version. "
338
+ "Please use remove_project() instead.",
339
+ DeprecationWarning,
340
+ stacklevel=2
341
+ )
342
+ return self.remove_project(project_id)
343
+
344
+ def list_projects(self) -> Dict[str, Dict[str, Any]]:
345
+ """
346
+ List all configured projects
347
+
348
+ Returns:
349
+ Dictionary mapping project IDs to project information
350
+ """
351
+ return {
352
+ project_id: {
353
+ "name": project.name,
354
+ "database_type": project.database_type.value,
355
+ "project_id": project.database_config.project_id,
356
+ "instance_id": project.database_config.instance_id,
357
+ "database_id": project.database_config.database_id,
358
+ "graph_name": project.database_config.graph_name,
359
+ "auth_type": project.database_config.auth_type.value,
360
+ "created_at": project.create_time.isoformat(),
361
+ "api_endpoint": f"/api/projects/{project_id}"
362
+ }
363
+ for project_id, project in self.projects.items()
364
+ }
365
+
366
+ def list_databases(self) -> Dict[str, Dict[str, Any]]:
367
+ """
368
+ List all configured databases (deprecated, use list_projects instead)
369
+
370
+ This method is kept for backward compatibility. Please use list_projects() instead.
371
+ """
372
+ import warnings
373
+ warnings.warn(
374
+ "list_databases() is deprecated and will be removed in a future version. "
375
+ "Please use list_projects() instead.",
376
+ DeprecationWarning,
377
+ stacklevel=2
378
+ )
379
+ return self.list_projects()
380
+
381
+ def _find_project_by_name(self, project_name: str) -> Optional[str]:
382
+ """
383
+ Find project ID by project name
384
+
385
+ Args:
386
+ project_name: Name of the project to find
387
+
388
+ Returns:
389
+ Project ID if found, None otherwise
390
+ """
391
+ for project_id, project in self.projects.items():
392
+ if project.name == project_name:
393
+ return project_id
394
+ return None
395
+
396
+ def get_project_apis(self, project_identifier: Optional[str] = None) -> Dict[str, Any]:
397
+ """
398
+ Get API endpoints for projects
399
+
400
+ Args:
401
+ project_identifier: Project ID or project name, or None for all projects
402
+
403
+ Returns:
404
+ Dictionary with API endpoint information
405
+ """
406
+ if project_identifier:
407
+ # First try to find by project_id
408
+ project_id = project_identifier
409
+
410
+ # If not found by ID, try to find by name
411
+ if project_id not in self.projects:
412
+ found_id = self._find_project_by_name(project_identifier)
413
+ if found_id:
414
+ project_id = found_id
415
+ else:
416
+ return {"error": f"Project not found by ID or name: {project_identifier}"}
417
+
418
+ project = self.projects[project_id]
419
+ return {
420
+ "project_id": project_id,
421
+ "name": project.name,
422
+ "endpoints": {
423
+ "base": f"/api/projects/{project_id}",
424
+ "query": f"/api/projects/{project_id}/query",
425
+ "schema": f"/api/projects/{project_id}/schema",
426
+ "health": f"/api/projects/{project_id}/health"
427
+ }
428
+ }
429
+ else:
430
+ # Return all projects
431
+ return {
432
+ "projects": {
433
+ pid: {
434
+ "name": project.name,
435
+ "endpoints": {
436
+ "base": f"/api/projects/{pid}",
437
+ "query": f"/api/projects/{pid}/query",
438
+ "schema": f"/api/projects/{pid}/schema",
439
+ "health": f"/api/projects/{pid}/health"
440
+ }
441
+ }
442
+ for pid, project in self.projects.items()
443
+ }
444
+ }
445
+
446
+ def start(
447
+ self,
448
+ host: str = "0.0.0.0",
449
+ port: int = 9080,
450
+ dev: bool = False,
451
+ show_apis: bool = True
452
+ ) -> None:
453
+ """
454
+ Start the GraphXR Database Proxy server
455
+ Args:
456
+ host: Host to bind to (default: "0.0.0.0")
457
+ port: Port to bind to (default: 9080)
458
+ show_apis: Show API endpoints information (default: True)
459
+ """
460
+ print("\n🚀 Starting GraphXR Database Proxy...")
461
+ print(f" Web UI: http://{host if host != '0.0.0.0' else 'localhost'}:{port}")
462
+
463
+ if show_apis and self.projects:
464
+ print("\n📡 Available API Endpoints:")
465
+ for project_id, project in self.projects.items():
466
+ databaseType = project.database_type._value_ if hasattr(project.database_type, '_value_') else project.database_type
467
+ print(f" Project: {project.name}")
468
+ base_url = f"http://{host if host != '0.0.0.0' else 'localhost'}:{port}/api/{databaseType}/{project.name}"
469
+ print(f" - API URL(GraphXR): {base_url}")
470
+ print(f" - Query API : {base_url}/query")
471
+ print(f" - Schema API : {base_url}/schema")
472
+
473
+ # Configure logging
474
+ import logging
475
+ logging.basicConfig(
476
+ level=logging.INFO if not dev else logging.DEBUG,
477
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
478
+ )
479
+
480
+ # Run the server
481
+ try:
482
+ uvicorn.run(
483
+ "graphxr_database_proxy.main:app",
484
+ host=host,
485
+ port=port,
486
+ reload=dev,
487
+ log_level="info" if not dev else "debug"
488
+ )
489
+ except KeyboardInterrupt:
490
+ print("\n⏹️ Stopping GraphXR Database Proxy...")
491
+ self.stop()
492
+
493
+ def stop(self) -> None:
494
+ """Stop the server"""
495
+ print("✅ GraphXR Database Proxy stopped")