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