nebu 0.1.123__py3-none-any.whl → 0.1.125__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.
@@ -10,6 +10,7 @@ from typing import (
10
10
  List,
11
11
  Optional,
12
12
  TypeVar,
13
+ Union,
13
14
  cast,
14
15
  get_args,
15
16
  get_origin,
@@ -257,7 +258,7 @@ class Processor(Generic[InputType, OutputType]):
257
258
  logs: bool = False,
258
259
  api_key: Optional[str] = None,
259
260
  user_key: Optional[str] = None,
260
- timeout: Optional[float] = 600.0,
261
+ timeout: Optional[float] = 3600,
261
262
  ) -> OutputType | Dict[str, Any] | None:
262
263
  """
263
264
  Allows the Processor instance to be called like a function, sending data.
@@ -278,12 +279,21 @@ class Processor(Generic[InputType, OutputType]):
278
279
  logs: bool = False,
279
280
  api_key: Optional[str] = None,
280
281
  user_key: Optional[str] = None,
281
- timeout: Optional[float] = 600.0,
282
+ timeout: Optional[float] = 3600,
283
+ poll: bool = False,
284
+ poll_interval_seconds: float = 2.0,
282
285
  ) -> OutputType | Dict[str, Any] | None:
283
286
  """
284
- Send data to the processor and optionally stream logs in the background.
287
+ Send data to the processor.
288
+ If wait=True, the request to the /messages endpoint waits for the processing to complete.
289
+ If wait=False and poll=True, sends the message and then polls the /return/:message_id endpoint for the result.
290
+ If wait=False and poll=False, sends the message and returns the initial response (e.g., an acknowledgement).
291
+ Optionally streams logs in the background if logs=True.
285
292
  """
286
- print("sending data to processor: ", data)
293
+ logger.debug(
294
+ f"Sending data to processor {self.name}: {data}, wait={wait}, poll={poll}, logs={logs}"
295
+ )
296
+
287
297
  if (
288
298
  not self.processor
289
299
  or not self.processor.metadata.name
@@ -294,35 +304,168 @@ class Processor(Generic[InputType, OutputType]):
294
304
  processor_name = self.processor.metadata.name
295
305
  processor_namespace = self.processor.metadata.namespace
296
306
 
297
- if not api_key:
298
- api_key = self.api_key
307
+ # Determine the API key to use for this send operation
308
+ current_op_api_key = api_key if api_key is not None else self.api_key
309
+ if not current_op_api_key:
310
+ logger.error(
311
+ f"Processor {processor_name}: API key is missing for the send operation."
312
+ )
313
+ raise ValueError("API key not available for sending message.")
299
314
 
300
- # --- Send Data ---
315
+ # --- Send Initial Message ---
301
316
  messages_url = (
302
317
  f"{self.processors_url}/{processor_namespace}/{processor_name}/messages"
303
318
  )
319
+
320
+ # The 'wait' parameter for V1StreamData dictates if the /messages endpoint itself should block.
321
+ stream_data_wait_param = wait
322
+
304
323
  stream_data = V1StreamData(
305
324
  content=data,
306
- wait=wait,
325
+ wait=stream_data_wait_param,
307
326
  user_key=user_key,
308
327
  )
328
+
329
+ # Timeout for the initial POST request.
330
+ # If stream_data_wait_param is True, use the overall timeout.
331
+ # Otherwise (quick ack expected), use a shorter fixed timeout.
332
+ initial_request_timeout = timeout if stream_data_wait_param else 30.0
333
+
334
+ logger.debug(
335
+ f"Processor {processor_name}: Posting to {messages_url} with stream_data_wait={stream_data_wait_param}, initial_timeout={initial_request_timeout}"
336
+ )
309
337
  response = requests.post(
310
338
  messages_url,
311
339
  json=stream_data.model_dump(mode="json", exclude_none=True),
312
- headers={"Authorization": f"Bearer {api_key}"},
313
- timeout=timeout,
340
+ headers={"Authorization": f"Bearer {current_op_api_key}"},
341
+ timeout=initial_request_timeout,
314
342
  )
315
343
  response.raise_for_status()
316
344
  raw_response_json = response.json()
317
-
318
- print(f">>> Raw response JSON: {raw_response_json}")
345
+ logger.debug(
346
+ f"Processor {processor_name}: Initial response JSON: {raw_response_json}"
347
+ )
319
348
 
320
349
  if "error" in raw_response_json:
321
- print("error in raw_response_json")
350
+ logger.error(
351
+ f"Processor {processor_name}: Error in initial response: {raw_response_json['error']}"
352
+ )
322
353
  raise Exception(raw_response_json["error"])
323
354
 
324
- raw_content = raw_response_json.get("content")
325
- print(f">>> Raw content: {raw_content}")
355
+ # Initialize raw_content. This will hold the final data payload.
356
+ raw_content: Optional[Union[Dict[str, Any], List[Any], str]] = None
357
+
358
+ # --- Handle Response: Polling or Direct Content ---
359
+ # Poll only if poll=True AND the initial request was configured not to wait (wait=False).
360
+ if poll and not stream_data_wait_param:
361
+ message_id = raw_response_json.get("message_id")
362
+ if not message_id or not isinstance(message_id, str):
363
+ logger.error(
364
+ f"Processor {processor_name}: Polling requested but 'message_id' (string) not found in initial response. Response: {raw_response_json}"
365
+ )
366
+ raise ValueError(
367
+ "Polling failed: 'message_id' (string) missing or invalid in initial server response."
368
+ )
369
+
370
+ # Polling URL using self.orign_host for consistency
371
+ polling_url = f"{self.orign_host}/v1/processors/{processor_namespace}/{processor_name}/return/{message_id}"
372
+
373
+ logger.info(
374
+ f"Processor {processor_name}: Polling for message_id {message_id} at {polling_url}. Overall timeout: {timeout}s, Interval: {poll_interval_seconds}s."
375
+ )
376
+ polling_start_time = time.time()
377
+
378
+ while True:
379
+ current_time = time.time()
380
+ if (
381
+ timeout is not None
382
+ and (current_time - polling_start_time) > timeout
383
+ ):
384
+ logger.warning(
385
+ f"Processor {processor_name}: Polling for message_id {message_id} timed out after {timeout} seconds."
386
+ )
387
+ raise TimeoutError(
388
+ f"Polling for message_id {message_id} timed out after {timeout} seconds."
389
+ )
390
+
391
+ individual_poll_timeout = max(10.0, poll_interval_seconds * 2)
392
+ logger.debug(
393
+ f"Processor {processor_name}: Making polling attempt for {message_id}, attempt timeout: {individual_poll_timeout}s"
394
+ )
395
+ try:
396
+ poll_response = requests.post(
397
+ polling_url,
398
+ headers={"Authorization": f"Bearer {current_op_api_key}"},
399
+ timeout=individual_poll_timeout,
400
+ json={}, # Send an empty JSON body for POST
401
+ )
402
+
403
+ if poll_response.status_code == 200:
404
+ logger.info(
405
+ f"Processor {processor_name}: Successfully retrieved message {message_id} via polling. Status: 200."
406
+ )
407
+ try:
408
+ polled_data = poll_response.json()
409
+ if isinstance(polled_data, (dict, list, str)):
410
+ raw_content = polled_data
411
+ else:
412
+ logger.warning(
413
+ f"Processor {processor_name}: Polled data for {message_id} is of unexpected type: {type(polled_data)}. Content: {polled_data}"
414
+ )
415
+ raw_content = polled_data
416
+ except json.JSONDecodeError:
417
+ logger.error(
418
+ f"Processor {processor_name}: Failed to decode JSON from polling response for {message_id}. Response text: {poll_response.text[:200]}..."
419
+ )
420
+ raise ValueError(
421
+ f"Polling for {message_id} returned non-JSON response with status 200."
422
+ )
423
+ break # Exit polling loop
424
+
425
+ elif poll_response.status_code == 404:
426
+ logger.debug(
427
+ f"Processor {processor_name}: Message {message_id} not yet ready (404). Retrying in {poll_interval_seconds}s..."
428
+ )
429
+ elif poll_response.status_code == 202:
430
+ logger.debug(
431
+ f"Processor {processor_name}: Message {message_id} processing (202). Retrying in {poll_interval_seconds}s..."
432
+ )
433
+ else:
434
+ logger.error(
435
+ f"Processor {processor_name}: Polling for message_id {message_id} received unexpected status {poll_response.status_code}. Response: {poll_response.text[:500]}"
436
+ )
437
+ poll_response.raise_for_status()
438
+
439
+ except requests.exceptions.Timeout:
440
+ logger.warning(
441
+ f"Processor {processor_name}: Polling request for message_id {message_id} timed out. Retrying if overall timeout not exceeded..."
442
+ )
443
+ except requests.exceptions.HTTPError as e:
444
+ logger.error(
445
+ f"Processor {processor_name}: Polling for message_id {message_id} failed with HTTPError: {e}. Response: {e.response.text[:500] if e.response else 'No response text'}"
446
+ )
447
+ raise
448
+ except requests.exceptions.RequestException as e:
449
+ logger.error(
450
+ f"Processor {processor_name}: Polling for message_id {message_id} failed with RequestException: {e}"
451
+ )
452
+ raise
453
+
454
+ if timeout is not None and (time.time() - polling_start_time) > timeout:
455
+ logger.warning(
456
+ f"Processor {processor_name}: Polling for {message_id} timed out after {timeout}s (checked before sleep)."
457
+ )
458
+ raise TimeoutError(
459
+ f"Polling for message_id {message_id} timed out after {timeout} seconds."
460
+ )
461
+
462
+ time.sleep(poll_interval_seconds)
463
+ else:
464
+ # Handles: wait=True (polling skipped, server waited) OR (wait=False AND poll=False) (fire-and-forget)
465
+ raw_content = raw_response_json.get("content")
466
+ logger.debug(
467
+ f"Processor {processor_name}: Not polling. Raw content from initial response: {str(raw_content)[:200]}..."
468
+ )
326
469
 
327
470
  # --- Fetch Logs (if requested and not already running) ---
328
471
  if logs:
@@ -332,7 +475,11 @@ class Processor(Generic[InputType, OutputType]):
332
475
  )
333
476
  self._log_thread = threading.Thread(
334
477
  target=_fetch_and_print_logs,
335
- args=(log_url, self.api_key, processor_name), # Pass processor_name
478
+ args=(
479
+ log_url,
480
+ self.api_key,
481
+ processor_name,
482
+ ), # Use self.api_key for logs
336
483
  daemon=True,
337
484
  )
338
485
  try:
@@ -348,41 +495,60 @@ class Processor(Generic[InputType, OutputType]):
348
495
  else:
349
496
  logger.info(f"Log fetching is already running for {processor_name}.")
350
497
 
351
- # Attempt to parse into OutputType if conditions are met
352
- print(f">>> wait: {wait}")
353
- print(f">>> self.output_model_cls: {self.output_model_cls}")
354
- print(">>> type(self.output_model_cls): ", type(self.output_model_cls))
355
- print(
356
- f">>> isinstance(self.output_model_cls, type): {isinstance(self.output_model_cls, type)}"
498
+ # --- Attempt to parse into OutputType if conditions are met ---
499
+ logger.debug(
500
+ f"Processor {processor_name}: Attempting to parse result. output_model_cls: {self.output_model_cls}, raw_content type: {type(raw_content)}"
357
501
  )
358
- print(f">>> isinstance(raw_content, dict): {isinstance(raw_content, dict)}")
502
+
503
+ # Attempt to parse if the operation was intended to yield full content (either by waiting or polling),
504
+ # and raw_content is a dictionary, and output_model_cls is a Pydantic model.
505
+ should_attempt_parse = (
506
+ wait or poll
507
+ ) # True if client expects full content back from this method call
508
+
359
509
  if (
360
- wait
361
- and self.output_model_cls
510
+ should_attempt_parse
511
+ and self.output_model_cls is not None
362
512
  and isinstance(self.output_model_cls, type)
363
- and issubclass(self.output_model_cls, BaseModel) # type: ignore
364
513
  and isinstance(raw_content, dict)
365
514
  ):
366
- print(f">>> raw_content: {raw_content}")
515
+ logger.debug(
516
+ f"Processor {processor_name}: Valid conditions for parsing. Raw content (dict): {str(raw_content)[:200]}..."
517
+ )
367
518
  try:
368
519
  parsed_model = self.output_model_cls.model_validate(raw_content)
369
- print(f">>> parsed_model: {parsed_model}")
520
+ logger.debug(
521
+ f"Processor {processor_name}: Successfully parsed to {self.output_model_cls.__name__}. Parsed model: {str(parsed_model)[:200]}..."
522
+ )
370
523
  parsed_output: OutputType = cast(OutputType, parsed_model)
371
- print(f">>> parsed_output: {parsed_output}")
372
524
  return parsed_output
373
525
  except Exception as e:
374
- print(f">>> error: {e}")
375
526
  model_name = getattr(
376
527
  self.output_model_cls, "__name__", str(self.output_model_cls)
377
528
  )
378
529
  logger.error(
379
530
  f"Processor {processor_name}: Failed to parse 'content' field into output type {model_name}. "
380
- f"Error: {e}. Returning raw JSON response."
531
+ f"Error: {e}. Raw content was: {str(raw_content)[:500]}. Returning raw content instead."
532
+ )
533
+ return raw_content # type: ignore
534
+ else:
535
+ if (
536
+ not isinstance(raw_content, dict)
537
+ and should_attempt_parse
538
+ and self.output_model_cls
539
+ ):
540
+ logger.debug(
541
+ f"Processor {processor_name}: Skipping Pydantic parsing because raw_content is not a dict (type: {type(raw_content)})."
542
+ )
543
+ elif not (should_attempt_parse and self.output_model_cls):
544
+ logger.debug(
545
+ f"Processor {processor_name}: Skipping Pydantic parsing due to conditions not met (should_attempt_parse: {should_attempt_parse}, output_model_cls: {self.output_model_cls is not None})."
381
546
  )
382
- return raw_content
383
- # Fallback logic using self.schema_ has been removed.
384
547
 
385
- return raw_content
548
+ logger.debug(
549
+ f"Processor {processor_name}: Returning raw_content (type: {type(raw_content)}): {str(raw_content)[:200]}..."
550
+ )
551
+ return raw_content # type: ignore
386
552
 
387
553
  def scale(self, replicas: int) -> Dict[str, Any]:
388
554
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nebu
3
- Version: 0.1.123
3
+ Version: 0.1.125
4
4
  Summary: A globally distributed container runtime
5
5
  Requires-Python: >=3.10.14
6
6
  Description-Content-Type: text/markdown
@@ -19,11 +19,11 @@ nebu/processors/consumer_process_worker.py,sha256=h--eNFKaLbUayxn88mB8oGGdrU2liE
19
19
  nebu/processors/decorate.py,sha256=5p9pQrk_H8-Fj0UjsgSVCYx7Jk7KFuhMZtNhkKvpmkQ,61306
20
20
  nebu/processors/default.py,sha256=cy4ETMdbdRGkrvbYec1o60h7mGDlGN5JsuUph0ENtDU,364
21
21
  nebu/processors/models.py,sha256=8-TmKha2_QAnPlXcZxYjrCSPDCX7FFcMDMcHK77jK0U,4223
22
- nebu/processors/processor.py,sha256=wos5BkNZUVaY5ifT7yeDLbJkdfeloJ7hRuK9P18LU7U,26155
22
+ nebu/processors/processor.py,sha256=KS9zSjSj9KJT8ktcdiLTfXTfh3VvNCl7H0t2jnpH9GY,35250
23
23
  nebu/redis/models.py,sha256=coPovAcVXnOU1Xh_fpJL4PO3QctgK9nBe5QYoqEcnxg,1230
24
24
  nebu/services/service.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- nebu-0.1.123.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
26
- nebu-0.1.123.dist-info/METADATA,sha256=8L9L8vNoFhK8A4U-iWvXP9DIgeVXOW0MV324qcOhsV0,1798
27
- nebu-0.1.123.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
- nebu-0.1.123.dist-info/top_level.txt,sha256=uLIbEKJeGSHWOAJN5S0i5XBGwybALlF9bYoB1UhdEgQ,5
29
- nebu-0.1.123.dist-info/RECORD,,
25
+ nebu-0.1.125.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
26
+ nebu-0.1.125.dist-info/METADATA,sha256=tGSLm0cFTlfFS5lBPfqDzEX8tGWIgqlD1KfSGkISKqw,1798
27
+ nebu-0.1.125.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
+ nebu-0.1.125.dist-info/top_level.txt,sha256=uLIbEKJeGSHWOAJN5S0i5XBGwybALlF9bYoB1UhdEgQ,5
29
+ nebu-0.1.125.dist-info/RECORD,,
File without changes