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.
- {nebu-0.1.124/src/nebu.egg-info → nebu-0.1.126}/PKG-INFO +1 -1
- {nebu-0.1.124 → nebu-0.1.126}/pyproject.toml +1 -1
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/processors/processor.py +202 -32
- {nebu-0.1.124 → nebu-0.1.126/src/nebu.egg-info}/PKG-INFO +1 -1
- {nebu-0.1.124 → nebu-0.1.126}/LICENSE +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/README.md +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/setup.cfg +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/__init__.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/auth.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/builders/builder.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/builders/models.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/cache.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/config.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/containers/container.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/containers/models.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/data.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/errors.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/logging.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/meta.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/namespaces/models.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/namespaces/namespace.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/orign.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/processors/consumer.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/processors/consumer_health_worker.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/processors/consumer_process_worker.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/processors/decorate.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/processors/default.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/processors/models.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/redis/models.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu/services/service.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu.egg-info/SOURCES.txt +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu.egg-info/dependency_links.txt +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu.egg-info/requires.txt +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/src/nebu.egg-info/top_level.txt +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/tests/test_bucket.py +0 -0
- {nebu-0.1.124 → nebu-0.1.126}/tests/test_containers.py +0 -0
@@ -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
|
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
|
-
|
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
|
-
|
298
|
-
|
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
|
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=
|
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 {
|
313
|
-
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
|
-
|
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
|
-
|
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
|
325
|
-
|
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=(
|
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
|
-
|
353
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
"""
|
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
|
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
|
File without changes
|
File without changes
|