nebu 0.1.123__tar.gz → 0.1.125__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.123/src/nebu.egg-info → nebu-0.1.125}/PKG-INFO +1 -1
- {nebu-0.1.123 → nebu-0.1.125}/pyproject.toml +1 -1
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/processors/processor.py +200 -34
- {nebu-0.1.123 → nebu-0.1.125/src/nebu.egg-info}/PKG-INFO +1 -1
- {nebu-0.1.123 → nebu-0.1.125}/LICENSE +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/README.md +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/setup.cfg +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/__init__.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/auth.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/builders/builder.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/builders/models.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/cache.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/config.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/containers/container.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/containers/models.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/data.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/errors.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/logging.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/meta.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/namespaces/models.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/namespaces/namespace.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/orign.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/processors/consumer.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/processors/consumer_health_worker.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/processors/consumer_process_worker.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/processors/decorate.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/processors/default.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/processors/models.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/redis/models.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu/services/service.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu.egg-info/SOURCES.txt +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu.egg-info/dependency_links.txt +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu.egg-info/requires.txt +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/src/nebu.egg-info/top_level.txt +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/tests/test_bucket.py +0 -0
- {nebu-0.1.123 → nebu-0.1.125}/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,
|
@@ -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] =
|
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] =
|
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
|
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
|
-
|
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
|
-
|
298
|
-
|
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
|
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=
|
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 {
|
313
|
-
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
|
-
|
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
|
-
|
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
|
325
|
-
|
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=(
|
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
|
-
|
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)}"
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
"""
|
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
|