omnata-plugin-runtime 0.7.0a184__py3-none-any.whl → 0.8.0__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 +2 -4
- omnata_plugin_runtime/logging.py +29 -1
- omnata_plugin_runtime/omnata_plugin.py +67 -54
- omnata_plugin_runtime/plugin_entrypoints.py +178 -130
- omnata_plugin_runtime/rate_limiting.py +13 -11
- {omnata_plugin_runtime-0.7.0a184.dist-info → omnata_plugin_runtime-0.8.0.dist-info}/METADATA +5 -1
- omnata_plugin_runtime-0.8.0.dist-info/RECORD +12 -0
- omnata_plugin_runtime-0.7.0a184.dist-info/RECORD +0 -12
- {omnata_plugin_runtime-0.7.0a184.dist-info → omnata_plugin_runtime-0.8.0.dist-info}/LICENSE +0 -0
- {omnata_plugin_runtime-0.7.0a184.dist-info → omnata_plugin_runtime-0.8.0.dist-info}/WHEEL +0 -0
| @@ -11,6 +11,7 @@ from enum import Enum | |
| 11 11 |  | 
| 12 12 | 
             
            from abc import ABC
         | 
| 13 13 | 
             
            from pydantic import BaseModel, Field, PrivateAttr, SerializationInfo, TypeAdapter, field_validator, model_serializer, validator  # pylint: disable=no-name-in-module
         | 
| 14 | 
            +
            from .logging import logger, tracer
         | 
| 14 15 |  | 
| 15 16 | 
             
            if tuple(sys.version_info[:2]) >= (3, 9):
         | 
| 16 17 | 
             
                # Python 3.9 and above
         | 
| @@ -19,9 +20,6 @@ else: | |
| 19 20 | 
             
                # Python 3.8 and below
         | 
| 20 21 | 
             
                from typing_extensions import Annotated
         | 
| 21 22 |  | 
| 22 | 
            -
            logger = logging.getLogger(__name__)
         | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 23 | 
             
            class MapperType(str, Enum):
         | 
| 26 24 | 
             
                FIELD_MAPPING_SELECTOR = "field_mapping_selector"
         | 
| 27 25 | 
             
                JINJA_TEMPLATE = "jinja_template"
         | 
| @@ -872,7 +870,7 @@ InboundSyncStreamsConfiguration.model_rebuild() | |
| 872 870 | 
             
            StoredFieldMappings.model_rebuild()
         | 
| 873 871 | 
             
            OutboundSyncConfigurationParameters.model_rebuild()
         | 
| 874 872 |  | 
| 875 | 
            -
             | 
| 873 | 
            +
            @tracer.start_as_current_span("get_secrets")
         | 
| 876 874 | 
             
            def get_secrets(oauth_secret_name: Optional[str], other_secrets_name: Optional[str]
         | 
| 877 875 | 
             
            ) -> Dict[str, StoredConfigurationValue]:
         | 
| 878 876 | 
             
                connection_secrets = {}
         | 
    
        omnata_plugin_runtime/logging.py
    CHANGED
    
    | @@ -4,11 +4,39 @@ Custom logging functionality for Omnata | |
| 4 4 | 
             
            import logging
         | 
| 5 5 | 
             
            import logging.handlers
         | 
| 6 6 | 
             
            import traceback
         | 
| 7 | 
            -
            from logging import Logger
         | 
| 7 | 
            +
            from logging import Logger, getLogger
         | 
| 8 8 | 
             
            from typing import Dict, List, Optional
         | 
| 9 9 | 
             
            from snowflake.snowpark import Session
         | 
| 10 10 | 
             
            from pydantic import ValidationError
         | 
| 11 | 
            +
            from snowflake import telemetry
         | 
| 12 | 
            +
            from opentelemetry import trace
         | 
| 11 13 |  | 
| 14 | 
            +
            tracer = trace.get_tracer('omnata_plugin_runtime')
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            class CustomLoggerAdapter(logging.LoggerAdapter):
         | 
| 17 | 
            +
                """
         | 
| 18 | 
            +
                A logger adapter which attaches current trace and span IDs to log messages, so that they can be correlated to traces.
         | 
| 19 | 
            +
                Also offers the ability to add static extras.
         | 
| 20 | 
            +
                """
         | 
| 21 | 
            +
                def __init__(self, logger:logging.Logger, extra):
         | 
| 22 | 
            +
                    super(CustomLoggerAdapter, self).__init__(logger, extra)
         | 
| 23 | 
            +
                    self.extra_extras = {}
         | 
| 24 | 
            +
                
         | 
| 25 | 
            +
                def add_extra(self, key, value):
         | 
| 26 | 
            +
                    self.extra_extras[key] = value
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def process(self, msg, kwargs):
         | 
| 29 | 
            +
                    extra = kwargs.get("extra", {})
         | 
| 30 | 
            +
                    current_span = trace.get_current_span()
         | 
| 31 | 
            +
                    context = current_span.get_span_context() if current_span is not None else None
         | 
| 32 | 
            +
                    if context is not None:
         | 
| 33 | 
            +
                        extra["trace_id"] = format(context.trace_id, 'x') # format as hex string to be consistent with Snowflake's handler
         | 
| 34 | 
            +
                        extra["span_id"] = format(context.span_id, 'x')
         | 
| 35 | 
            +
                    extra.update(self.extra_extras)
         | 
| 36 | 
            +
                    kwargs["extra"] = extra
         | 
| 37 | 
            +
                    return msg, kwargs
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            logger = CustomLoggerAdapter(getLogger("omnata_plugin"), {})
         | 
| 12 40 |  | 
| 13 41 | 
             
            def log_exception(exception: Exception, logger_instance: Logger):
         | 
| 14 42 | 
             
                """
         | 
| @@ -47,7 +47,8 @@ from snowflake.snowpark import Session | |
| 47 47 | 
             
            from snowflake.snowpark.functions import col
         | 
| 48 48 | 
             
            from tenacity import Retrying, stop_after_attempt, wait_fixed, retry_if_exception_message
         | 
| 49 49 |  | 
| 50 | 
            -
            from .logging import OmnataPluginLogHandler
         | 
| 50 | 
            +
            from .logging import OmnataPluginLogHandler, logger, tracer
         | 
| 51 | 
            +
            from opentelemetry import context
         | 
| 51 52 |  | 
| 52 53 | 
             
            from .api import (
         | 
| 53 54 | 
             
                PluginMessage,
         | 
| @@ -88,7 +89,6 @@ from .rate_limiting import ( | |
| 88 89 | 
             
                RateLimitedSession
         | 
| 89 90 | 
             
            )
         | 
| 90 91 |  | 
| 91 | 
            -
            logger = getLogger(__name__)
         | 
| 92 92 | 
             
            SortDirectionType = Literal["asc", "desc"]
         | 
| 93 93 |  | 
| 94 94 |  | 
| @@ -317,6 +317,8 @@ class SyncRequest(ABC): | |
| 317 317 | 
             
                    self._cancel_checking_task = None
         | 
| 318 318 | 
             
                    self._rate_limit_update_task = None
         | 
| 319 319 | 
             
                    self._last_stream_progress_update = None
         | 
| 320 | 
            +
                    # store the opentelemetry context so that it can be attached inside threads
         | 
| 321 | 
            +
                    self.opentelemetry_context = context.get_current()
         | 
| 320 322 |  | 
| 321 323 | 
             
                    threading.excepthook = self.thread_exception_hook
         | 
| 322 324 | 
             
                    if self.development_mode is False:
         | 
| @@ -388,6 +390,7 @@ class SyncRequest(ABC): | |
| 388 390 | 
             
                    """
         | 
| 389 391 | 
             
                    Designed to be run in a thread, this method polls the results every 20 seconds and sends them back to Snowflake.
         | 
| 390 392 | 
             
                    """
         | 
| 393 | 
            +
                    context.attach(self.opentelemetry_context)
         | 
| 391 394 | 
             
                    while not cancellation_token.is_set():
         | 
| 392 395 | 
             
                        logger.debug("apply results worker checking for results")
         | 
| 393 396 | 
             
                        self.apply_results_queue()
         | 
| @@ -400,6 +403,7 @@ class SyncRequest(ABC): | |
| 400 403 | 
             
                    It also gives us the latest rate limit state from Snowflake, so that activity on other syncs/branches can
         | 
| 401 404 | 
             
                    impact rate limiting on this one.
         | 
| 402 405 | 
             
                    """
         | 
| 406 | 
            +
                    context.attach(self.opentelemetry_context)
         | 
| 403 407 | 
             
                    while not cancellation_token.is_set():
         | 
| 404 408 | 
             
                        try:
         | 
| 405 409 | 
             
                            self.apply_rate_limit_state()
         | 
| @@ -409,6 +413,7 @@ class SyncRequest(ABC): | |
| 409 413 | 
             
                    logger.info("rate limit update worker exiting")
         | 
| 410 414 |  | 
| 411 415 | 
             
                def __cancel_checking_worker(self, cancellation_token:threading.Event):
         | 
| 416 | 
            +
                    context.attach(self.opentelemetry_context)
         | 
| 412 417 | 
             
                    """
         | 
| 413 418 | 
             
                    Designed to be run in a thread, this method checks to see if the sync run has been cancelled
         | 
| 414 419 | 
             
                    or if the deadline has been reached.
         | 
| @@ -810,22 +815,23 @@ class OutboundSyncRequest(SyncRequest): | |
| 810 815 | 
             
                    logger.debug("applying results to table")
         | 
| 811 816 | 
             
                    # use a random table name with a random string to avoid collisions
         | 
| 812 817 | 
             
                    with self._snowflake_query_lock:
         | 
| 813 | 
            -
                         | 
| 814 | 
            -
                             | 
| 815 | 
            -
                                 | 
| 816 | 
            -
                                     | 
| 817 | 
            -
             | 
| 818 | 
            -
             | 
| 819 | 
            -
             | 
| 820 | 
            -
             | 
| 821 | 
            -
             | 
| 822 | 
            -
             | 
| 823 | 
            -
                                     | 
| 824 | 
            -
                                         | 
| 818 | 
            +
                        with tracer.start_as_current_span("apply_results"):
         | 
| 819 | 
            +
                            for attempt in Retrying(stop=stop_after_attempt(30),wait=wait_fixed(2),reraise=True,retry=retry_if_exception_message(match=".*(is being|was) committed.*")):
         | 
| 820 | 
            +
                                with attempt:
         | 
| 821 | 
            +
                                    success, nchunks, nrows, _ = write_pandas(
         | 
| 822 | 
            +
                                        conn=self._session._conn._cursor.connection,  # pylint: disable=protected-access
         | 
| 823 | 
            +
                                        df=self._preprocess_results_dataframe(results_df),
         | 
| 824 | 
            +
                                        quote_identifiers=False,
         | 
| 825 | 
            +
                                        table_name=self._full_results_table_name,
         | 
| 826 | 
            +
                                        auto_create_table=False
         | 
| 827 | 
            +
                                    )
         | 
| 828 | 
            +
                                    if not success:
         | 
| 829 | 
            +
                                        raise ValueError(
         | 
| 830 | 
            +
                                            f"Failed to write results to table {self._full_results_table_name}"
         | 
| 831 | 
            +
                                        )
         | 
| 832 | 
            +
                                    logger.debug(
         | 
| 833 | 
            +
                                        f"Wrote {nrows} rows and {nchunks} chunks to table {self._full_results_table_name}"
         | 
| 825 834 | 
             
                                    )
         | 
| 826 | 
            -
                                logger.debug(
         | 
| 827 | 
            -
                                    f"Wrote {nrows} rows and {nchunks} chunks to table {self._full_results_table_name}"
         | 
| 828 | 
            -
                                )
         | 
| 829 835 |  | 
| 830 836 | 
             
                def __dataframe_wrapper(
         | 
| 831 837 | 
             
                    self, data_frame: pandas.DataFrame, render_jinja: bool = True
         | 
| @@ -1465,36 +1471,37 @@ class InboundSyncRequest(SyncRequest): | |
| 1465 1471 | 
             
                    """
         | 
| 1466 1472 | 
             
                    if len(results_df) > 0:
         | 
| 1467 1473 | 
             
                        with self._snowflake_query_lock:
         | 
| 1468 | 
            -
                             | 
| 1469 | 
            -
                                 | 
| 1470 | 
            -
                                     | 
| 1471 | 
            -
                                         | 
| 1472 | 
            -
             | 
| 1473 | 
            -
                                    # try setting parquet engine here, since the engine parameter does not seem to make it through to the write_pandas function
         | 
| 1474 | 
            -
                                    success, nchunks, nrows, _ = write_pandas(
         | 
| 1475 | 
            -
                                        conn=self._session._conn._cursor.connection,  # pylint: disable=protected-access
         | 
| 1476 | 
            -
                                        df=results_df,
         | 
| 1477 | 
            -
                                        table_name=self._full_results_table_name,
         | 
| 1478 | 
            -
                                        quote_identifiers=False,  # already done in get_temp_table_name
         | 
| 1479 | 
            -
                                        # schema='INBOUND_RAW', # it seems to be ok to provide schema in the table name
         | 
| 1480 | 
            -
                                        table_type="transient"
         | 
| 1481 | 
            -
                                    )
         | 
| 1482 | 
            -
                                    if not success:
         | 
| 1483 | 
            -
                                        raise ValueError(
         | 
| 1484 | 
            -
                                            f"Failed to write results to table {self._full_results_table_name}"
         | 
| 1474 | 
            +
                            with tracer.start_as_current_span("apply_results"):
         | 
| 1475 | 
            +
                                for attempt in Retrying(stop=stop_after_attempt(30),wait=wait_fixed(2),reraise=True,retry=retry_if_exception_message(match=".*(is being|was) committed.*")):
         | 
| 1476 | 
            +
                                    with attempt:
         | 
| 1477 | 
            +
                                        logger.debug(
         | 
| 1478 | 
            +
                                            f"Applying {len(results_df)} results to {self._full_results_table_name}"
         | 
| 1485 1479 | 
             
                                        )
         | 
| 1486 | 
            -
             | 
| 1487 | 
            -
                                         | 
| 1488 | 
            -
             | 
| 1489 | 
            -
             | 
| 1490 | 
            -
             | 
| 1491 | 
            -
             | 
| 1492 | 
            -
             | 
| 1493 | 
            -
             | 
| 1494 | 
            -
             | 
| 1495 | 
            -
             | 
| 1496 | 
            -
             | 
| 1497 | 
            -
             | 
| 1480 | 
            +
                                        # try setting parquet engine here, since the engine parameter does not seem to make it through to the write_pandas function
         | 
| 1481 | 
            +
                                        success, nchunks, nrows, _ = write_pandas(
         | 
| 1482 | 
            +
                                            conn=self._session._conn._cursor.connection,  # pylint: disable=protected-access
         | 
| 1483 | 
            +
                                            df=results_df,
         | 
| 1484 | 
            +
                                            table_name=self._full_results_table_name,
         | 
| 1485 | 
            +
                                            quote_identifiers=False,  # already done in get_temp_table_name
         | 
| 1486 | 
            +
                                            # schema='INBOUND_RAW', # it seems to be ok to provide schema in the table name
         | 
| 1487 | 
            +
                                            table_type="transient"
         | 
| 1488 | 
            +
                                        )
         | 
| 1489 | 
            +
                                        if not success:
         | 
| 1490 | 
            +
                                            raise ValueError(
         | 
| 1491 | 
            +
                                                f"Failed to write results to table {self._full_results_table_name}"
         | 
| 1492 | 
            +
                                            )
         | 
| 1493 | 
            +
                                        logger.debug(
         | 
| 1494 | 
            +
                                            f"Wrote {nrows} rows and {nchunks} chunks to table {self._full_results_table_name}"
         | 
| 1495 | 
            +
                                        )
         | 
| 1496 | 
            +
                                        # temp tables aren't allowed
         | 
| 1497 | 
            +
                                        # snowflake_df = self._session.create_dataframe(results_df)
         | 
| 1498 | 
            +
                                        # snowflake_df.write.save_as_table(table_name=temp_table,
         | 
| 1499 | 
            +
                                        #                                mode='append',
         | 
| 1500 | 
            +
                                        #                                column_order='index',
         | 
| 1501 | 
            +
                                        #                                #create_temp_table=True
         | 
| 1502 | 
            +
                                        #                                )
         | 
| 1503 | 
            +
                                        for stream_name in stream_names:
         | 
| 1504 | 
            +
                                            self._results_exist[stream_name] = True
         | 
| 1498 1505 | 
             
                    else:
         | 
| 1499 1506 | 
             
                        logger.debug("Results dataframe is empty, not applying")
         | 
| 1500 1507 |  | 
| @@ -1636,6 +1643,8 @@ class OmnataPlugin(ABC): | |
| 1636 1643 | 
             
                    take care of various status updates and loading enqueued results.
         | 
| 1637 1644 | 
             
                    Only set this to True if you plan to do all of this yourself (e.g. in a Java stored proc)
         | 
| 1638 1645 | 
             
                    """
         | 
| 1646 | 
            +
                    # store the opentelemetry context so that it can be attached inside threads
         | 
| 1647 | 
            +
                    self.opentelemetry_context = context.get_current()
         | 
| 1639 1648 |  | 
| 1640 1649 |  | 
| 1641 1650 | 
             
                @abstractmethod
         | 
| @@ -1945,6 +1954,7 @@ def __managed_outbound_processing_worker( | |
| 1945 1954 | 
             
                Consumes a fixed sized set of records by passing them to the wrapped function,
         | 
| 1946 1955 | 
             
                while adhering to the defined API constraints.
         | 
| 1947 1956 | 
             
                """
         | 
| 1957 | 
            +
                context.attach(plugin_class_obj.opentelemetry_context)
         | 
| 1948 1958 | 
             
                logger.debug(
         | 
| 1949 1959 | 
             
                    f"worker {worker_index} processing. Cancelled: {cancellation_token.is_set()}"
         | 
| 1950 1960 | 
             
                )
         | 
| @@ -2127,6 +2137,7 @@ def __managed_inbound_processing_worker( | |
| 2127 2137 | 
             
                A worker thread for the managed_inbound_processing annotation.
         | 
| 2128 2138 | 
             
                Passes single streams at a time to the wrapped function, adhering to concurrency constraints.
         | 
| 2129 2139 | 
             
                """
         | 
| 2140 | 
            +
                context.attach(plugin_class_obj.opentelemetry_context)
         | 
| 2130 2141 | 
             
                while not cancellation_token.is_set():
         | 
| 2131 2142 | 
             
                    # Get our generator object out of the queue
         | 
| 2132 2143 | 
             
                    logger.debug(
         | 
| @@ -2137,14 +2148,16 @@ def __managed_inbound_processing_worker( | |
| 2137 2148 | 
             
                        logger.debug(f"stream returned from queue: {stream}")
         | 
| 2138 2149 | 
             
                        # restore the first argument, was originally the dataframe/generator but now it's the appropriately sized dataframe
         | 
| 2139 2150 | 
             
                        try:
         | 
| 2140 | 
            -
                             | 
| 2141 | 
            -
             | 
| 2142 | 
            -
             | 
| 2143 | 
            -
             | 
| 2144 | 
            -
                                logger. | 
| 2145 | 
            -
             | 
| 2146 | 
            -
             | 
| 2147 | 
            -
                                 | 
| 2151 | 
            +
                            with tracer.start_as_current_span("managed_inbound_processing") as managed_inbound_processing_span:
         | 
| 2152 | 
            +
                                logger.debug(f"worker {worker_index} processing stream {stream.stream_name}, invoking plugin class method {method.__name__}")
         | 
| 2153 | 
            +
                                managed_inbound_processing_span.set_attribute("stream_name", stream.stream_name)
         | 
| 2154 | 
            +
                                result = method(plugin_class_obj, *(stream, *method_args), **method_kwargs)
         | 
| 2155 | 
            +
                                logger.debug(f"worker {worker_index} completed processing stream {stream.stream_name}")
         | 
| 2156 | 
            +
                                if result is not None and result is False:
         | 
| 2157 | 
            +
                                    logger.info(f"worker {worker_index} requested that {stream.stream_name} be not marked as complete")
         | 
| 2158 | 
            +
                                else:
         | 
| 2159 | 
            +
                                    logger.info(f"worker {worker_index} marking stream {stream.stream_name} as complete")
         | 
| 2160 | 
            +
                                    plugin_class_obj._sync_request.mark_stream_complete(stream.stream_name)
         | 
| 2148 2161 | 
             
                        except InterruptedWhileWaitingException:
         | 
| 2149 2162 | 
             
                            # If an inbound run is cancelled while waiting for rate limiting, this should mean that
         | 
| 2150 2163 | 
             
                            # the cancellation is handled elsewhere, so we don't need to do anything special here other than stop waiting
         | 
| @@ -24,7 +24,7 @@ from .configuration import ( | |
| 24 24 | 
             
                ConnectivityOption
         | 
| 25 25 | 
             
            )
         | 
| 26 26 | 
             
            from .forms import ConnectionMethod, FormInputField, FormOption
         | 
| 27 | 
            -
            from .logging import OmnataPluginLogHandler
         | 
| 27 | 
            +
            from .logging import OmnataPluginLogHandler, logger, tracer
         | 
| 28 28 | 
             
            from .omnata_plugin import (
         | 
| 29 29 | 
             
                SnowflakeBillingEvent,
         | 
| 30 30 | 
             
                BillingEventRequest,
         | 
| @@ -36,13 +36,10 @@ from .omnata_plugin import ( | |
| 36 36 | 
             
            )
         | 
| 37 37 | 
             
            from pydantic import TypeAdapter
         | 
| 38 38 | 
             
            from .rate_limiting import ApiLimits, RateLimitState
         | 
| 39 | 
            -
             | 
| 40 | 
            -
            # set the logger class to our custom logger so that pydantic errors are handled correctly
         | 
| 41 | 
            -
            logger = logging.getLogger(__name__)
         | 
| 39 | 
            +
            from opentelemetry import trace
         | 
| 42 40 |  | 
| 43 41 | 
             
            IMPORT_DIRECTORY_NAME = "snowflake_import_directory"
         | 
| 44 42 |  | 
| 45 | 
            -
             | 
| 46 43 | 
             
            class PluginEntrypoint:
         | 
| 47 44 | 
             
                """
         | 
| 48 45 | 
             
                This class gives each plugin's stored procs an initial point of contact.
         | 
| @@ -53,89 +50,107 @@ class PluginEntrypoint: | |
| 53 50 | 
             
                    self, plugin_fqn: str, session: Session, module_name: str, class_name: str
         | 
| 54 51 | 
             
                ):
         | 
| 55 52 | 
             
                    logger.info(f"Initialising plugin entrypoint for {plugin_fqn}")
         | 
| 56 | 
            -
                     | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
             | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 67 | 
            -
             | 
| 68 | 
            -
                         | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 71 | 
            -
                             | 
| 72 | 
            -
                                 | 
| 73 | 
            -
                                 | 
| 74 | 
            -
             | 
| 75 | 
            -
             | 
| 76 | 
            -
                                     | 
| 77 | 
            -
                                     | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 80 | 
            -
             | 
| 53 | 
            +
                    with tracer.start_as_current_span("plugin_initialization") as span:
         | 
| 54 | 
            +
                        self._session = session
         | 
| 55 | 
            +
                        import_dir = sys._xoptions[IMPORT_DIRECTORY_NAME]
         | 
| 56 | 
            +
                        span.add_event("Adding plugin zip to path")
         | 
| 57 | 
            +
                        sys.path.append(os.path.join(import_dir, "app.zip"))
         | 
| 58 | 
            +
                        span.add_event("Importing plugin module")
         | 
| 59 | 
            +
                        module = importlib.import_module(module_name)
         | 
| 60 | 
            +
                        class_obj = getattr(module, class_name)
         | 
| 61 | 
            +
                        self._plugin_instance: OmnataPlugin = class_obj()
         | 
| 62 | 
            +
                        self._plugin_instance._session = session  # pylint: disable=protected-access
         | 
| 63 | 
            +
                        # logging defaults
         | 
| 64 | 
            +
                        snowflake_logger = logging.getLogger("snowflake")
         | 
| 65 | 
            +
                        snowflake_logger.setLevel(logging.WARN) # we don't want snowflake queries being logged by default
         | 
| 66 | 
            +
                        # the sync engine can tell the plugin to override log level via a session variable
         | 
| 67 | 
            +
                        if session is not None:
         | 
| 68 | 
            +
                            try:
         | 
| 69 | 
            +
                                span.add_event("Checking log level overrides")
         | 
| 70 | 
            +
                                v = session.sql("select getvariable('LOG_LEVEL_OVERRIDES')").collect()
         | 
| 71 | 
            +
                                result = v[0][0]
         | 
| 72 | 
            +
                                if result is not None:
         | 
| 73 | 
            +
                                    log_level_overrides:Dict[str,str] = json.loads(result)
         | 
| 74 | 
            +
                                    span.add_event("Applying log level overrides",log_level_overrides)
         | 
| 75 | 
            +
                                    for logger_name,level in log_level_overrides.items():
         | 
| 76 | 
            +
                                        logger_override = logging.getLogger(logger_name)
         | 
| 77 | 
            +
                                        logger_override.setLevel(level)
         | 
| 78 | 
            +
                                        logger_override.propagate = False
         | 
| 79 | 
            +
                                        for handler in logger_override.handlers:
         | 
| 80 | 
            +
                                            handler.setLevel(level)
         | 
| 81 | 
            +
                            except Exception as e:
         | 
| 82 | 
            +
                                logger.error(f"Error setting log level overrides: {str(e)}")
         | 
| 81 83 |  | 
| 82 84 |  | 
| 83 85 | 
             
                def sync(self, sync_request: Dict):
         | 
| 84 | 
            -
                    logger.info("Entered sync method")
         | 
| 85 86 | 
             
                    request = TypeAdapter(SyncRequestPayload).validate_python(sync_request)
         | 
| 86 | 
            -
                     | 
| 87 | 
            -
             | 
| 88 | 
            -
                    )
         | 
| 89 | 
            -
                     | 
| 90 | 
            -
             | 
| 91 | 
            -
             | 
| 92 | 
            -
             | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 95 | 
            -
             | 
| 96 | 
            -
             | 
| 97 | 
            -
                         | 
| 98 | 
            -
             | 
| 99 | 
            -
             | 
| 100 | 
            -
             | 
| 101 | 
            -
                         | 
| 102 | 
            -
             | 
| 103 | 
            -
             | 
| 104 | 
            -
             | 
| 105 | 
            -
             | 
| 106 | 
            -
             | 
| 107 | 
            -
                         | 
| 108 | 
            -
             | 
| 109 | 
            -
             | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 112 | 
            -
             | 
| 113 | 
            -
                         | 
| 114 | 
            -
             | 
| 115 | 
            -
             | 
| 116 | 
            -
             | 
| 117 | 
            -
                             | 
| 118 | 
            -
             | 
| 119 | 
            -
             | 
| 120 | 
            -
                             | 
| 87 | 
            +
                    logger.add_extra('omnata.operation', 'sync')
         | 
| 88 | 
            +
                    logger.add_extra('omnata.sync.id', request.sync_id)
         | 
| 89 | 
            +
                    logger.add_extra('omnata.sync.direction', request.sync_direction)
         | 
| 90 | 
            +
                    logger.add_extra('omnata.connection.id', request.connection_id)
         | 
| 91 | 
            +
                    logger.add_extra('omnata.sync_run.id', request.run_id)
         | 
| 92 | 
            +
                    logger.add_extra('omnata.sync_branch.id', request.sync_branch_id)
         | 
| 93 | 
            +
                    logger.add_extra('omnata.sync_branch.name', request.sync_branch_name)
         | 
| 94 | 
            +
                    logger.info("Entered sync method")
         | 
| 95 | 
            +
                    with tracer.start_as_current_span("initialization") as span:
         | 
| 96 | 
            +
                        span.add_event("Fetching secrets")
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                        connection_secrets = get_secrets(
         | 
| 99 | 
            +
                            request.oauth_secret_name, request.other_secrets_name
         | 
| 100 | 
            +
                        )
         | 
| 101 | 
            +
                        span.add_event("Configuring log handler")
         | 
| 102 | 
            +
                        omnata_log_handler = OmnataPluginLogHandler(
         | 
| 103 | 
            +
                            session=self._session,
         | 
| 104 | 
            +
                            sync_id=request.sync_id,
         | 
| 105 | 
            +
                            sync_branch_id=request.sync_branch_id,
         | 
| 106 | 
            +
                            connection_id=request.connection_id,
         | 
| 107 | 
            +
                            sync_run_id=request.run_id,
         | 
| 108 | 
            +
                        )
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                        omnata_log_handler.register(
         | 
| 111 | 
            +
                            request.logging_level, self._plugin_instance.additional_loggers()
         | 
| 112 | 
            +
                        )
         | 
| 113 | 
            +
                        # construct some connection parameters for the purpose of getting the api limits
         | 
| 114 | 
            +
                        connection_parameters = ConnectionConfigurationParameters(
         | 
| 115 | 
            +
                            connection_method=request.connection_method,
         | 
| 116 | 
            +
                            connectivity_option=request.connectivity_option,
         | 
| 117 | 
            +
                            connection_parameters=request.connection_parameters,
         | 
| 118 | 
            +
                            connection_secrets=connection_secrets
         | 
| 119 | 
            +
                        )
         | 
| 120 | 
            +
                        if request.oauth_secret_name is not None:
         | 
| 121 | 
            +
                            connection_parameters.access_token_secret_name = request.oauth_secret_name
         | 
| 122 | 
            +
                        span.add_event("Configuring API Limits")
         | 
| 123 | 
            +
                        all_api_limits = self._plugin_instance.api_limits(connection_parameters)
         | 
| 124 | 
            +
                        logger.info(
         | 
| 125 | 
            +
                            f"Default API limits: {json.dumps(to_jsonable_python(all_api_limits))}"
         | 
| 126 | 
            +
                        )
         | 
| 127 | 
            +
                        all_api_limits_by_category = {
         | 
| 128 | 
            +
                            api_limit.endpoint_category: api_limit for api_limit in all_api_limits
         | 
| 121 129 | 
             
                        }
         | 
| 122 | 
            -
             | 
| 123 | 
            -
             | 
| 124 | 
            -
             | 
| 125 | 
            -
             | 
| 126 | 
            -
             | 
| 127 | 
            -
             | 
| 128 | 
            -
             | 
| 129 | 
            -
             | 
| 130 | 
            -
             | 
| 131 | 
            -
                         | 
| 132 | 
            -
             | 
| 133 | 
            -
             | 
| 134 | 
            -
             | 
| 135 | 
            -
                         | 
| 136 | 
            -
             | 
| 137 | 
            -
             | 
| 138 | 
            -
                             | 
| 130 | 
            +
                        all_api_limits_by_category.update(
         | 
| 131 | 
            +
                            {
         | 
| 132 | 
            +
                                k: v
         | 
| 133 | 
            +
                                for k, v in [
         | 
| 134 | 
            +
                                    (x.endpoint_category, x) for x in request.api_limit_overrides
         | 
| 135 | 
            +
                                ]
         | 
| 136 | 
            +
                            }
         | 
| 137 | 
            +
                        )
         | 
| 138 | 
            +
                        api_limits = list(all_api_limits_by_category.values())
         | 
| 139 | 
            +
                        return_dict = {}
         | 
| 140 | 
            +
                        logger.info(
         | 
| 141 | 
            +
                            f"Rate limits state: {json.dumps(to_jsonable_python(request.rate_limits_state))}"
         | 
| 142 | 
            +
                        )
         | 
| 143 | 
            +
                        (rate_limit_state_all, rate_limit_state_this_branch) = RateLimitState.collapse(request.rate_limits_state,request.sync_id, request.sync_branch_name)
         | 
| 144 | 
            +
                        # if any endpoint categories have no state, give them an empty state
         | 
| 145 | 
            +
                        for api_limit in api_limits:
         | 
| 146 | 
            +
                            if api_limit.endpoint_category not in rate_limit_state_all:
         | 
| 147 | 
            +
                                rate_limit_state_all[api_limit.endpoint_category] = RateLimitState(
         | 
| 148 | 
            +
                                    wait_until=None, previous_request_timestamps=[]
         | 
| 149 | 
            +
                                )
         | 
| 150 | 
            +
                            if api_limit.endpoint_category not in rate_limit_state_this_branch:
         | 
| 151 | 
            +
                                rate_limit_state_this_branch[api_limit.endpoint_category] = RateLimitState(
         | 
| 152 | 
            +
                                    wait_until=None, previous_request_timestamps=[]
         | 
| 153 | 
            +
                                )
         | 
| 139 154 |  | 
| 140 155 | 
             
                    if request.sync_direction == "outbound":
         | 
| 141 156 | 
             
                        parameters = OutboundSyncConfigurationParameters(
         | 
| @@ -169,11 +184,13 @@ class PluginEntrypoint: | |
| 169 184 | 
             
                        )
         | 
| 170 185 | 
             
                        try:
         | 
| 171 186 | 
             
                            self._plugin_instance._configuration_parameters = parameters
         | 
| 172 | 
            -
                            with  | 
| 173 | 
            -
                                 | 
| 187 | 
            +
                            with tracer.start_as_current_span("invoke_plugin") as span:
         | 
| 188 | 
            +
                                with HttpRateLimiting(outbound_sync_request, parameters):
         | 
| 189 | 
            +
                                    self._plugin_instance.sync_outbound(parameters, outbound_sync_request)
         | 
| 174 190 | 
             
                            if self._plugin_instance.disable_background_workers is False:
         | 
| 175 | 
            -
                                 | 
| 176 | 
            -
             | 
| 191 | 
            +
                                with tracer.start_as_current_span("results_finalization") as span:
         | 
| 192 | 
            +
                                    outbound_sync_request.apply_results_queue()
         | 
| 193 | 
            +
                                    outbound_sync_request.apply_rate_limit_state()
         | 
| 177 194 | 
             
                            if outbound_sync_request.deadline_reached:
         | 
| 178 195 | 
             
                                # if we actually hit the deadline, this is flagged by the cancellation checking worker and the cancellation
         | 
| 179 196 | 
             
                                # token is set. We throw it here as an error since that's currently how it flows back to the engine with a DELAYED state
         | 
| @@ -227,19 +244,21 @@ class PluginEntrypoint: | |
| 227 244 | 
             
                            inbound_sync_request.update_activity("Invoking plugin")
         | 
| 228 245 | 
             
                            logger.info(f"inbound sync request: {inbound_sync_request}")
         | 
| 229 246 | 
             
                            # plugin_instance._inbound_sync_request = outbound_sync_request
         | 
| 230 | 
            -
                            with  | 
| 231 | 
            -
                                 | 
| 247 | 
            +
                            with tracer.start_as_current_span("invoke_plugin"):
         | 
| 248 | 
            +
                                with HttpRateLimiting(inbound_sync_request, parameters):
         | 
| 249 | 
            +
                                    self._plugin_instance.sync_inbound(parameters, inbound_sync_request)
         | 
| 232 250 | 
             
                            logger.info("Finished invoking plugin")
         | 
| 233 251 | 
             
                            if self._plugin_instance.disable_background_workers is False:
         | 
| 234 | 
            -
                                 | 
| 235 | 
            -
             | 
| 236 | 
            -
             | 
| 237 | 
            -
             | 
| 238 | 
            -
                                     | 
| 239 | 
            -
             | 
| 240 | 
            -
             | 
| 241 | 
            -
             | 
| 242 | 
            -
                                     | 
| 252 | 
            +
                                with tracer.start_as_current_span("results_finalization") as span:
         | 
| 253 | 
            +
                                    inbound_sync_request.update_activity("Staging remaining records")
         | 
| 254 | 
            +
                                    logger.info("Calling apply_results_queue")
         | 
| 255 | 
            +
                                    inbound_sync_request.apply_results_queue()
         | 
| 256 | 
            +
                                    try:
         | 
| 257 | 
            +
                                        # this is not critical, we wouldn't fail the sync over rate limit usage capture
         | 
| 258 | 
            +
                                        logger.info("Calling apply_rate_limit_state")
         | 
| 259 | 
            +
                                        inbound_sync_request.apply_rate_limit_state()
         | 
| 260 | 
            +
                                    except Exception as e:
         | 
| 261 | 
            +
                                        logger.error(f"Error applying rate limit state: {str(e)}")
         | 
| 243 262 | 
             
                                # here we used to do a final inbound_sync_request.apply_progress_updates(ignore_errors=False)
         | 
| 244 263 | 
             
                                # but it was erroring too much since there was usually a lot of DDL activity on the Snowflake side
         | 
| 245 264 | 
             
                                # so instead, we'll provide a final progress update via a return value from the proc
         | 
| @@ -283,6 +302,14 @@ class PluginEntrypoint: | |
| 283 302 | 
             
                    sync_parameters: Dict,
         | 
| 284 303 | 
             
                    current_form_parameters: Optional[Dict],
         | 
| 285 304 | 
             
                ):
         | 
| 305 | 
            +
                    if function_name is None:
         | 
| 306 | 
            +
                        function_name = f"{sync_direction}_configuration_form"
         | 
| 307 | 
            +
                    logger.add_extra('omnata.operation', 'configuration_form')
         | 
| 308 | 
            +
                    logger.add_extra('omnata.connection.connectivity_option', connectivity_option)
         | 
| 309 | 
            +
                    logger.add_extra('omnata.connection.connection_method', connection_method)
         | 
| 310 | 
            +
                    logger.add_extra('omnata.configuration_form.function_name', function_name)
         | 
| 311 | 
            +
                    logger.add_extra('omnata.sync.direction', sync_direction)
         | 
| 312 | 
            +
             | 
| 286 313 | 
             
                    logger.info("Entered configuration_form method")
         | 
| 287 314 | 
             
                    sync_strategy = normalise_nulls(sync_strategy)
         | 
| 288 315 | 
             
                    oauth_secret_name = normalise_nulls(oauth_secret_name)
         | 
| @@ -322,9 +349,10 @@ class PluginEntrypoint: | |
| 322 349 | 
             
                            parameters.access_token_secret_name = oauth_secret_name
         | 
| 323 350 | 
             
                    the_function = getattr(
         | 
| 324 351 | 
             
                        self._plugin_instance,
         | 
| 325 | 
            -
                        function_name | 
| 352 | 
            +
                        function_name
         | 
| 326 353 | 
             
                    )
         | 
| 327 | 
            -
                     | 
| 354 | 
            +
                    with tracer.start_as_current_span("invoke_plugin"):
         | 
| 355 | 
            +
                        script_result = the_function(parameters)
         | 
| 328 356 | 
             
                    if isinstance(script_result, BaseModel):
         | 
| 329 357 | 
             
                        script_result = script_result.model_dump()
         | 
| 330 358 | 
             
                    elif isinstance(script_result, List):
         | 
| @@ -342,6 +370,10 @@ class PluginEntrypoint: | |
| 342 370 | 
             
                    sync_parameters: Dict,
         | 
| 343 371 | 
             
                    selected_streams: Optional[List[str]], # None to return all streams without requiring schema
         | 
| 344 372 | 
             
                ):
         | 
| 373 | 
            +
                    logger.add_extra('omnata.operation', 'list_streams')
         | 
| 374 | 
            +
                    logger.add_extra('omnata.connection.connectivity_option', connectivity_option)
         | 
| 375 | 
            +
                    logger.add_extra('omnata.connection.connection_method', connection_method)
         | 
| 376 | 
            +
                    logger.add_extra('omnata.sync.direction', 'inbound')
         | 
| 345 377 | 
             
                    logger.debug("Entered list_streams method")
         | 
| 346 378 | 
             
                    oauth_secret_name = normalise_nulls(oauth_secret_name)
         | 
| 347 379 | 
             
                    other_secrets_name = normalise_nulls(other_secrets_name)
         | 
| @@ -362,8 +394,8 @@ class PluginEntrypoint: | |
| 362 394 | 
             
                    )
         | 
| 363 395 | 
             
                    if oauth_secret_name is not None:
         | 
| 364 396 | 
             
                        parameters.access_token_secret_name = oauth_secret_name
         | 
| 365 | 
            -
             | 
| 366 | 
            -
             | 
| 397 | 
            +
                    with tracer.start_as_current_span("invoke_plugin"):
         | 
| 398 | 
            +
                        script_result = self._plugin_instance.inbound_stream_list(parameters)
         | 
| 367 399 | 
             
                    if isinstance(script_result, BaseModel):
         | 
| 368 400 | 
             
                        script_result = script_result.model_dump()
         | 
| 369 401 | 
             
                    elif isinstance(script_result, List):
         | 
| @@ -393,20 +425,25 @@ class PluginEntrypoint: | |
| 393 425 | 
             
                    return results
         | 
| 394 426 |  | 
| 395 427 | 
             
                def connection_form(self,connectivity_option: str):
         | 
| 428 | 
            +
                    logger.add_extra('omnata.operation', 'connection_form')
         | 
| 429 | 
            +
                    logger.add_extra('omnata.connection.connectivity_option', connectivity_option)
         | 
| 396 430 | 
             
                    connectivity_option = TypeAdapter(ConnectivityOption).validate_python(connectivity_option)
         | 
| 397 431 | 
             
                    logger.info("Entered connection_form method")
         | 
| 398 | 
            -
                     | 
| 399 | 
            -
                         | 
| 400 | 
            -
             | 
| 401 | 
            -
                         | 
| 432 | 
            +
                    with tracer.start_as_current_span("invoke_plugin"):
         | 
| 433 | 
            +
                        if self._plugin_instance.connection_form.__code__.co_argcount==1:
         | 
| 434 | 
            +
                            form: List[ConnectionMethod] = self._plugin_instance.connection_form()
         | 
| 435 | 
            +
                        else:
         | 
| 436 | 
            +
                            form: List[ConnectionMethod] = self._plugin_instance.connection_form(connectivity_option)
         | 
| 402 437 | 
             
                    return [f.model_dump() for f in form]
         | 
| 403 438 |  | 
| 404 439 | 
             
                def create_billing_events(self, session, event_request: Dict):
         | 
| 440 | 
            +
                    logger.add_extra('omnata.operation', 'create_billing_events')
         | 
| 405 441 | 
             
                    logger.info("Entered create_billing_events method")
         | 
| 406 442 | 
             
                    request = TypeAdapter(BillingEventRequest).validate_python(event_request)
         | 
| 407 | 
            -
                     | 
| 408 | 
            -
                         | 
| 409 | 
            -
             | 
| 443 | 
            +
                    with tracer.start_as_current_span("invoke_plugin"):
         | 
| 444 | 
            +
                        events: List[SnowflakeBillingEvent] = self._plugin_instance.create_billing_events(
         | 
| 445 | 
            +
                            request
         | 
| 446 | 
            +
                        )
         | 
| 410 447 | 
             
                    # create each billing event, waiting a second between each one
         | 
| 411 448 | 
             
                    first_time = True
         | 
| 412 449 | 
             
                    for billing_event in events:
         | 
| @@ -474,6 +511,9 @@ class PluginEntrypoint: | |
| 474 511 | 
             
                    oauth_secret_name: Optional[str],
         | 
| 475 512 | 
             
                    other_secrets_name: Optional[str],
         | 
| 476 513 | 
             
                ):
         | 
| 514 | 
            +
                    logger.add_extra('omnata.operation', 'connection_test')
         | 
| 515 | 
            +
                    logger.add_extra('omnata.connection.connectivity_option', connectivity_option)
         | 
| 516 | 
            +
                    logger.add_extra('omnata.connection.connection_method', method)
         | 
| 477 517 | 
             
                    logger.info("Entered connect method")
         | 
| 478 518 | 
             
                    logger.info(f"Connection parameters: {connection_parameters}")
         | 
| 479 519 | 
             
                    connection_secrets = get_secrets(oauth_secret_name, other_secrets_name)
         | 
| @@ -490,31 +530,35 @@ class PluginEntrypoint: | |
| 490 530 | 
             
                    )
         | 
| 491 531 | 
             
                    if oauth_secret_name is not None:
         | 
| 492 532 | 
             
                        parameters.access_token_secret_name = oauth_secret_name
         | 
| 493 | 
            -
                     | 
| 494 | 
            -
                         | 
| 495 | 
            -
             | 
| 533 | 
            +
                    with tracer.start_as_current_span("invoke_plugin"):
         | 
| 534 | 
            +
                        connect_response = self._plugin_instance.connect(
         | 
| 535 | 
            +
                            parameters=parameters
         | 
| 536 | 
            +
                        )
         | 
| 496 537 | 
             
                    # the connect method can also return more network addresses. If so, we need to update the
         | 
| 497 538 | 
             
                    # network rule associated with the external access integration
         | 
| 498 539 | 
             
                    if connect_response is None:
         | 
| 499 540 | 
             
                        raise ValueError("Plugin did not return a ConnectResponse object from the connect method")
         | 
| 500 541 | 
             
                    if connect_response.network_addresses is not None:
         | 
| 501 | 
            -
                         | 
| 502 | 
            -
                             | 
| 503 | 
            -
             | 
| 504 | 
            -
             | 
| 505 | 
            -
             | 
| 506 | 
            -
             | 
| 507 | 
            -
             | 
| 508 | 
            -
                             | 
| 509 | 
            -
             | 
| 510 | 
            -
             | 
| 511 | 
            -
             | 
| 512 | 
            -
             | 
| 513 | 
            -
             | 
| 514 | 
            -
             | 
| 515 | 
            -
             | 
| 516 | 
            -
                            f" | 
| 517 | 
            -
             | 
| 542 | 
            +
                        with tracer.start_as_current_span("network_rule_update") as network_rule_update_span:
         | 
| 543 | 
            +
                            network_rule_update_span.add_event("Retrieving existing network rule")
         | 
| 544 | 
            +
                            existing_rule_result = self._session.sql(
         | 
| 545 | 
            +
                                f"desc network rule {network_rule_name}"
         | 
| 546 | 
            +
                            ).collect()
         | 
| 547 | 
            +
                            rule_values: List[str] = existing_rule_result[0].value_list.split(",")
         | 
| 548 | 
            +
                            rule_values = [r for r in rule_values if r != '']
         | 
| 549 | 
            +
                            logger.info(f"Existing rules for {network_rule_name}: {rule_values}")
         | 
| 550 | 
            +
                            for network_address in connect_response.network_addresses:
         | 
| 551 | 
            +
                                if network_address not in rule_values:
         | 
| 552 | 
            +
                                    rule_values.append(network_address)
         | 
| 553 | 
            +
                            #if len(rule_values)==0:
         | 
| 554 | 
            +
                            #    logger.info("No network addresses for plugin, adding localhost")
         | 
| 555 | 
            +
                            #    rule_values.append("https://localhost")
         | 
| 556 | 
            +
                            logger.info(f"New rules for {network_rule_name}: {rule_values}")
         | 
| 557 | 
            +
                            rule_values_string = ",".join([f"'{value}'" for value in rule_values])
         | 
| 558 | 
            +
                            network_rule_update_span.add_event("Updating network rule")
         | 
| 559 | 
            +
                            self._session.sql(
         | 
| 560 | 
            +
                                f"alter network rule {network_rule_name} set value_list = ({rule_values_string})"
         | 
| 561 | 
            +
                            ).collect()
         | 
| 518 562 |  | 
| 519 563 | 
             
                    return connect_response.model_dump()
         | 
| 520 564 |  | 
| @@ -524,6 +568,9 @@ class PluginEntrypoint: | |
| 524 568 | 
             
                                connection_parameters: Dict,
         | 
| 525 569 | 
             
                                oauth_secret_name: Optional[str],
         | 
| 526 570 | 
             
                                other_secrets_name: Optional[str]):
         | 
| 571 | 
            +
                    logger.add_extra('omnata.operation', 'api_limits')
         | 
| 572 | 
            +
                    logger.add_extra('omnata.connection.connectivity_option', connectivity_option)
         | 
| 573 | 
            +
                    logger.add_extra('omnata.connection.connection_method', method)
         | 
| 527 574 | 
             
                    logger.info("Entered api_limits method")
         | 
| 528 575 | 
             
                    connection_secrets = get_secrets(oauth_secret_name, other_secrets_name)
         | 
| 529 576 | 
             
                    from omnata_plugin_runtime.omnata_plugin import (
         | 
| @@ -538,7 +585,8 @@ class PluginEntrypoint: | |
| 538 585 | 
             
                    )
         | 
| 539 586 | 
             
                    if oauth_secret_name is not None:
         | 
| 540 587 | 
             
                        connection_parameters.access_token_secret_name = oauth_secret_name
         | 
| 541 | 
            -
                     | 
| 588 | 
            +
                    with tracer.start_as_current_span("invoke_plugin"):
         | 
| 589 | 
            +
                        response: List[ApiLimits] = self._plugin_instance.api_limits(connection_parameters)
         | 
| 542 590 | 
             
                    return [api_limit.model_dump() for api_limit in response]
         | 
| 543 591 |  | 
| 544 592 | 
             
                def outbound_record_validator(
         | 
| @@ -15,12 +15,11 @@ import logging | |
| 15 15 | 
             
            from pydantic import Field, root_validator, PrivateAttr, field_serializer
         | 
| 16 16 | 
             
            from pydantic_core import to_jsonable_python
         | 
| 17 17 | 
             
            from .configuration import SubscriptableBaseModel
         | 
| 18 | 
            +
            from .logging import logger, tracer
         | 
| 18 19 | 
             
            import pytz
         | 
| 19 20 | 
             
            from requests.adapters import HTTPAdapter
         | 
| 20 21 | 
             
            from urllib3.util.retry import Retry
         | 
| 21 22 |  | 
| 22 | 
            -
            logger = getLogger(__name__)
         | 
| 23 | 
            -
             | 
| 24 23 | 
             
            TimeUnitType = Literal["second", "minute", "hour", "day"]
         | 
| 25 24 |  | 
| 26 25 | 
             
            HttpMethodType = Literal[
         | 
| @@ -384,11 +383,12 @@ class RetryWithLogging(Retry): | |
| 384 383 | 
             
                    retry_after = self.get_retry_after(response)
         | 
| 385 384 | 
             
                    if retry_after:
         | 
| 386 385 | 
             
                        logger.info(f"Retrying after {retry_after} seconds due to Retry-After header")
         | 
| 387 | 
            -
                         | 
| 388 | 
            -
                             | 
| 389 | 
            -
             | 
| 390 | 
            -
                             | 
| 391 | 
            -
                                 | 
| 386 | 
            +
                        with tracer.start_as_current_span("http_retry_wait"):
         | 
| 387 | 
            +
                            if self.thread_cancellation_token is None:
         | 
| 388 | 
            +
                                time.sleep(retry_after)
         | 
| 389 | 
            +
                            else:
         | 
| 390 | 
            +
                                if self.thread_cancellation_token.wait(retry_after):
         | 
| 391 | 
            +
                                    raise InterruptedWhileWaitingException(message="The sync was interrupted while waiting for rate limiting to expire")
         | 
| 392 392 | 
             
                        return True
         | 
| 393 393 | 
             
                    return False
         | 
| 394 394 |  | 
| @@ -505,8 +505,9 @@ class RateLimitedSession(requests.Session): | |
| 505 505 | 
             
                                    raise InterruptedWhileWaitingException(message=f"The rate limiting wait time ({wait_time} seconds) would exceed the run deadline")
         | 
| 506 506 | 
             
                                logger.info(f"Waiting for {wait_time} seconds before retrying {method} request to {url}")
         | 
| 507 507 | 
             
                                # if wait() returns true, it means that the thread was cancelled
         | 
| 508 | 
            -
                                 | 
| 509 | 
            -
                                     | 
| 508 | 
            +
                                with tracer.start_as_current_span("http_retry_wait"):
         | 
| 509 | 
            +
                                    if self.thread_cancellation_token.wait(wait_time):
         | 
| 510 | 
            +
                                        raise InterruptedWhileWaitingException(message="The sync was interrupted while waiting for rate limiting to expire")
         | 
| 510 511 | 
             
                            else:
         | 
| 511 512 | 
             
                                current_url_retries = self.increment_retries(url)
         | 
| 512 513 | 
             
                                if current_url_retries >= self.max_retries:
         | 
| @@ -515,8 +516,9 @@ class RateLimitedSession(requests.Session): | |
| 515 516 | 
             
                                if datetime.datetime.now(pytz.UTC) + datetime.timedelta(seconds=backoff_time) > self.run_deadline:
         | 
| 516 517 | 
             
                                    raise InterruptedWhileWaitingException(message=f"The rate limiting backoff time ({backoff_time} seconds) would exceed the run deadline")
         | 
| 517 518 | 
             
                                logger.info(f"Waiting for {backoff_time} seconds before retrying {method} request to {url}")
         | 
| 518 | 
            -
                                 | 
| 519 | 
            -
                                     | 
| 519 | 
            +
                                with tracer.start_as_current_span("http_retry_wait"):
         | 
| 520 | 
            +
                                    if self.thread_cancellation_token.wait(backoff_time):
         | 
| 521 | 
            +
                                        raise InterruptedWhileWaitingException(message="The sync was interrupted while waiting for rate limiting backoff")
         | 
| 520 522 | 
             
                        else:
         | 
| 521 523 | 
             
                            self.set_retries(url,0)  # Reset retries if the request is successful
         | 
| 522 524 | 
             
                            return response
         | 
    
        {omnata_plugin_runtime-0.7.0a184.dist-info → omnata_plugin_runtime-0.8.0.dist-info}/METADATA
    RENAMED
    
    | @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            Metadata-Version: 2.1
         | 
| 2 2 | 
             
            Name: omnata-plugin-runtime
         | 
| 3 | 
            -
            Version: 0. | 
| 3 | 
            +
            Version: 0.8.0
         | 
| 4 4 | 
             
            Summary: Classes and common runtime components for building and running Omnata Plugins
         | 
| 5 5 | 
             
            Author: James Weakley
         | 
| 6 6 | 
             
            Author-email: james.weakley@omnata.com
         | 
| @@ -19,9 +19,11 @@ Requires-Dist: idna (<=3.7) | |
| 19 19 | 
             
            Requires-Dist: jinja2 (>=3.1.2,<=3.1.4)
         | 
| 20 20 | 
             
            Requires-Dist: markupsafe (<=2.1.3)
         | 
| 21 21 | 
             
            Requires-Dist: numpy (<1.27.0)
         | 
| 22 | 
            +
            Requires-Dist: opentelemetry-api (<=1.23.0)
         | 
| 22 23 | 
             
            Requires-Dist: packaging (<=24.1)
         | 
| 23 24 | 
             
            Requires-Dist: pandas (<=2.2.2)
         | 
| 24 25 | 
             
            Requires-Dist: platformdirs (<=3.10.0)
         | 
| 26 | 
            +
            Requires-Dist: protobuf (<=4.25.3)
         | 
| 25 27 | 
             
            Requires-Dist: pyarrow (<=16.1.0)
         | 
| 26 28 | 
             
            Requires-Dist: pycparser (<=2.21)
         | 
| 27 29 | 
             
            Requires-Dist: pydantic (>=2,<=2.8.2)
         | 
| @@ -34,10 +36,12 @@ Requires-Dist: requests (>=2,<=2.32.3) | |
| 34 36 | 
             
            Requires-Dist: setuptools (<=72.1.0)
         | 
| 35 37 | 
             
            Requires-Dist: snowflake-connector-python (>=3,<=3.12.0)
         | 
| 36 38 | 
             
            Requires-Dist: snowflake-snowpark-python (==1.23.0)
         | 
| 39 | 
            +
            Requires-Dist: snowflake-telemetry-python (<=0.5.0)
         | 
| 37 40 | 
             
            Requires-Dist: tenacity (>=8,<=8.2.3)
         | 
| 38 41 | 
             
            Requires-Dist: tomlkit (<=0.11.1)
         | 
| 39 42 | 
             
            Requires-Dist: urllib3 (<=2.2.2)
         | 
| 40 43 | 
             
            Requires-Dist: wheel (<=0.43.0)
         | 
| 44 | 
            +
            Requires-Dist: wrapt (<=1.14.1)
         | 
| 41 45 | 
             
            Description-Content-Type: text/markdown
         | 
| 42 46 |  | 
| 43 47 | 
             
            # omnata-plugin-runtime
         | 
| @@ -0,0 +1,12 @@ | |
| 1 | 
            +
            omnata_plugin_runtime/__init__.py,sha256=MS9d1whnfT_B3-ThqZ7l63QeC_8OEKTuaYV5wTwRpBA,1576
         | 
| 2 | 
            +
            omnata_plugin_runtime/api.py,sha256=tVi4KLL0v5N3yz3Ie0kSyFemryu572gCbtSRfWN6wBU,6523
         | 
| 3 | 
            +
            omnata_plugin_runtime/configuration.py,sha256=6JmgE4SL3F5cGlDYqt17A1vTFu6nB74yWgEpQ5qV9ho,38380
         | 
| 4 | 
            +
            omnata_plugin_runtime/forms.py,sha256=ueodN2GIMS5N9fqebpY4uNGJnjEb9HcuaVQVfWH-cGg,19838
         | 
| 5 | 
            +
            omnata_plugin_runtime/logging.py,sha256=WBuZt8lF9E5oFWM4KYQbE8dDJ_HctJ1pN3BHwU6rcd0,4461
         | 
| 6 | 
            +
            omnata_plugin_runtime/omnata_plugin.py,sha256=GAvFRnx02_beTuw1LhPgOBxS_cTJmjDXc4EwVk69ZY8,131191
         | 
| 7 | 
            +
            omnata_plugin_runtime/plugin_entrypoints.py,sha256=sB_h6OBEMk7lTLIjdNrNo9Sthk8UE9PnK2AUcQJPe9I,32728
         | 
| 8 | 
            +
            omnata_plugin_runtime/rate_limiting.py,sha256=eOWVRYWiqPlVeYzmB1exVXfXbrcpmYb7vtTi9B-4zkQ,25868
         | 
| 9 | 
            +
            omnata_plugin_runtime-0.8.0.dist-info/LICENSE,sha256=rGaMQG3R3F5-JGDp_-rlMKpDIkg5n0SI4kctTk8eZSI,56
         | 
| 10 | 
            +
            omnata_plugin_runtime-0.8.0.dist-info/METADATA,sha256=ezmYz0AgrYsHNQlaR3k9uimL8ag-RpoWbZE6AnlxB1I,2144
         | 
| 11 | 
            +
            omnata_plugin_runtime-0.8.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
         | 
| 12 | 
            +
            omnata_plugin_runtime-0.8.0.dist-info/RECORD,,
         | 
| @@ -1,12 +0,0 @@ | |
| 1 | 
            -
            omnata_plugin_runtime/__init__.py,sha256=MS9d1whnfT_B3-ThqZ7l63QeC_8OEKTuaYV5wTwRpBA,1576
         | 
| 2 | 
            -
            omnata_plugin_runtime/api.py,sha256=tVi4KLL0v5N3yz3Ie0kSyFemryu572gCbtSRfWN6wBU,6523
         | 
| 3 | 
            -
            omnata_plugin_runtime/configuration.py,sha256=Yyz3trj7G6nh3JyEw6S5qlrXUfS_MB-vZgATcNdWzp0,38339
         | 
| 4 | 
            -
            omnata_plugin_runtime/forms.py,sha256=ueodN2GIMS5N9fqebpY4uNGJnjEb9HcuaVQVfWH-cGg,19838
         | 
| 5 | 
            -
            omnata_plugin_runtime/logging.py,sha256=bn7eKoNWvtuyTk7RTwBS9UARMtqkiICtgMtzq3KA2V0,3272
         | 
| 6 | 
            -
            omnata_plugin_runtime/omnata_plugin.py,sha256=aggjb_CTTjhgqjS8CHPOm4ENU0jNcYoT6LC8yI1IeF4,130048
         | 
| 7 | 
            -
            omnata_plugin_runtime/plugin_entrypoints.py,sha256=Ulu4udC4tfCH-EA3VGXAT_WKHw5yZ6_ulsL7SjAN0qo,28953
         | 
| 8 | 
            -
            omnata_plugin_runtime/rate_limiting.py,sha256=JukA0l7x7Klqz2b54mR-poP7NRxpUHgWSGp6h0B8u6Q,25612
         | 
| 9 | 
            -
            omnata_plugin_runtime-0.7.0a184.dist-info/LICENSE,sha256=rGaMQG3R3F5-JGDp_-rlMKpDIkg5n0SI4kctTk8eZSI,56
         | 
| 10 | 
            -
            omnata_plugin_runtime-0.7.0a184.dist-info/METADATA,sha256=CsI5s6i4uyWKk2g6aUHbSHq9WXkclnRVQL3rDQ7rY4s,1985
         | 
| 11 | 
            -
            omnata_plugin_runtime-0.7.0a184.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
         | 
| 12 | 
            -
            omnata_plugin_runtime-0.7.0a184.dist-info/RECORD,,
         | 
| 
            File without changes
         | 
| 
            File without changes
         |