omnata-plugin-runtime 0.12.1a330__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.
@@ -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
 
@@ -2762,7 +2877,7 @@ def omnata_udf(
2762
2877
 
2763
2878
  return decorator
2764
2879
 
2765
- def find_udf_functions(path:str = '.',top_level_modules:Optional[List[str]] = None) -> List[UDFDefinition]:
2880
+ def find_udf_functions(path:str = '.',top_level_modules:Optional[List[str]] = None, exclude_top_level_modules:Optional[List[str]] = None) -> List[UDFDefinition]:
2766
2881
  """
2767
2882
  Finds all functions in the specified directory which have the 'omnata_udf' decorator applied
2768
2883
  """
@@ -2778,6 +2893,9 @@ def find_udf_functions(path:str = '.',top_level_modules:Optional[List[str]] = No
2778
2893
  if top_level_modules is not None:
2779
2894
  if len([x for x in top_level_modules if module_name.startswith(x)]) == 0:
2780
2895
  continue
2896
+ if exclude_top_level_modules is not None:
2897
+ if any(module_name.startswith(y) for y in exclude_top_level_modules):
2898
+ continue
2781
2899
  module = importlib.import_module(module_name)
2782
2900
 
2783
2901
  # Iterate over all members of the module
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omnata-plugin-runtime
3
- Version: 0.12.1a330
3
+ Version: 0.12.2a337
4
4
  Summary: Classes and common runtime components for building and running Omnata Plugins
5
5
  License-File: LICENSE
6
6
  Author: James Weakley
@@ -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=8FT3XNdZzty76OldvcxdKpbKrPENKjAIbwa_rxceVyg,143564
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.1a330.dist-info/METADATA,sha256=S5v-G348UEulYX6Uai-HY9xuEBqGq6Ija1SPwgiKoTU,2226
11
- omnata_plugin_runtime-0.12.1a330.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
12
- omnata_plugin_runtime-0.12.1a330.dist-info/licenses/LICENSE,sha256=rGaMQG3R3F5-JGDp_-rlMKpDIkg5n0SI4kctTk8eZSI,56
13
- omnata_plugin_runtime-0.12.1a330.dist-info/RECORD,,