dc-python-sdk 1.5.22__tar.gz → 1.5.24__tar.gz

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 (35) hide show
  1. {dc_python_sdk-1.5.22/src/dc_python_sdk.egg-info → dc_python_sdk-1.5.24}/PKG-INFO +1 -1
  2. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/pyproject.toml +1 -1
  3. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/setup.cfg +1 -1
  4. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24/src/dc_python_sdk.egg-info}/PKG-INFO +1 -1
  5. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/cli.py +3 -4
  6. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/ai_http.py +240 -232
  7. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/server.py +17 -7
  8. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/LICENSE +0 -0
  9. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/README.md +0 -0
  10. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_python_sdk.egg-info/SOURCES.txt +0 -0
  11. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_python_sdk.egg-info/dependency_links.txt +0 -0
  12. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_python_sdk.egg-info/entry_points.txt +0 -0
  13. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_python_sdk.egg-info/requires.txt +0 -0
  14. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_python_sdk.egg-info/top_level.txt +0 -0
  15. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/__init__.py +0 -0
  16. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/app.py +0 -0
  17. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/errors.py +0 -0
  18. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/handler.py +0 -0
  19. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/__init__.py +0 -0
  20. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/ai.py +0 -0
  21. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/mapping.py +0 -0
  22. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/models/__init__.py +0 -0
  23. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/models/enums.py +0 -0
  24. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/models/errors.py +0 -0
  25. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/models/log_templates.py +0 -0
  26. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/models/pipeline_details.py +0 -0
  27. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/pipeline.py +0 -0
  28. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/services/__init__.py +0 -0
  29. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/services/api.py +0 -0
  30. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/services/aws.py +0 -0
  31. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/services/environment.py +0 -0
  32. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/services/loader.py +0 -0
  33. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/services/logger.py +0 -0
  34. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/src/services/session.py +0 -0
  35. {dc_python_sdk-1.5.22 → dc_python_sdk-1.5.24}/src/dc_sdk/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dc-python-sdk
3
- Version: 1.5.22
3
+ Version: 1.5.24
4
4
  Summary: Data Connector Python SDK
5
5
  Home-page: https://github.com/data-connector/dc-python-sdk
6
6
  Author: DataConnector
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dc-python-sdk"
7
- version = "1.5.22"
7
+ version = "1.5.24"
8
8
  description = "Data Connector Python SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.6"
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = dc-python-sdk
3
- version = 1.5.22
3
+ version = 1.5.24
4
4
  author = DataConnector
5
5
  author_email = josh@dataconnector.com
6
6
  description = A small example package
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dc-python-sdk
3
- Version: 1.5.22
3
+ Version: 1.5.24
4
4
  Summary: Data Connector Python SDK
5
5
  Home-page: https://github.com/data-connector/dc-python-sdk
6
6
  Author: DataConnector
@@ -1,5 +1,6 @@
1
1
  import sys
2
2
  from importlib.metadata import version
3
+ import os
3
4
 
4
5
  def get_arg(name, default=None):
5
6
  if name in sys.argv:
@@ -15,15 +16,13 @@ def main():
15
16
 
16
17
  command = sys.argv[1]
17
18
 
18
- port = int(get_arg("--port", 5000))
19
-
20
19
  print("version: ", version("dc-python-sdk"))
21
20
 
22
21
  print(f"[DC SDK] Command: {command}")
23
22
 
24
23
  if command == "http":
25
24
  from dc_sdk.src.server import start_server
26
- start_server(port)
25
+ start_server()
27
26
 
28
27
  elif command == "ai":
29
28
  from dc_sdk.src.ai import start_ai
@@ -31,7 +30,7 @@ def main():
31
30
 
32
31
  elif command == "ai-http":
33
32
  from dc_sdk.src.ai_http import start_ai_http
34
- start_ai_http(connector_port=port)
33
+ start_ai_http()
35
34
 
36
35
  else:
37
36
  print(f"Unknown command: {command}")
@@ -18,15 +18,25 @@ logging.basicConfig(level=logging.INFO)
18
18
 
19
19
  app = FastAPI()
20
20
 
21
+ _DEFAULT_PROCESS_TTL_SECONDS = 600.0
22
+
23
+ # Default ports: connector HTTP (dc-sdk http) 5000, AI FastAPI 5001, code-server 5002.
24
+ CONNECTOR_PORT = os.getenv("CONNECTOR_PORT", 5000)
25
+ AI_PORT = os.getenv("AI_PORT", 5001)
26
+ CODE_SERVER_PORT = os.getenv("CODE_SERVER_PORT", 5002)
27
+
21
28
  client = None
22
29
  workspace = os.getenv("WORKSPACE", "/workspace")
23
- port_for_connector = 5000
24
30
 
25
31
  # Process-wide TTL for ai-http (see DC_SDK_TTL_SECONDS); exposed in /session-info.
26
32
  process_ttl_seconds: Optional[float] = None
27
33
  process_ttl_deadline_unix: Optional[float] = None
28
34
  _process_ttl_timer: Optional[threading.Timer] = None
29
35
 
36
+ # Long-lived `dc-sdk http` subprocess (uvicorn with CONNECTOR_HTTP_RELOAD reloads connector.py).
37
+ _connector_proc: Optional[subprocess.Popen] = None
38
+ _connector_lock = threading.Lock()
39
+
30
40
  # -----------------------------
31
41
  # MODELS
32
42
  # -----------------------------
@@ -65,21 +75,6 @@ class RunRequest(BaseModel):
65
75
  test_object_ids: Optional[List[str]] = None
66
76
 
67
77
 
68
- # -----------------------------
69
- # UTIL
70
- # -----------------------------
71
- def get_free_port():
72
- import socket
73
- s = socket.socket()
74
- s.bind(('', 0))
75
- port = s.getsockname()[1]
76
- s.close()
77
- return port
78
-
79
-
80
- _DEFAULT_PROCESS_TTL_SECONDS = 600.0
81
-
82
-
83
78
  def _parse_process_ttl_seconds() -> float:
84
79
  """
85
80
  Wall-clock lifetime for the whole ai-http process from start_ai_http() entry.
@@ -104,43 +99,68 @@ def _parse_process_ttl_seconds() -> float:
104
99
  return 0.0
105
100
  return v
106
101
 
102
+ def _wait_connector_healthy() -> None:
103
+ for _ in range(50):
104
+ try:
105
+ res = requests.get(f"http://localhost:{CONNECTOR_PORT}/health", timeout=2)
106
+ if res.status_code == 200:
107
+ logger.info("Connector ready on port %s", CONNECTOR_PORT)
108
+ return
109
+ except Exception:
110
+ logger.debug("Health check not ready yet on port %s", CONNECTOR_PORT)
111
+ time.sleep(0.1)
112
+ raise RuntimeError(
113
+ f"Connector failed to become healthy on port {CONNECTOR_PORT} within timeout"
114
+ )
115
+
107
116
 
108
- def start_connector():
109
- global workspace
110
- global port_for_connector
117
+ def _start_connector_subprocess_unlocked() -> None:
118
+ global _connector_proc, workspace
119
+ env = os.environ.copy()
120
+ env["CONNECTOR_HTTP_RELOAD"] = "1"
111
121
  proc = subprocess.Popen(
112
- ["dc-sdk", "http", "--port", str(port_for_connector)],
122
+ ["dc-sdk", "http"],
113
123
  cwd=workspace,
124
+ env=env,
114
125
  stdout=subprocess.DEVNULL,
115
- stderr=subprocess.DEVNULL
126
+ stderr=subprocess.DEVNULL,
116
127
  )
117
-
118
- # wait for health
119
- for _ in range(50):
128
+ try:
129
+ _wait_connector_healthy()
130
+ except Exception:
131
+ proc.terminate()
120
132
  try:
121
- res = requests.get(f"http://localhost:{port_for_connector}/health")
122
- if res.status_code == 200:
123
- logger.info("Connector ready on port %s", port_for_connector)
124
- return {"process": proc, "port": port_for_connector}
133
+ proc.wait(timeout=5)
125
134
  except Exception:
126
- logger.debug("Health check not ready yet on port %s", port_for_connector)
127
- time.sleep(0.1)
135
+ pass
136
+ raise
137
+ _connector_proc = proc
128
138
 
129
- logger.error("Connector failed to become healthy on port %s within timeout", port_for_connector)
130
- proc.terminate()
131
- raise Exception("Failed to start connector")
132
139
 
133
-
134
- def stop_connector(runtime):
140
+ def _stop_connector_subprocess_unlocked() -> None:
141
+ global _connector_proc
142
+ proc = _connector_proc
143
+ _connector_proc = None
144
+ if proc is None:
145
+ return
135
146
  try:
136
- runtime["process"].terminate()
137
- runtime["process"].wait()
147
+ proc.terminate()
148
+ proc.wait(timeout=15)
138
149
  logger.debug("Connector subprocess stopped")
139
150
  except Exception:
140
151
  logger.debug("Error stopping connector subprocess", exc_info=True)
141
152
 
142
153
 
143
- def invoke(port, method, session_id=None, credentials=None, params=None):
154
+ def start_connector_http_server() -> None:
155
+ """Start the connector HTTP child once; restart if the previous child exited."""
156
+ with _connector_lock:
157
+ if _connector_proc is not None and _connector_proc.poll() is None:
158
+ return
159
+ _stop_connector_subprocess_unlocked()
160
+ _start_connector_subprocess_unlocked()
161
+
162
+
163
+ def invoke(method, session_id=None, credentials=None, params=None):
144
164
  payload = {
145
165
  "method": method,
146
166
  "session_id": session_id,
@@ -148,21 +168,19 @@ def invoke(port, method, session_id=None, credentials=None, params=None):
148
168
  "params": params or {}
149
169
  }
150
170
 
151
- logger.debug("invoke port=%s method=%s session_id=%s", port, method, session_id)
152
- res = requests.post(f"http://localhost:{port}/invoke", json=payload)
171
+ logger.debug("invoke port=%s method=%s session_id=%s", CONNECTOR_PORT, method, session_id)
172
+ res = requests.post(f"http://localhost:{CONNECTOR_PORT}/invoke", json=payload)
153
173
  if res.status_code != 200:
154
174
  logger.warning(
155
175
  "invoke returned HTTP %s for method=%s", res.status_code, method
156
176
  )
157
177
  return res.json()
158
178
 
159
-
160
179
  def read_connector():
161
180
  global workspace
162
181
  with open(f"{workspace}/src/connector.py", "r") as f:
163
182
  return f.read()
164
183
 
165
-
166
184
  def _resolved_connector_context(opt: Optional[ConnectorContext]) -> ConnectorContext:
167
185
  from dc_sdk.src.ai import CONNECTOR_CONTEXT as defaults
168
186
 
@@ -179,7 +197,6 @@ def _resolved_connector_context(opt: Optional[ConnectorContext]) -> ConnectorCon
179
197
  merged[key] = value
180
198
  return ConnectorContext(**merged)
181
199
 
182
-
183
200
  def _chat_messages_to_openai(messages: Optional[List[ChatMessage]]) -> List[Dict[str, str]]:
184
201
  if not messages:
185
202
  return []
@@ -190,7 +207,6 @@ def _chat_messages_to_openai(messages: Optional[List[ChatMessage]]) -> List[Dict
190
207
  out.append({"role": m.role, "content": m.content})
191
208
  return out
192
209
 
193
-
194
210
  def _coerce_prior_dicts(prior: Optional[List[Dict[str, str]]]) -> List[Dict[str, str]]:
195
211
  out: List[Dict[str, str]] = []
196
212
  for m in prior or []:
@@ -200,19 +216,16 @@ def _coerce_prior_dicts(prior: Optional[List[Dict[str, str]]]) -> List[Dict[str,
200
216
  out.append({"role": role, "content": content})
201
217
  return out
202
218
 
203
-
204
219
  def write_connector(code):
205
220
  global workspace
206
221
  with open(f"{workspace}/src/connector.py", "w") as f:
207
222
  f.write(code)
208
223
 
209
-
210
224
  def _canon_object_id(value: Any) -> str:
211
225
  if value is None:
212
226
  return ""
213
227
  return str(value).strip()
214
228
 
215
-
216
229
  def _object_ids_from_get_objects_results(objects: List[Any]) -> List[str]:
217
230
  seen = set()
218
231
  out: List[str] = []
@@ -225,176 +238,165 @@ def _object_ids_from_get_objects_results(objects: List[Any]) -> List[str]:
225
238
  out.append(oid)
226
239
  return out
227
240
 
228
-
229
- # -----------------------------
230
- # TEST FLOW
231
- # -----------------------------
232
241
  def run_test(
233
242
  credentials,
234
243
  *,
235
244
  connector_context: ConnectorContext,
236
245
  test_object_ids: Optional[List[str]] = None,
237
246
  ):
238
- runtime = start_connector()
239
247
  requested_ids = [_canon_object_id(x) for x in (test_object_ids or []) if _canon_object_id(x)]
240
248
  print(requested_ids)
241
249
 
242
- try:
243
- session_id = None
250
+ session_id = None
244
251
 
245
- # authenticate
246
- res = invoke(runtime["port"], "authenticate", None, credentials)
247
- if res.get("results") is not True:
248
- logger.warning("run_test failed at stage=authenticate")
249
- return False, "authenticate", res
252
+ # authenticate
253
+ res = invoke("authenticate", None, credentials)
254
+ if res.get("results") is not True:
255
+ logger.warning("run_test failed at stage=authenticate")
256
+ return False, "authenticate", res
250
257
 
251
- session_id = res.get("session_id")
258
+ session_id = res.get("session_id")
252
259
 
253
- # get_objects
254
- res = invoke(runtime["port"], "get_objects", session_id)
255
- if not isinstance(res.get("results"), list):
256
- logger.warning("run_test failed at stage=get_objects (invalid results type)")
257
- return False, "get_objects", res
258
-
259
- objects = res["results"]
260
- api_ids = _object_ids_from_get_objects_results(objects)
261
- dynamic = connector_context.objects_dynamic
262
-
263
- if dynamic:
264
- if api_ids:
265
- if requested_ids:
266
- api_set = set(api_ids)
267
- wanted = [x for x in requested_ids if x in api_set]
268
- if not wanted:
269
- logger.warning(
270
- "run_test failed: test_object_ids disjoint from get_objects"
271
- )
272
- return False, "get_objects", {
273
- "message": (
274
- f"objects_dynamic is true: none of test_object_ids {requested_ids} "
275
- f"appear in get_objects results {api_ids}."
276
- ),
277
- }
278
- ids_for_fields = wanted
279
- else:
280
- ids_for_fields = list(api_ids)
281
- else:
282
- if not requested_ids:
260
+ # get_objects
261
+ res = invoke("get_objects", session_id)
262
+ if not isinstance(res.get("results"), list):
263
+ logger.warning("run_test failed at stage=get_objects (invalid results type)")
264
+ return False, "get_objects", res
265
+
266
+ objects = res["results"]
267
+ api_ids = _object_ids_from_get_objects_results(objects)
268
+ dynamic = connector_context.objects_dynamic
269
+
270
+ if dynamic:
271
+ if api_ids:
272
+ if requested_ids:
273
+ api_set = set(api_ids)
274
+ wanted = [x for x in requested_ids if x in api_set]
275
+ if not wanted:
283
276
  logger.warning(
284
- "run_test failed: objects_dynamic with empty get_objects and no test_object_ids"
277
+ "run_test failed: test_object_ids disjoint from get_objects"
285
278
  )
286
279
  return False, "get_objects", {
287
280
  "message": (
288
- "objects_dynamic is true: get_objects returned no objects. "
289
- "Provide test_object_ids in the request to probe get_fields and get_data."
281
+ f"objects_dynamic is true: none of test_object_ids {requested_ids} "
282
+ f"appear in get_objects results {api_ids}."
290
283
  ),
291
284
  }
292
- ids_for_fields = list(requested_ids)
285
+ ids_for_fields = wanted
286
+ else:
287
+ ids_for_fields = list(api_ids)
293
288
  else:
294
- print(requested_ids)
295
289
  if not requested_ids:
296
- logger.warning("run_test failed: static objects require test_object_ids")
290
+ logger.warning(
291
+ "run_test failed: objects_dynamic with empty get_objects and no test_object_ids"
292
+ )
297
293
  return False, "get_objects", {
298
294
  "message": (
299
- "objects_dynamic is false: test_object_ids is required and must list "
300
- "object ids you expect get_objects to return."
295
+ "objects_dynamic is true: get_objects returned no objects. "
296
+ "Provide test_object_ids in the request to probe get_fields and get_data."
301
297
  ),
302
298
  }
303
- if not objects:
304
- logger.warning("run_test failed at stage=get_objects (empty list)")
305
- return False, "get_objects", {"message": "No objects returned from get_objects"}
306
-
307
299
  ids_for_fields = list(requested_ids)
308
-
309
- if not ids_for_fields:
310
- logger.warning("run_test failed: no object ids to probe")
300
+ else:
301
+ print(requested_ids)
302
+ if not requested_ids:
303
+ logger.warning("run_test failed: static objects require test_object_ids")
311
304
  return False, "get_objects", {
312
- "message": "No object ids available to test get_fields (empty candidate list).",
313
- }
314
-
315
- objects_with_fields: List[Tuple[str, List[Any]]] = []
316
- last_fields_payload: Any = None
317
-
318
- print(ids_for_fields)
319
-
320
- for object_id in ids_for_fields:
321
- res = invoke(
322
- runtime["port"],
323
- "get_fields",
324
- session_id,
325
- params={"object_id": object_id},
326
- )
327
- if not isinstance(res.get("results"), list):
328
- logger.warning(
329
- "run_test failed at stage=get_fields (invalid results type) object_id=%s",
330
- object_id,
331
- )
332
- return False, "get_fields", res
333
-
334
- fields = res["results"]
335
- last_fields_payload = res
336
- logger.info("get_fields results for object_id=%s: %s", object_id, len(fields))
337
- if fields:
338
- fids = [
339
- f.get("field_id") or f.get("id")
340
- for f in fields[:5]
341
- if (f.get("field_id") or f.get("id")) is not None
342
- ]
343
- if fids:
344
- objects_with_fields.append((object_id, fids))
345
-
346
- if not objects_with_fields:
347
- logger.warning(
348
- "run_test failed: no fields on any object tried ids=%s",
349
- ids_for_fields,
350
- )
351
- return False, "get_fields", {
352
305
  "message": (
353
- f"get_fields returned no usable fields for any tested object_id(s): {ids_for_fields}. "
354
- "Verify object ids and connector field discovery."
306
+ "objects_dynamic is false: test_object_ids is required and must list "
307
+ "object ids you expect get_objects to return."
355
308
  ),
356
- "last_invoke": last_fields_payload,
357
309
  }
310
+ if not objects:
311
+ logger.warning("run_test failed at stage=get_objects (empty list)")
312
+ return False, "get_objects", {"message": "No objects returned from get_objects"}
358
313
 
359
- last_data_res: Any = None
360
- for object_id, field_ids in objects_with_fields:
361
- res = invoke(
362
- runtime["port"],
363
- "get_data",
364
- session_id,
365
- params={
366
- "object_id": object_id,
367
- "field_ids": field_ids,
368
- "n_rows": 5,
369
- },
370
- )
371
- last_data_res = res
372
- if not isinstance(res.get("results"), dict):
373
- logger.warning(
374
- "run_test failed at stage=get_data (invalid results type) object_id=%s",
375
- object_id,
376
- )
377
- return False, "get_data", res
314
+ ids_for_fields = list(requested_ids)
315
+
316
+ if not ids_for_fields:
317
+ logger.warning("run_test failed: no object ids to probe")
318
+ return False, "get_objects", {
319
+ "message": "No object ids available to test get_fields (empty candidate list).",
320
+ }
321
+
322
+ objects_with_fields: List[Tuple[str, List[Any]]] = []
323
+ last_fields_payload: Any = None
378
324
 
379
- payload = res["results"]
380
- rows = payload.get("data")
381
- if isinstance(rows, list) and len(rows) > 0:
382
- logger.info("run_test completed successfully object_id=%s, rows retrieved=%s", object_id, len(rows))
383
- return True, None, None
325
+ print(ids_for_fields)
384
326
 
327
+ for object_id in ids_for_fields:
328
+ res = invoke(
329
+ "get_fields",
330
+ session_id,
331
+ params={"object_id": object_id},
332
+ )
333
+ if not isinstance(res.get("results"), list):
334
+ logger.warning(
335
+ "run_test failed at stage=get_fields (invalid results type) object_id=%s",
336
+ object_id,
337
+ )
338
+ return False, "get_fields", res
339
+
340
+ fields = res["results"]
341
+ last_fields_payload = res
342
+ logger.info("get_fields results for object_id=%s: %s", object_id, len(fields))
343
+ if fields:
344
+ fids = [
345
+ f.get("field_id") or f.get("id")
346
+ for f in fields[:5]
347
+ if (f.get("field_id") or f.get("id")) is not None
348
+ ]
349
+ if fids:
350
+ objects_with_fields.append((object_id, fids))
351
+
352
+ if not objects_with_fields:
385
353
  logger.warning(
386
- "run_test failed: get_data returned no rows for any object with fields"
354
+ "run_test failed: no fields on any object tried ids=%s",
355
+ ids_for_fields,
387
356
  )
388
- return False, "get_data", {
357
+ return False, "get_fields", {
389
358
  "message": (
390
- f"get_data returned no rows for any object that had fields "
391
- f"(tried {len(objects_with_fields)} object(s))."
359
+ f"get_fields returned no usable fields for any tested object_id(s): {ids_for_fields}. "
360
+ "Verify object ids and connector field discovery."
392
361
  ),
393
- "last_invoke": last_data_res,
362
+ "last_invoke": last_fields_payload,
394
363
  }
395
364
 
396
- finally:
397
- stop_connector(runtime)
365
+ last_data_res: Any = None
366
+ for object_id, field_ids in objects_with_fields:
367
+ res = invoke(
368
+ "get_data",
369
+ session_id,
370
+ params={
371
+ "object_id": object_id,
372
+ "field_ids": field_ids,
373
+ "n_rows": 5,
374
+ },
375
+ )
376
+ last_data_res = res
377
+ if not isinstance(res.get("results"), dict):
378
+ logger.warning(
379
+ "run_test failed at stage=get_data (invalid results type) object_id=%s",
380
+ object_id,
381
+ )
382
+ return False, "get_data", res
383
+
384
+ payload = res["results"]
385
+ rows = payload.get("data")
386
+ if isinstance(rows, list) and len(rows) > 0:
387
+ logger.info("run_test completed successfully object_id=%s, rows retrieved=%s", object_id, len(rows))
388
+ return True, None, None
389
+
390
+ logger.warning(
391
+ "run_test failed: get_data returned no rows for any object with fields"
392
+ )
393
+ return False, "get_data", {
394
+ "message": (
395
+ f"get_data returned no rows for any object that had fields "
396
+ f"(tried {len(objects_with_fields)} object(s))."
397
+ ),
398
+ "last_invoke": last_data_res,
399
+ }
398
400
 
399
401
 
400
402
  def _validate_static_requires_object_ids(
@@ -413,7 +415,6 @@ def _validate_static_requires_object_ids(
413
415
  ),
414
416
  )
415
417
 
416
-
417
418
  def _build_full_fix_prompt(
418
419
  code: str,
419
420
  error_message: str,
@@ -738,16 +739,57 @@ def clone_repo():
738
739
  subprocess.run(["git", "-C", workspace, "checkout", branch], check=True)
739
740
  subprocess.run(["git", "-C", workspace, "pull"], check=True)
740
741
 
741
- def start_ai_http(connector_port=5000):
742
+ def run_code_server():
743
+ logger.info("code-server path: %s", shutil.which("code-server"))
744
+
745
+ # ✅ Ensure config dir exists
746
+ os.makedirs("/root/.config/code-server", exist_ok=True)
747
+
748
+ # ✅ Write code-server config
749
+ with open("/root/.config/code-server/config.yaml", "w") as f:
750
+ f.write(
751
+ f"bind-addr: 0.0.0.0:{CODE_SERVER_PORT}\n"
752
+ "auth: none\n"
753
+ )
754
+
755
+ with open("/root/.config/code-server/config.yaml", "r") as f:
756
+ logger.info("code-server config:\n%s", f.read())
757
+
758
+ # ✅ Ensure workspace + vscode dir exists
759
+ os.makedirs(workspace, exist_ok=True)
760
+ os.makedirs(f"{workspace}/.vscode", exist_ok=True)
761
+
762
+ # ✅ Write VS Code settings
763
+ with open(f"{workspace}/.vscode/settings.json", "w") as f:
764
+ f.write("""{
765
+ "chat.disableAIFeatures": true,
766
+ "workbench.colorTheme": "Default Dark+",
767
+ "outline.showFiles": false,
768
+ "timeline.enabled": false,
769
+ "python.defaultInterpreterPath": "/usr/local/bin/python"
770
+ }""")
771
+
772
+ # ✅ Start code-server (NO extension install here)
773
+ code_server_proc = subprocess.Popen([
774
+ "code-server",
775
+ workspace,
776
+ "--config", "/root/.config/code-server/config.yaml"
777
+ ])
778
+
779
+ time.sleep(3)
780
+
781
+ if code_server_proc.poll() is not None:
782
+ raise Exception("code-server failed to start")
783
+
784
+ return code_server_proc
785
+
786
+ def start_ai_http(start_code_server=True):
742
787
  global client
743
788
  global workspace
744
- global port_for_connector
745
789
  global process_ttl_seconds
746
790
  global process_ttl_deadline_unix
747
791
  global _process_ttl_timer
748
792
 
749
- port_for_connector = connector_port
750
-
751
793
  process_ttl_seconds = None
752
794
  process_ttl_deadline_unix = None
753
795
  _process_ttl_timer = None
@@ -786,51 +828,13 @@ def start_ai_http(connector_port=5000):
786
828
  )
787
829
 
788
830
  api_key = os.getenv("OPENAI_API_KEY")
789
- ai_port = int(os.getenv("AI_PORT", 5050))
790
- code_port = int(os.getenv("CODE_SERVER_PORT", 5002))
791
831
 
792
832
  clone_repo()
833
+ start_connector_http_server()
793
834
 
794
- logger.info("code-server path: %s", shutil.which("code-server"))
795
-
796
- # Ensure config dir exists
797
- os.makedirs("/root/.config/code-server", exist_ok=True)
798
-
799
- # ✅ Write code-server config
800
- with open("/root/.config/code-server/config.yaml", "w") as f:
801
- f.write(
802
- f"bind-addr: 0.0.0.0:{code_port}\n"
803
- "auth: none\n"
804
- )
805
-
806
- with open("/root/.config/code-server/config.yaml", "r") as f:
807
- logger.info("code-server config:\n%s", f.read())
808
-
809
- # ✅ Ensure workspace + vscode dir exists
810
- os.makedirs(workspace, exist_ok=True)
811
- os.makedirs(f"{workspace}/.vscode", exist_ok=True)
812
-
813
- # ✅ Write VS Code settings
814
- with open(f"{workspace}/.vscode/settings.json", "w") as f:
815
- f.write("""{
816
- "chat.disableAIFeatures": true,
817
- "workbench.colorTheme": "Default Dark+",
818
- "outline.showFiles": false,
819
- "timeline.enabled": false,
820
- "python.defaultInterpreterPath": "/usr/local/bin/python"
821
- }""")
822
-
823
- # ✅ Start code-server (NO extension install here)
824
- code_server_proc = subprocess.Popen([
825
- "code-server",
826
- workspace,
827
- "--config", "/root/.config/code-server/config.yaml"
828
- ])
829
-
830
- time.sleep(3)
831
-
832
- if code_server_proc.poll() is not None:
833
- raise Exception("code-server failed to start")
835
+ code_server_proc = None
836
+ if start_code_server:
837
+ code_server_proc = run_code_server()
834
838
 
835
839
  def shutdown():
836
840
  global _process_ttl_timer
@@ -839,8 +843,11 @@ def start_ai_http(connector_port=5000):
839
843
  if tm is not None:
840
844
  tm.cancel()
841
845
  logger.info("Shutting down code-server...")
842
- if code_server_proc:
846
+ if code_server_proc is not None:
843
847
  code_server_proc.terminate()
848
+ logger.info("Shutting down connector HTTP...")
849
+ with _connector_lock:
850
+ _stop_connector_subprocess_unlocked()
844
851
 
845
852
  ttl_shutdown_holder["fn"] = shutdown
846
853
 
@@ -858,18 +865,19 @@ def start_ai_http(connector_port=5000):
858
865
 
859
866
  client = OpenAI(api_key=api_key)
860
867
 
861
- logger.info("Starting AI HTTP controller on 0.0.0.0:%s", ai_port)
868
+ logger.info("Starting AI HTTP controller on 0.0.0.0:%s", AI_PORT)
862
869
 
863
870
  import uvicorn
864
- uvicorn.run(app, host="0.0.0.0", port=ai_port)
871
+ uvicorn.run(app, host="0.0.0.0", port=AI_PORT)
865
872
 
866
873
  @app.get("/session-info")
867
874
  def session_info():
868
875
  out: Dict[str, Any] = {
869
876
  "workspace": workspace,
870
- "ai_port": int(os.getenv("AI_PORT", 5050)),
871
- "code_server_port": int(os.getenv("CODE_SERVER_PORT", 5002)),
872
- "api_base": f"http://localhost:{os.getenv('AI_PORT', 5050)}",
877
+ "connector_port": CONNECTOR_PORT,
878
+ "ai_port": AI_PORT,
879
+ "code_server_port": CODE_SERVER_PORT,
880
+ "api_base": f"http://localhost:{AI_PORT}",
873
881
  }
874
882
  if process_ttl_seconds is not None and process_ttl_deadline_unix is not None:
875
883
  out["ttl_seconds"] = process_ttl_seconds
@@ -88,11 +88,21 @@ def invoke(req: InvokeRequest):
88
88
  }
89
89
 
90
90
 
91
- def start_server(port: Optional[int] = None):
91
+ def start_server():
92
92
  import uvicorn
93
- listen = int(os.getenv("PORT", 5000)) if port is None else int(port)
94
- uvicorn.run(
95
- "dc_sdk.src.server:app",
96
- host="0.0.0.0",
97
- port=listen,
98
- )
93
+
94
+ listen = int(os.getenv("CONNECTOR_PORT", 5000))
95
+ workspace = os.getenv("WORKSPACE", os.getcwd())
96
+ connector_src = os.path.join(workspace, "src")
97
+ use_reload = os.getenv("CONNECTOR_HTTP_RELOAD", "0").lower() in ("1", "true", "yes")
98
+
99
+ kw: Dict[str, Any] = {
100
+ "host": "0.0.0.0",
101
+ "port": listen,
102
+ }
103
+ if use_reload:
104
+ kw["reload"] = True
105
+ watch = connector_src if os.path.isdir(connector_src) else workspace
106
+ kw["reload_dirs"] = [os.path.abspath(watch)]
107
+
108
+ uvicorn.run("dc_sdk.src.server:app", **kw)
File without changes
File without changes