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
|