omnata-plugin-runtime 0.7.0a185__tar.gz → 0.8.0__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {omnata_plugin_runtime-0.7.0a185 → omnata_plugin_runtime-0.8.0}/PKG-INFO +5 -1
- {omnata_plugin_runtime-0.7.0a185 → omnata_plugin_runtime-0.8.0}/pyproject.toml +5 -1
- {omnata_plugin_runtime-0.7.0a185 → omnata_plugin_runtime-0.8.0}/src/omnata_plugin_runtime/configuration.py +2 -4
- {omnata_plugin_runtime-0.7.0a185 → omnata_plugin_runtime-0.8.0}/src/omnata_plugin_runtime/logging.py +29 -1
- {omnata_plugin_runtime-0.7.0a185 → omnata_plugin_runtime-0.8.0}/src/omnata_plugin_runtime/omnata_plugin.py +67 -54
- {omnata_plugin_runtime-0.7.0a185 → omnata_plugin_runtime-0.8.0}/src/omnata_plugin_runtime/plugin_entrypoints.py +178 -130
- {omnata_plugin_runtime-0.7.0a185 → omnata_plugin_runtime-0.8.0}/src/omnata_plugin_runtime/rate_limiting.py +13 -11
- {omnata_plugin_runtime-0.7.0a185 → omnata_plugin_runtime-0.8.0}/LICENSE +0 -0
- {omnata_plugin_runtime-0.7.0a185 → omnata_plugin_runtime-0.8.0}/README.md +0 -0
- {omnata_plugin_runtime-0.7.0a185 → omnata_plugin_runtime-0.8.0}/src/omnata_plugin_runtime/__init__.py +0 -0
- {omnata_plugin_runtime-0.7.0a185 → omnata_plugin_runtime-0.8.0}/src/omnata_plugin_runtime/api.py +0 -0
- {omnata_plugin_runtime-0.7.0a185 → omnata_plugin_runtime-0.8.0}/src/omnata_plugin_runtime/forms.py +0 -0
@@ -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
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "omnata-plugin-runtime"
|
3
|
-
version = "0.
|
3
|
+
version = "0.8.0"
|
4
4
|
description = "Classes and common runtime components for building and running Omnata Plugins"
|
5
5
|
authors = ["James Weakley <james.weakley@omnata.com>"]
|
6
6
|
readme = "README.md"
|
@@ -38,6 +38,10 @@ wheel = "<=0.43.0" # latest version available on Snowflake Anaconda
|
|
38
38
|
pyyaml = "<=6.0.1" # latest version available on Snowflake Anaconda
|
39
39
|
cffi = "<=1.16.0" # latest version available on Snowflake Anaconda
|
40
40
|
pyarrow = "<=16.1.0" # latest version available on Snowflake Anaconda
|
41
|
+
wrapt = "<=1.14.1" # latest version available on Snowflake Anaconda
|
42
|
+
opentelemetry-api = "<=1.23.0" # latest version available on Snowflake Anaconda
|
43
|
+
snowflake-telemetry-python = "<=0.5.0" # latest version available on Snowflake Anaconda
|
44
|
+
protobuf = "<=4.25.3" # latest version available on Snowflake Anaconda
|
41
45
|
|
42
46
|
[tool.poetry.dev-dependencies]
|
43
47
|
pytest = "^6.2.4"
|
@@ -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-0.7.0a185 → omnata_plugin_runtime-0.8.0}/src/omnata_plugin_runtime/logging.py
RENAMED
@@ -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
|
File without changes
|
File without changes
|
File without changes
|
{omnata_plugin_runtime-0.7.0a185 → omnata_plugin_runtime-0.8.0}/src/omnata_plugin_runtime/api.py
RENAMED
File without changes
|
{omnata_plugin_runtime-0.7.0a185 → omnata_plugin_runtime-0.8.0}/src/omnata_plugin_runtime/forms.py
RENAMED
File without changes
|