omnata-plugin-runtime 0.12.2a331__py3-none-any.whl → 0.12.2a337__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.
- omnata_plugin_runtime/configuration.py +45 -1
- omnata_plugin_runtime/omnata_plugin.py +115 -0
- omnata_plugin_runtime/plugin_entrypoints.py +4 -0
- omnata_plugin_runtime/threading_utils.py +27 -0
- {omnata_plugin_runtime-0.12.2a331.dist-info → omnata_plugin_runtime-0.12.2a337.dist-info}/METADATA +1 -1
- omnata_plugin_runtime-0.12.2a337.dist-info/RECORD +14 -0
- omnata_plugin_runtime-0.12.2a331.dist-info/RECORD +0 -13
- {omnata_plugin_runtime-0.12.2a331.dist-info → omnata_plugin_runtime-0.12.2a337.dist-info}/WHEEL +0 -0
- {omnata_plugin_runtime-0.12.2a331.dist-info → omnata_plugin_runtime-0.12.2a337.dist-info}/licenses/LICENSE +0 -0
|
@@ -693,6 +693,9 @@ class ConnectionConfigurationParameters(SubscriptableBaseModel):
|
|
|
693
693
|
_snowflake: Optional[Any] = PrivateAttr( # or use Any to annotate the type and use Field to initialize
|
|
694
694
|
default=None
|
|
695
695
|
)
|
|
696
|
+
_sync_request: Optional[Any] = PrivateAttr( # Reference to SyncRequest for worker thread access
|
|
697
|
+
default=None
|
|
698
|
+
)
|
|
696
699
|
|
|
697
700
|
@model_validator(mode='after')
|
|
698
701
|
def validate_ngrok_tunnel_settings(self) -> Self:
|
|
@@ -739,6 +742,23 @@ class ConnectionConfigurationParameters(SubscriptableBaseModel):
|
|
|
739
742
|
"""
|
|
740
743
|
if parameter_name=='access_token' and self.access_token_secret_name is not None:
|
|
741
744
|
import _snowflake # pylint: disable=import-error, import-outside-toplevel # type: ignore
|
|
745
|
+
from .threading_utils import is_managed_worker_thread
|
|
746
|
+
|
|
747
|
+
# Check if we're in a worker thread using the explicit flag
|
|
748
|
+
# This is more reliable than checking thread names
|
|
749
|
+
if is_managed_worker_thread() and self._sync_request is not None:
|
|
750
|
+
logger.debug(f"Worker thread requesting access_token via secrets service")
|
|
751
|
+
try:
|
|
752
|
+
secrets = self._sync_request.request_secrets_from_main_thread(
|
|
753
|
+
self.access_token_secret_name, None
|
|
754
|
+
)
|
|
755
|
+
if 'access_token' in secrets:
|
|
756
|
+
return secrets['access_token']
|
|
757
|
+
except Exception as e:
|
|
758
|
+
logger.error(f"Error requesting access_token from main thread: {e}")
|
|
759
|
+
raise
|
|
760
|
+
|
|
761
|
+
# Otherwise, call _snowflake directly (main thread)
|
|
742
762
|
return StoredConfigurationValue(
|
|
743
763
|
value=_snowflake.get_oauth_access_token(self.access_token_secret_name)
|
|
744
764
|
)
|
|
@@ -1005,10 +1025,34 @@ StoredFieldMappings.model_rebuild()
|
|
|
1005
1025
|
OutboundSyncConfigurationParameters.model_rebuild()
|
|
1006
1026
|
|
|
1007
1027
|
@tracer.start_as_current_span("get_secrets")
|
|
1008
|
-
def get_secrets(oauth_secret_name: Optional[str], other_secrets_name: Optional[str]
|
|
1028
|
+
def get_secrets(oauth_secret_name: Optional[str], other_secrets_name: Optional[str],
|
|
1029
|
+
sync_request: Optional[Any] = None
|
|
1009
1030
|
) -> Dict[str, StoredConfigurationValue]:
|
|
1031
|
+
"""
|
|
1032
|
+
Get secrets from Snowflake. This function can be called from the main thread or worker threads.
|
|
1033
|
+
When called from worker threads (e.g., within @managed_inbound_processing), it will automatically
|
|
1034
|
+
route the request through the secrets service to avoid threading issues with _snowflake.get_oauth_access_token.
|
|
1035
|
+
|
|
1036
|
+
:param oauth_secret_name: The name of the OAuth secret to retrieve
|
|
1037
|
+
:param other_secrets_name: The name of other secrets to retrieve
|
|
1038
|
+
:param sync_request: Optional SyncRequest instance for worker threads. If not provided, will attempt to detect.
|
|
1039
|
+
:return: Dictionary of StoredConfigurationValue objects
|
|
1040
|
+
"""
|
|
1041
|
+
from .threading_utils import is_managed_worker_thread
|
|
1010
1042
|
connection_secrets = {}
|
|
1011
1043
|
import _snowflake # pylint: disable=import-error, import-outside-toplevel # type: ignore
|
|
1044
|
+
|
|
1045
|
+
# Check if we're in a worker thread using the explicit flag
|
|
1046
|
+
# This is more reliable than checking thread names
|
|
1047
|
+
if is_managed_worker_thread() and sync_request is not None:
|
|
1048
|
+
logger.debug(f"Worker thread requesting secrets via secrets service")
|
|
1049
|
+
try:
|
|
1050
|
+
return sync_request.request_secrets_from_main_thread(oauth_secret_name, other_secrets_name)
|
|
1051
|
+
except Exception as e:
|
|
1052
|
+
logger.error(f"Error requesting secrets from main thread: {e}")
|
|
1053
|
+
raise
|
|
1054
|
+
|
|
1055
|
+
# Otherwise, call _snowflake functions directly (main thread)
|
|
1012
1056
|
if oauth_secret_name is not None:
|
|
1013
1057
|
connection_secrets["access_token"] = StoredConfigurationValue(
|
|
1014
1058
|
value=_snowflake.get_oauth_access_token(oauth_secret_name)
|
|
@@ -101,6 +101,7 @@ from .rate_limiting import (
|
|
|
101
101
|
from .json_schema import (
|
|
102
102
|
FullyQualifiedTable
|
|
103
103
|
)
|
|
104
|
+
from .threading_utils import is_managed_worker_thread, set_managed_worker_thread
|
|
104
105
|
|
|
105
106
|
SortDirectionType = Literal["asc", "desc"]
|
|
106
107
|
|
|
@@ -375,6 +376,11 @@ class SyncRequest(ABC):
|
|
|
375
376
|
self._last_states_update = None
|
|
376
377
|
# store the opentelemetry context so that it can be attached inside threads
|
|
377
378
|
self.opentelemetry_context = context.get_current()
|
|
379
|
+
|
|
380
|
+
# Secrets service for thread-safe access to _snowflake.get_oauth_access_token
|
|
381
|
+
# which can only be called from the main thread
|
|
382
|
+
# The main thread (in decorator wait loops) will service these requests
|
|
383
|
+
self._secrets_request_queue: queue.Queue = queue.Queue()
|
|
378
384
|
|
|
379
385
|
threading.excepthook = self.thread_exception_hook
|
|
380
386
|
if self.development_mode is False:
|
|
@@ -499,6 +505,105 @@ class SyncRequest(ABC):
|
|
|
499
505
|
cancellation_token.wait(20)
|
|
500
506
|
logger.info("cancel checking worker exiting")
|
|
501
507
|
|
|
508
|
+
def _service_secrets_requests(self):
|
|
509
|
+
"""
|
|
510
|
+
Services any pending secrets requests from worker threads.
|
|
511
|
+
This should be called periodically from the main thread while waiting for workers.
|
|
512
|
+
Returns True if any requests were serviced, False otherwise.
|
|
513
|
+
"""
|
|
514
|
+
import _snowflake # pylint: disable=import-error, import-outside-toplevel # type: ignore
|
|
515
|
+
from .configuration import StoredConfigurationValue
|
|
516
|
+
|
|
517
|
+
serviced_any = False
|
|
518
|
+
# Process all pending requests (non-blocking)
|
|
519
|
+
while not self._secrets_request_queue.empty():
|
|
520
|
+
try:
|
|
521
|
+
request = self._secrets_request_queue.get_nowait()
|
|
522
|
+
except queue.Empty:
|
|
523
|
+
break
|
|
524
|
+
|
|
525
|
+
serviced_any = True
|
|
526
|
+
oauth_secret_name = request.get('oauth_secret_name')
|
|
527
|
+
other_secrets_name = request.get('other_secrets_name')
|
|
528
|
+
response_queue = request['response_queue']
|
|
529
|
+
|
|
530
|
+
logger.debug(f"Main thread servicing secrets request")
|
|
531
|
+
|
|
532
|
+
try:
|
|
533
|
+
# Call _snowflake functions directly (we're on the main thread now)
|
|
534
|
+
connection_secrets = {}
|
|
535
|
+
if oauth_secret_name is not None:
|
|
536
|
+
connection_secrets["access_token"] = StoredConfigurationValue(
|
|
537
|
+
value=_snowflake.get_oauth_access_token(oauth_secret_name)
|
|
538
|
+
)
|
|
539
|
+
if other_secrets_name is not None:
|
|
540
|
+
try:
|
|
541
|
+
secret_string_content = _snowflake.get_generic_secret_string(
|
|
542
|
+
other_secrets_name
|
|
543
|
+
)
|
|
544
|
+
if len(secret_string_content) > 2:
|
|
545
|
+
other_secrets = json.loads(secret_string_content)
|
|
546
|
+
connection_secrets = {
|
|
547
|
+
**connection_secrets,
|
|
548
|
+
**TypeAdapter(Dict[str, StoredConfigurationValue]).validate_python(other_secrets),
|
|
549
|
+
}
|
|
550
|
+
except Exception as exception:
|
|
551
|
+
logger.error(f"Error parsing secrets content for secret {other_secrets_name}: {str(exception)}")
|
|
552
|
+
raise ValueError(f"Error parsing secrets content: {str(exception)}") from exception
|
|
553
|
+
|
|
554
|
+
# Put the result in the response queue for the requesting thread
|
|
555
|
+
response_queue.put({
|
|
556
|
+
'success': True,
|
|
557
|
+
'result': connection_secrets
|
|
558
|
+
})
|
|
559
|
+
except Exception as e:
|
|
560
|
+
logger.error(f"Error servicing secrets request: {e}")
|
|
561
|
+
response_queue.put({
|
|
562
|
+
'success': False,
|
|
563
|
+
'error': str(e)
|
|
564
|
+
})
|
|
565
|
+
finally:
|
|
566
|
+
self._secrets_request_queue.task_done()
|
|
567
|
+
|
|
568
|
+
return serviced_any
|
|
569
|
+
|
|
570
|
+
def request_secrets_from_main_thread(self, oauth_secret_name: Optional[str],
|
|
571
|
+
other_secrets_name: Optional[str],
|
|
572
|
+
timeout: int = 30) -> Dict[str, Any]:
|
|
573
|
+
"""
|
|
574
|
+
Request secrets from the main thread. This should be called from worker threads
|
|
575
|
+
when they need to access secrets via _snowflake.get_oauth_access_token.
|
|
576
|
+
The main thread services these requests while waiting for workers to complete.
|
|
577
|
+
|
|
578
|
+
:param oauth_secret_name: The name of the OAuth secret to retrieve
|
|
579
|
+
:param other_secrets_name: The name of other secrets to retrieve
|
|
580
|
+
:param timeout: Maximum time to wait for the response in seconds
|
|
581
|
+
:return: Dictionary of StoredConfigurationValue objects
|
|
582
|
+
:raises TimeoutError: if the request times out
|
|
583
|
+
:raises ValueError: if the secrets service returns an error
|
|
584
|
+
"""
|
|
585
|
+
# Create a response queue for this specific request
|
|
586
|
+
response_queue: queue.Queue = queue.Queue()
|
|
587
|
+
|
|
588
|
+
logger.debug(f"Requesting secrets from main thread")
|
|
589
|
+
|
|
590
|
+
# Put the request in the queue with its own response queue
|
|
591
|
+
self._secrets_request_queue.put({
|
|
592
|
+
'oauth_secret_name': oauth_secret_name,
|
|
593
|
+
'other_secrets_name': other_secrets_name,
|
|
594
|
+
'response_queue': response_queue
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
# Block on the response queue with timeout
|
|
598
|
+
try:
|
|
599
|
+
response = response_queue.get(timeout=timeout)
|
|
600
|
+
if response['success']:
|
|
601
|
+
return response['result']
|
|
602
|
+
else:
|
|
603
|
+
raise ValueError(f"Error getting secrets: {response['error']}")
|
|
604
|
+
except queue.Empty:
|
|
605
|
+
raise TimeoutError(f"Timeout waiting for secrets request after {timeout} seconds")
|
|
606
|
+
|
|
502
607
|
@abstractmethod
|
|
503
608
|
def apply_results_queue(self):
|
|
504
609
|
"""
|
|
@@ -2184,6 +2289,9 @@ def __managed_outbound_processing_worker(
|
|
|
2184
2289
|
Consumes a fixed sized set of records by passing them to the wrapped function,
|
|
2185
2290
|
while adhering to the defined API constraints.
|
|
2186
2291
|
"""
|
|
2292
|
+
# Mark this thread as a managed worker thread
|
|
2293
|
+
set_managed_worker_thread(True)
|
|
2294
|
+
|
|
2187
2295
|
context.attach(plugin_class_obj.opentelemetry_context)
|
|
2188
2296
|
logger.debug(
|
|
2189
2297
|
f"worker {worker_index} processing. Cancelled: {cancellation_token.is_set()}"
|
|
@@ -2323,6 +2431,8 @@ def managed_outbound_processing(concurrency: int, batch_size: int):
|
|
|
2323
2431
|
task.join() # Ensure the thread is fully finished
|
|
2324
2432
|
tasks.remove(task)
|
|
2325
2433
|
logger.info(f"Thread {task.name} has completed processing")
|
|
2434
|
+
# Service any secrets requests from worker threads while we wait
|
|
2435
|
+
self._sync_request._service_secrets_requests()
|
|
2326
2436
|
time.sleep(1) # Avoid busy waiting
|
|
2327
2437
|
logger.info("All workers completed processing")
|
|
2328
2438
|
|
|
@@ -2367,6 +2477,9 @@ def __managed_inbound_processing_worker(
|
|
|
2367
2477
|
A worker thread for the managed_inbound_processing annotation.
|
|
2368
2478
|
Passes single streams at a time to the wrapped function, adhering to concurrency constraints.
|
|
2369
2479
|
"""
|
|
2480
|
+
# Mark this thread as a managed worker thread
|
|
2481
|
+
set_managed_worker_thread(True)
|
|
2482
|
+
|
|
2370
2483
|
context.attach(plugin_class_obj.opentelemetry_context)
|
|
2371
2484
|
while not cancellation_token.is_set():
|
|
2372
2485
|
# Get our generator object out of the queue
|
|
@@ -2505,6 +2618,8 @@ def managed_inbound_processing(concurrency: int):
|
|
|
2505
2618
|
task.join() # Ensure the thread is fully finished
|
|
2506
2619
|
tasks.remove(task)
|
|
2507
2620
|
logger.info(f"Thread {task.name} has completed processing")
|
|
2621
|
+
# Service any secrets requests from worker threads while we wait
|
|
2622
|
+
self._sync_request._service_secrets_requests()
|
|
2508
2623
|
time.sleep(1) # Avoid busy waiting
|
|
2509
2624
|
logger.info("All workers completed processing")
|
|
2510
2625
|
|
|
@@ -188,6 +188,8 @@ class PluginEntrypoint:
|
|
|
188
188
|
sync_id=request.sync_id,
|
|
189
189
|
branch_name=request.sync_branch_name
|
|
190
190
|
)
|
|
191
|
+
# Store sync_request reference in parameters for worker thread access
|
|
192
|
+
parameters._sync_request = outbound_sync_request # pylint: disable=protected-access
|
|
191
193
|
try:
|
|
192
194
|
self._plugin_instance._configuration_parameters = parameters
|
|
193
195
|
with tracer.start_as_current_span("invoke_plugin") as span:
|
|
@@ -246,6 +248,8 @@ class PluginEntrypoint:
|
|
|
246
248
|
sync_id=request.sync_id,
|
|
247
249
|
branch_name=request.sync_branch_name
|
|
248
250
|
)
|
|
251
|
+
# Store sync_request reference in parameters for worker thread access
|
|
252
|
+
parameters._sync_request = inbound_sync_request # pylint: disable=protected-access
|
|
249
253
|
try:
|
|
250
254
|
self._plugin_instance._configuration_parameters = parameters
|
|
251
255
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for thread management in the plugin runtime.
|
|
3
|
+
"""
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
# Thread-local storage to track if we're in a managed worker thread
|
|
7
|
+
# This is more reliable than checking thread names
|
|
8
|
+
_thread_local = threading.local()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_managed_worker_thread() -> bool:
|
|
12
|
+
"""
|
|
13
|
+
Check if the current thread is a managed worker thread.
|
|
14
|
+
Returns True if running in a @managed_inbound_processing or @managed_outbound_processing worker.
|
|
15
|
+
|
|
16
|
+
This is set by the decorator worker functions and is more reliable than checking thread names.
|
|
17
|
+
"""
|
|
18
|
+
return getattr(_thread_local, 'is_managed_worker', False)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def set_managed_worker_thread(is_worker: bool):
|
|
22
|
+
"""
|
|
23
|
+
Set the flag indicating whether the current thread is a managed worker thread.
|
|
24
|
+
|
|
25
|
+
This should only be called by the managed processing decorator worker functions.
|
|
26
|
+
"""
|
|
27
|
+
_thread_local.is_managed_worker = is_worker
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
omnata_plugin_runtime/__init__.py,sha256=MS9d1whnfT_B3-ThqZ7l63QeC_8OEKTuaYV5wTwRpBA,1576
|
|
2
|
+
omnata_plugin_runtime/api.py,sha256=5gbjbnFy72Xjf0E3kbG23G0V2J3CorvD5kpBn_BkdlI,8084
|
|
3
|
+
omnata_plugin_runtime/configuration.py,sha256=-y9TmhDz6iI6u1AZ0sPG7HIJLOpiBpuscUXFQm7SkRs,49351
|
|
4
|
+
omnata_plugin_runtime/forms.py,sha256=Lrbr3otsFDrvHWJw7v-slsW4PvEHJ6BG1Yl8oaJfiDo,20529
|
|
5
|
+
omnata_plugin_runtime/json_schema.py,sha256=ZfHMG-XSJBE9Smt33Y6GPpl5skF7pB1TRCf9AvWuw-Y,59705
|
|
6
|
+
omnata_plugin_runtime/logging.py,sha256=qUtRA9syQNnjfJZHA2W18K282voXX6vHwrBIPOBo1n8,4521
|
|
7
|
+
omnata_plugin_runtime/omnata_plugin.py,sha256=FLVU88CjTwI52OOxAsouu1DZh-stY_b-r1uc7tgMTn8,149390
|
|
8
|
+
omnata_plugin_runtime/plugin_entrypoints.py,sha256=0glIkuQpva-C-a3Cn3KNI6Jj2u7fpdWV1aiUgrEz3Mo,32986
|
|
9
|
+
omnata_plugin_runtime/rate_limiting.py,sha256=qpr5esU4Ks8hMzuMpSR3gLFdor2ZUXYWCjmsQH_K6lQ,25882
|
|
10
|
+
omnata_plugin_runtime/threading_utils.py,sha256=fqlKLCPTEPVYdMinf8inPKLYxwD4d4WWVMLB3a2mNqk,906
|
|
11
|
+
omnata_plugin_runtime-0.12.2a337.dist-info/METADATA,sha256=eQIYDh_p1MuA0hE6rQeZaTdWV9VL5TLWk1A1j6n5jvs,2226
|
|
12
|
+
omnata_plugin_runtime-0.12.2a337.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
13
|
+
omnata_plugin_runtime-0.12.2a337.dist-info/licenses/LICENSE,sha256=rGaMQG3R3F5-JGDp_-rlMKpDIkg5n0SI4kctTk8eZSI,56
|
|
14
|
+
omnata_plugin_runtime-0.12.2a337.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
omnata_plugin_runtime/__init__.py,sha256=MS9d1whnfT_B3-ThqZ7l63QeC_8OEKTuaYV5wTwRpBA,1576
|
|
2
|
-
omnata_plugin_runtime/api.py,sha256=5gbjbnFy72Xjf0E3kbG23G0V2J3CorvD5kpBn_BkdlI,8084
|
|
3
|
-
omnata_plugin_runtime/configuration.py,sha256=SffokJfgvy6V3kUsoEjXcK3GdNgHo6U3mgBEs0qBv4I,46972
|
|
4
|
-
omnata_plugin_runtime/forms.py,sha256=Lrbr3otsFDrvHWJw7v-slsW4PvEHJ6BG1Yl8oaJfiDo,20529
|
|
5
|
-
omnata_plugin_runtime/json_schema.py,sha256=ZfHMG-XSJBE9Smt33Y6GPpl5skF7pB1TRCf9AvWuw-Y,59705
|
|
6
|
-
omnata_plugin_runtime/logging.py,sha256=qUtRA9syQNnjfJZHA2W18K282voXX6vHwrBIPOBo1n8,4521
|
|
7
|
-
omnata_plugin_runtime/omnata_plugin.py,sha256=YB-IASiAyDJZW8VrY376r04jacdAgLkbmPGnmcLF-4w,143775
|
|
8
|
-
omnata_plugin_runtime/plugin_entrypoints.py,sha256=_1pDLov3iQorGmfcae8Sw2bVjxw1vYeowBaKKNzRclQ,32629
|
|
9
|
-
omnata_plugin_runtime/rate_limiting.py,sha256=qpr5esU4Ks8hMzuMpSR3gLFdor2ZUXYWCjmsQH_K6lQ,25882
|
|
10
|
-
omnata_plugin_runtime-0.12.2a331.dist-info/METADATA,sha256=Txeu9SmDqhjEq-t3I8QziHGhJ1GrGMfcjdaD1U7EAkw,2226
|
|
11
|
-
omnata_plugin_runtime-0.12.2a331.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
12
|
-
omnata_plugin_runtime-0.12.2a331.dist-info/licenses/LICENSE,sha256=rGaMQG3R3F5-JGDp_-rlMKpDIkg5n0SI4kctTk8eZSI,56
|
|
13
|
-
omnata_plugin_runtime-0.12.2a331.dist-info/RECORD,,
|
{omnata_plugin_runtime-0.12.2a331.dist-info → omnata_plugin_runtime-0.12.2a337.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|