Soumik Bose commited on
Commit
369b363
·
1 Parent(s): 95d1612

update mongo exec

Browse files
Files changed (3) hide show
  1. controller.py +68 -12
  2. mongo_service.py +71 -102
  3. pydantic_mongo_executor_model.py +3 -6
controller.py CHANGED
@@ -13,7 +13,6 @@ import psutil
13
  import ssl
14
  from typing import List, Optional, Dict, Any, TypeVar, Generic
15
  from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
16
- from threading import Lock
17
 
18
  # --- FastAPI & Core ---
19
  from fastapi import FastAPI, HTTPException, Depends
@@ -32,16 +31,13 @@ from asyncmy.cursors import DictCursor
32
 
33
  # --- Database Drivers (Sync - unused but kept for imports) ---
34
  from bson import ObjectId
35
- import mysql.connector
36
- import psycopg2
37
- from psycopg2.extras import RealDictCursor
38
  import uvicorn
39
 
40
  # --- Existing Services ---
41
  from csv_analysis_service import execute_analysis_logic
42
  from csv_chart_service import execute_python_code
43
  from csv_metadata_service import CsvDataRequest, CsvInfoRequest, CsvInfoResponse, PythonExecutionRequest, PythonExecutionResponse, execute_python_logic, get_csv_basic_info, get_robust_csv_rows
44
- from mongo_service import execute_mongo_operation, parse_query_input
45
  from pydantic_csv_analysis_model import AnalysisRequest, AnalysisResponse
46
  from pydantic_csv_charts_model import ChartExecutionPayload, ChartExecutionResponse
47
  from pydantic_mongo_executor_model import ExecutorPayload, ExecutorResponse
@@ -452,19 +448,79 @@ async def execute_postgres_endpoint(query: PgQueryRequest, token: str = Depends(
452
  async def execute_mongo_endpoint(payload: ExecutorPayload, token: str = Depends(validate_token)):
453
  request_id = str(uuid.uuid4())[:8]
454
  start_time = time.time()
 
455
  try:
456
- parsed_query = parse_query_input(payload.generated_query)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  unique_params = {
458
- "uri": payload.mongo_uri, "db": payload.db_name, "col": payload.collection_name,
459
- "q": parsed_query, "lim": payload.limited, "lrows": payload.limit_rows
 
 
 
460
  }
 
 
 
461
  result_data = await coalescer.execute(
462
- "mongo", unique_params, run_in_threadpool, execute_mongo_operation,
463
- mongo_uri=payload.mongo_uri, db_name=payload.db_name, collection_name=payload.collection_name,
464
- query=parsed_query, limited=payload.limited, limit_rows=payload.limit_rows
 
 
 
 
 
 
 
465
  )
466
- return ExecutorResponse(status="success", count=len(result_data), data=jsonable_encoder(result_data, custom_encoder={ObjectId: str}), duration_seconds=round(time.time() - start_time, 4), request_id=request_id)
 
 
 
 
 
 
 
 
 
 
467
  except Exception as e:
 
468
  raise HTTPException(status_code=500, detail=str(e))
469
 
470
  @app.post("/api/execute_chart", response_model=ChartExecutionResponse)
 
13
  import ssl
14
  from typing import List, Optional, Dict, Any, TypeVar, Generic
15
  from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
 
16
 
17
  # --- FastAPI & Core ---
18
  from fastapi import FastAPI, HTTPException, Depends
 
31
 
32
  # --- Database Drivers (Sync - unused but kept for imports) ---
33
  from bson import ObjectId
 
 
 
34
  import uvicorn
35
 
36
  # --- Existing Services ---
37
  from csv_analysis_service import execute_analysis_logic
38
  from csv_chart_service import execute_python_code
39
  from csv_metadata_service import CsvDataRequest, CsvInfoRequest, CsvInfoResponse, PythonExecutionRequest, PythonExecutionResponse, execute_python_logic, get_csv_basic_info, get_robust_csv_rows
40
+ from mongo_service import convert_oid, execute_mongo_operation, extract_database_name, sanitize_json_input
41
  from pydantic_csv_analysis_model import AnalysisRequest, AnalysisResponse
42
  from pydantic_csv_charts_model import ChartExecutionPayload, ChartExecutionResponse
43
  from pydantic_mongo_executor_model import ExecutorPayload, ExecutorResponse
 
448
  async def execute_mongo_endpoint(payload: ExecutorPayload, token: str = Depends(validate_token)):
449
  request_id = str(uuid.uuid4())[:8]
450
  start_time = time.time()
451
+
452
  try:
453
+ # 1. Determine Database Name
454
+ final_db_name = payload.db_name
455
+
456
+ # If DB Name is missing in payload, try to extract it from URI
457
+ if not final_db_name:
458
+ final_db_name = extract_database_name(payload.mongo_uri)
459
+
460
+ # Final check
461
+ if not final_db_name:
462
+ raise HTTPException(
463
+ status_code=400,
464
+ detail="Database name not provided in payload and could not be extracted from the URI path."
465
+ )
466
+
467
+ # 2. Parse Query & Determine Collection Name
468
+ parsed_input = sanitize_json_input(payload.generated_query)
469
+ final_query = parsed_input
470
+ final_collection = payload.collection_name
471
+
472
+ # Check for structured AI response: { "data": [ { "collectionName": "...", "pipeline": [...] } ] }
473
+ if isinstance(parsed_input, dict) and "data" in parsed_input and isinstance(parsed_input["data"], list):
474
+ if len(parsed_input["data"]) > 0:
475
+ extracted_data = parsed_input["data"][0]
476
+
477
+ # Extract Pipeline
478
+ if "pipeline" in extracted_data:
479
+ final_query = extracted_data["pipeline"]
480
+
481
+ # Extract Collection Name (Overrides payload if present)
482
+ if "collectionName" in extracted_data and extracted_data["collectionName"]:
483
+ final_collection = extracted_data["collectionName"]
484
+
485
+ if not final_collection:
486
+ raise HTTPException(status_code=400, detail="Collection name could not be determined from payload or query.")
487
+
488
+ # 3. Execute
489
  unique_params = {
490
+ "uri": payload.mongo_uri,
491
+ "db": final_db_name,
492
+ "col": final_collection,
493
+ "q": str(final_query),
494
+ "lim": payload.limited
495
  }
496
+
497
+ final_query = convert_oid(final_query)
498
+
499
  result_data = await coalescer.execute(
500
+ "mongo",
501
+ unique_params,
502
+ run_in_threadpool,
503
+ execute_mongo_operation,
504
+ mongo_uri=payload.mongo_uri,
505
+ db_name=final_db_name,
506
+ collection_name=final_collection,
507
+ query=final_query,
508
+ limited=payload.limited,
509
+ limit_rows=payload.limit_rows
510
  )
511
+
512
+ return ExecutorResponse(
513
+ status="success",
514
+ count=len(result_data),
515
+ data=jsonable_encoder(result_data, custom_encoder={ObjectId: str}),
516
+ duration_seconds=round(time.time() - start_time, 4),
517
+ request_id=request_id
518
+ )
519
+
520
+ except HTTPException as he:
521
+ raise he
522
  except Exception as e:
523
+ logger.error(f"Endpoint Error: {e}")
524
  raise HTTPException(status_code=500, detail=str(e))
525
 
526
  @app.post("/api/execute_chart", response_model=ChartExecutionResponse)
mongo_service.py CHANGED
@@ -1,11 +1,13 @@
1
  import logging
2
- from typing import Dict, List, Union
3
  from bson import ObjectId
4
  import ast
5
  import json
6
  import re
7
  from pymongo import MongoClient
8
- from threading import Lock # Needed for thread-safe pooling
 
 
9
 
10
  logging.basicConfig(
11
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
@@ -14,7 +16,7 @@ logging.basicConfig(
14
  logger = logging.getLogger("mongo_service")
15
 
16
  # ==============================================================================
17
- # MONGO CONNECTION POOL MANAGER
18
  # ==============================================================================
19
  class MongoConnectionManager:
20
  def __init__(self):
@@ -22,83 +24,57 @@ class MongoConnectionManager:
22
  self._lock = Lock()
23
 
24
  def get_client(self, uri: str) -> MongoClient:
25
- """
26
- Returns a cached MongoClient instance.
27
- If it doesn't exist, creates one with a connection pool.
28
- """
29
- # Double-checked locking for performance
30
  if uri not in self._clients:
31
  with self._lock:
32
  if uri not in self._clients:
33
  logger.info(f"Initialize new MongoDB Client for URI: {uri[:20]}...")
34
- # maxPoolSize=50 allows 50 concurrent ops per URI.
35
- # The rest will wait in queue automatically.
36
  self._clients[uri] = MongoClient(
37
  uri,
38
  serverSelectionTimeoutMS=5000,
39
- connectTimeoutMS=5000,
40
- maxPoolSize=50,
41
- minPoolSize=1
42
  )
43
  return self._clients[uri]
44
 
45
- # Global Manager Instance
46
  mongo_manager = MongoConnectionManager()
47
 
48
  # ==============================================================================
49
- # HELPER FUNCTIONS (Unchanged)
50
  # ==============================================================================
51
-
52
  def convert_oid(obj):
53
- """Recursively convert $oid format or ObjectId objects to strings."""
54
- if isinstance(obj, ObjectId):
55
- return str(obj)
56
  if isinstance(obj, dict):
57
- if set(obj.keys()) == {"$oid"}:
58
- return str(obj["$oid"])
59
  return {k: convert_oid(v) for k, v in obj.items()}
60
  elif isinstance(obj, list):
61
  return [convert_oid(item) for item in obj]
62
- else:
63
- return obj
64
 
65
- def parse_query_input(query_input: Union[str, Dict, List]) -> Union[Dict, List]:
66
- """Ensures the input is a valid MongoDB query object (Dict or List)."""
67
- if isinstance(query_input, (dict, list)):
68
- return convert_oid(query_input)
69
 
70
  query_str = str(query_input).strip()
 
 
 
 
 
 
71
 
72
  try:
73
  return json.loads(query_str)
74
- except json.JSONDecodeError:
75
- pass
76
-
77
- match = re.search(r"```(?:python|json|javascript)?\s*(.*?)\s*```", query_str, re.DOTALL)
78
- if match:
79
- query_str = match.group(1).strip()
80
-
81
- query_str = re.sub(r'^db\.\w+\.\w+\(', '', query_str)
82
- if query_str.endswith(')'):
83
- query_str = query_str[:-1]
84
-
85
- try:
86
- parsed = ast.literal_eval(query_str)
87
- return convert_oid(parsed)
88
- except (ValueError, SyntaxError) as e:
89
- logger.error(f"Failed to parse query string: {e}")
90
- raise ValueError(f"Could not parse query string: {str(e)}")
91
-
92
- def get_value_ignore_case(d: Dict, keys: List[str], default=None):
93
- for k in keys:
94
- if k in d:
95
- return d[k]
96
- return default
97
 
98
  # ==============================================================================
99
- # MAIN EXECUTION LOGIC (Modified)
100
  # ==============================================================================
101
-
102
  def execute_mongo_operation(
103
  mongo_uri: str,
104
  db_name: str,
@@ -107,73 +83,66 @@ def execute_mongo_operation(
107
  limited: bool = False,
108
  limit_rows: int = 20
109
  ):
110
- """
111
- Executes MongoDB operations using the shared connection pool.
112
- """
113
- # 1. GET CLIENT FROM MANAGER (Do NOT create new MongoClient here)
114
  client = mongo_manager.get_client(mongo_uri)
115
 
116
  try:
117
  db = client[db_name]
118
  collection = db[collection_name]
119
-
120
  results = []
121
 
122
- # --- Aggregation ---
123
  if isinstance(query, list):
124
- # Apply Limit if requested and not already present at the end
125
  if limited:
126
- if not (query and "$limit" in query[-1]):
127
- query.append({"$limit": limit_rows})
128
-
129
- cursor = collection.aggregate(query, allowDiskUse=True)
130
  results = list(cursor)
131
 
132
- # --- Find / Command ---
133
  elif isinstance(query, dict):
134
- command_keys = {'filter', 'query', '$query', 'projection', 'sort', 'limit', 'skip'}
135
- has_command_keys = bool(set(query.keys()) & command_keys)
136
-
137
- cursor = None
138
-
139
- if has_command_keys and ('filter' in query or 'query' in query or '$query' in query):
140
- query_filter = get_value_ignore_case(query, ['filter', 'query', '$query'], {})
141
- projection = get_value_ignore_case(query, ['projection', 'fields'], None)
142
- if projection == {}: projection = None
143
-
144
- internal_limit = int(get_value_ignore_case(query, ['limit'], 0))
145
- skip = int(get_value_ignore_case(query, ['skip'], 0))
146
- sort_val = get_value_ignore_case(query, ['sort', '$orderby'], None)
147
-
148
- cursor = collection.find(query_filter, projection)
149
-
150
- if sort_val:
151
- if isinstance(sort_val, dict):
152
- sort_val = list(sort_val.items())
153
- cursor = cursor.sort(sort_val)
154
-
155
- if skip > 0: cursor = cursor.skip(skip)
156
-
157
- if limited:
158
- cursor = cursor.limit(limit_rows)
159
- elif internal_limit > 0:
160
- cursor = cursor.limit(internal_limit)
161
-
162
- else:
163
- cursor = collection.find(query)
164
- if limited:
165
- cursor = cursor.limit(limit_rows)
166
-
167
  results = list(cursor)
 
168
  else:
169
- raise ValueError("Query must be a Dictionary (find) or List (aggregate)")
170
 
171
  return results
172
-
173
  except Exception as e:
174
  logger.error(f"DB Execution Error: {e}")
175
  raise e
176
 
177
- # IMPORTANT: DO NOT CLOSE THE CLIENT
178
- # finally:
179
- # if client: client.close() <-- REMOVED
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import logging
2
+ from typing import Dict, List, Union, Any
3
  from bson import ObjectId
4
  import ast
5
  import json
6
  import re
7
  from pymongo import MongoClient
8
+ from threading import Lock
9
+ from urllib.parse import urlparse
10
+ from pymongo.uri_parser import parse_uri
11
 
12
  logging.basicConfig(
13
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
 
16
  logger = logging.getLogger("mongo_service")
17
 
18
  # ==============================================================================
19
+ # MONGO CONNECTION MANAGER
20
  # ==============================================================================
21
  class MongoConnectionManager:
22
  def __init__(self):
 
24
  self._lock = Lock()
25
 
26
  def get_client(self, uri: str) -> MongoClient:
 
 
 
 
 
27
  if uri not in self._clients:
28
  with self._lock:
29
  if uri not in self._clients:
30
  logger.info(f"Initialize new MongoDB Client for URI: {uri[:20]}...")
 
 
31
  self._clients[uri] = MongoClient(
32
  uri,
33
  serverSelectionTimeoutMS=5000,
34
+ connectTimeoutMS=5000,
35
+ maxPoolSize=50
 
36
  )
37
  return self._clients[uri]
38
 
 
39
  mongo_manager = MongoConnectionManager()
40
 
41
  # ==============================================================================
42
+ # HELPER FUNCTIONS
43
  # ==============================================================================
 
44
  def convert_oid(obj):
45
+ """Recursively convert $oid or ObjectId to strings."""
46
+ if isinstance(obj, ObjectId): return str(obj)
 
47
  if isinstance(obj, dict):
48
+ if set(obj.keys()) == {"$oid"}: return str(obj["$oid"])
 
49
  return {k: convert_oid(v) for k, v in obj.items()}
50
  elif isinstance(obj, list):
51
  return [convert_oid(item) for item in obj]
52
+ return obj
 
53
 
54
+ def sanitize_json_input(query_input: Union[str, Dict, List]) -> Union[Dict, List]:
55
+ """Parses input string/dict/list into Python objects, handling ObjectIds."""
56
+ if isinstance(query_input, (dict, list)): return query_input
 
57
 
58
  query_str = str(query_input).strip()
59
+ match = re.search(r"```(?:json|javascript)?\s*(.*?)\s*```", query_str, re.DOTALL)
60
+ if match: query_str = match.group(1).strip()
61
+
62
+ # Sanitize JS specific wrappers
63
+ query_str = re.sub(r"ObjectId\(['\"]([a-fA-F0-9]+)['\"]\)", r"'\1'", query_str)
64
+ query_str = re.sub(r"ISODate\(['\"](.*?)['\"]\)", r"'\1'", query_str)
65
 
66
  try:
67
  return json.loads(query_str)
68
+ except:
69
+ try:
70
+ return ast.literal_eval(query_str)
71
+ except Exception as e:
72
+ logger.error(f"Failed to parse query: {e}")
73
+ raise ValueError(f"Parsing error: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
  # ==============================================================================
76
+ # MAIN EXECUTION LOGIC
77
  # ==============================================================================
 
78
  def execute_mongo_operation(
79
  mongo_uri: str,
80
  db_name: str,
 
83
  limited: bool = False,
84
  limit_rows: int = 20
85
  ):
 
 
 
 
86
  client = mongo_manager.get_client(mongo_uri)
87
 
88
  try:
89
  db = client[db_name]
90
  collection = db[collection_name]
 
91
  results = []
92
 
93
+ # --- Pipeline (List) ---
94
  if isinstance(query, list):
95
+ pipeline = query
96
  if limited:
97
+ if not (pipeline and "$limit" in pipeline[-1]):
98
+ pipeline.append({"$limit": limit_rows})
99
+ cursor = collection.aggregate(pipeline, allowDiskUse=True)
 
100
  results = list(cursor)
101
 
102
+ # --- Find (Dict) ---
103
  elif isinstance(query, dict):
104
+ cursor = collection.find(query)
105
+ if limited: cursor = cursor.limit(limit_rows)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  results = list(cursor)
107
+
108
  else:
109
+ raise ValueError("Query must be a Pipeline (List) or Filter (Dict)")
110
 
111
  return results
 
112
  except Exception as e:
113
  logger.error(f"DB Execution Error: {e}")
114
  raise e
115
 
116
+ def extract_database_name(uri: str) -> str:
117
+ """
118
+ Robustly extracts database name from a MongoDB URI.
119
+ 1. Tries PyMongo's strict parser.
120
+ 2. Falls back to standard URL path extraction (like your TS code).
121
+ """
122
+ if not uri:
123
+ return None
124
+
125
+ # Method 1: PyMongo Strict Parser
126
+ try:
127
+ parsed = parse_uri(uri)
128
+ if parsed.get('database'):
129
+ return parsed['database']
130
+ except Exception:
131
+ pass # Fallback if PyMongo complains about the URI format
132
+
133
+ # Method 2: Generic URL Parsing (Mimics your TypeScript logic)
134
+ try:
135
+ # Standard URL parsing
136
+ result = urlparse(uri)
137
+
138
+ # The database name is usually the 'path' part (e.g., /api_platform)
139
+ path = result.path
140
+
141
+ # Remove leading slash if present
142
+ if path.startswith('/'):
143
+ path = path[1:]
144
+
145
+ # If empty, return None
146
+ return path if path else None
147
+ except Exception:
148
+ return None
pydantic_mongo_executor_model.py CHANGED
@@ -1,15 +1,12 @@
1
-
2
  # --- Mongo Models ---
3
  from typing import Any, Dict, List, Optional, Union
4
  from pydantic import BaseModel, Field
5
 
6
-
7
  class ExecutorPayload(BaseModel):
8
  mongo_uri: str = Field(..., description="MongoDB connection string")
9
- db_name: str = Field(..., description="Target Database Name")
10
- collection_name: str = Field(..., description="Target Collection Name")
11
- generated_query: Union[Dict, List, str] = Field(..., description="The query to execute (Find dict or Aggregation list)")
12
- user_query: Optional[str] = Field(None, description="Original user question for logging context")
13
  limited: bool = False
14
  limit_rows: int = 20
15
 
 
 
1
  # --- Mongo Models ---
2
  from typing import Any, Dict, List, Optional, Union
3
  from pydantic import BaseModel, Field
4
 
 
5
  class ExecutorPayload(BaseModel):
6
  mongo_uri: str = Field(..., description="MongoDB connection string")
7
+ db_name: Optional[str] = Field(None, description="Target Database Name")
8
+ collection_name: Optional[str] = Field(None, description="Target Collection Name")
9
+ generated_query: Union[Dict, List, str] = Field(..., description="Query Payload")
 
10
  limited: bool = False
11
  limit_rows: int = 20
12