omnata-plugin-runtime 0.3.20a63__py3-none-any.whl → 0.3.21a64__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.
@@ -443,10 +443,9 @@ class StoredStreamConfiguration(SubscriptableBaseModel):
443
443
  None,
444
444
  description="The field to use as a cursor",
445
445
  )
446
- # Composite primary keys is not really a thing in SaaS applications
447
- primary_key_field: Optional[str] = Field(
446
+ primary_key_field: Optional[Union[str,List[str]]] = Field(
448
447
  None,
449
- description="The field that will be used as primary key.",
448
+ description="The field(s) that will be used as primary key.",
450
449
  )
451
450
  storage_behaviour: InboundStorageBehaviour
452
451
  stream: StreamConfiguration
@@ -465,9 +464,17 @@ class StreamConfiguration(SubscriptableBaseModel):
465
464
  None,
466
465
  description="If true, the plugin controls the cursor field",
467
466
  )
468
- source_defined_primary_key: Optional[str] = Field(
467
+ source_defined_primary_key: Optional[Union[str,List[str]]] = Field(
469
468
  None,
470
- description="If true, the plugin controls the primary key",
469
+ description="If defined, the plugin controls the primary key field",
470
+ )
471
+ # For SaaS applications, typically the primary key is a single field, known to the plugin.
472
+ # For databases, the primary key can be composite, and the plugin may not know what it is if there is no key defined explicitly on the table.
473
+ # this flag was added to support this scenario. If it is to True, then source_defined_primary_key will be a list of fields.
474
+ # When this occurs, the plugin runtime will build the APP_IDENTIFIER from the fields in the order they are listed, by concatenating them.
475
+ primary_key_can_be_composite: Optional[bool] = Field(
476
+ False,
477
+ description="If true, the primary key can be composite",
471
478
  )
472
479
  default_cursor_field: Optional[str] = Field(
473
480
  None,
@@ -19,6 +19,7 @@ import http
19
19
  import json
20
20
  import queue
21
21
  import threading
22
+ import time
22
23
  from abc import ABC, abstractmethod
23
24
  from decimal import Decimal
24
25
  from functools import partial, wraps, reduce
@@ -1200,7 +1201,8 @@ class InboundSyncRequest(SyncRequest):
1200
1201
  raise ValueError(
1201
1202
  f"Primary key field '{primary_key_field}' was not present in all records for stream {stream_name}"
1202
1203
  )
1203
-
1204
+ # TODO: handle the scenario where the primary key field is a list, and concatenate the values to form the APP_IDENTIFIER
1205
+ # Currently this is only implemented in the java runtime, since it's typically databases that use composite keys
1204
1206
  results_df["APP_IDENTIFIER"] = results_df["RECORD_DATA"].apply(lambda x: get_nested_value(dict(x),primary_key_field))
1205
1207
  # ensure APP_IDENTIFIER is a string
1206
1208
  results_df["APP_IDENTIFIER"] = results_df["APP_IDENTIFIER"].apply(str)
@@ -1836,9 +1838,13 @@ def managed_outbound_processing(concurrency: int, batch_size: int):
1836
1838
  task.start()
1837
1839
 
1838
1840
  # wait for workers to finish
1839
- for task in tasks:
1840
- task.join()
1841
- logger.info(f"Task {task.name} joined")
1841
+ while tasks:
1842
+ for task in tasks[:]: # shallow copy so we can remove items from the list while iterating
1843
+ if not task.is_alive():
1844
+ task.join() # Ensure the thread is fully finished
1845
+ tasks.remove(task)
1846
+ logger.info(f"Thread {task.name} has completed processing")
1847
+ time.sleep(1) # Avoid busy waiting
1842
1848
  logger.info("All workers completed processing")
1843
1849
 
1844
1850
  # it's possible that some records weren't applied, since they are processed asynchronously on a timer
@@ -1892,11 +1898,14 @@ def __managed_inbound_processing_worker(
1892
1898
  logger.info(f"stream returned from queue: {stream}")
1893
1899
  # restore the first argument, was originally the dataframe/generator but now it's the appropriately sized dataframe
1894
1900
  try:
1901
+ logger.info(f"worker {worker_index} processing stream {stream.stream_name}, invoking plugin class method {method.__name__}")
1895
1902
  method(plugin_class_obj, *(stream, *method_args), **method_kwargs)
1903
+ logger.info(f"worker {worker_index} completed processing stream {stream.stream_name}, marking as complete")
1896
1904
  plugin_class_obj._sync_request.mark_stream_complete(stream.stream_name)
1897
1905
  except InterruptedWhileWaitingException:
1898
1906
  # If an inbound run is cancelled while waiting for rate limiting, this should mean that
1899
1907
  # the cancellation is handled elsewhere, so we don't need to do anything special here other than stop waiting
1908
+ logger.info(f"worker {worker_index} interrupted while waiting, exiting")
1900
1909
  return
1901
1910
  except Exception as e:
1902
1911
  # logging this to the omnata_plugin logger in this way,
@@ -1967,13 +1976,14 @@ def managed_inbound_processing(concurrency: int):
1967
1976
  for stream in streams_list:
1968
1977
  streams_queue.put(stream)
1969
1978
 
1970
- tasks = []
1979
+ tasks:List[threading.Thread] = []
1971
1980
  logger.info(f"Creating {concurrency_to_use} worker(s) for retrieving records")
1972
1981
 
1973
1982
  for i in range(concurrency_to_use):
1974
1983
  # the dataframe/generator was put on the queue, so we remove it from the method args
1975
1984
  task = threading.Thread(
1976
1985
  target=__managed_inbound_processing_worker,
1986
+ name=f"managed_inbound_processing_worker_{i}",
1977
1987
  args=(
1978
1988
  self,
1979
1989
  method,
@@ -1988,9 +1998,13 @@ def managed_inbound_processing(concurrency: int):
1988
1998
  task.start()
1989
1999
 
1990
2000
  # wait for workers to finish
1991
- for task in tasks:
1992
- task.join()
1993
- logger.info("Task joined")
2001
+ while tasks:
2002
+ for task in tasks[:]: # shallow copy so we can remove items from the list while iterating
2003
+ if not task.is_alive():
2004
+ task.join() # Ensure the thread is fully finished
2005
+ tasks.remove(task)
2006
+ logger.info(f"Thread {task.name} has completed processing")
2007
+ time.sleep(1) # Avoid busy waiting
1994
2008
  logger.info("All workers completed processing")
1995
2009
 
1996
2010
  # it's possible that some records weren't applied, since they are processed asynchronously on a timer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: omnata-plugin-runtime
3
- Version: 0.3.20a63
3
+ Version: 0.3.21a64
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
@@ -1,12 +1,12 @@
1
1
  omnata_plugin_runtime/__init__.py,sha256=w63LVME5nY-hQ4BBzfacy9kvTunwqHGs8iiSPGAX2ns,1214
2
2
  omnata_plugin_runtime/api.py,sha256=_N5ok5LN7GDO4J9n3yduXp3tpjmhpySY__U2baiygrs,6217
3
- omnata_plugin_runtime/configuration.py,sha256=AY_hHBd283VHx_rU9TzUol5VTzA6PHxxTltYWV7clCg,35113
3
+ omnata_plugin_runtime/configuration.py,sha256=7cMekoY8CeZAJHpASU6tCMidF55Hzfr7CD74jtebqIY,35742
4
4
  omnata_plugin_runtime/forms.py,sha256=pw_aKVsXSz47EP8PFBI3VDwdSN5IjvZxp8JTjO1V130,18421
5
5
  omnata_plugin_runtime/logging.py,sha256=bn7eKoNWvtuyTk7RTwBS9UARMtqkiICtgMtzq3KA2V0,3272
6
- omnata_plugin_runtime/omnata_plugin.py,sha256=9t17Ug94v0IdXzUNx27v_U0itUVQV3k0Tqqx5b-djwE,99609
6
+ omnata_plugin_runtime/omnata_plugin.py,sha256=ArEtB7LvbLBq9u39Y_2ivg24Hi1Xe-kcnzj5UG4mLP0,100971
7
7
  omnata_plugin_runtime/plugin_entrypoints.py,sha256=s-SrUnXaS6FaBq1igiJhcCB3Md_a6op-dp_g1H_55QU,27736
8
8
  omnata_plugin_runtime/rate_limiting.py,sha256=se6MftQI5NrVHaLb1hByPCgAESPQhkAgIG7KIU1clDU,16562
9
- omnata_plugin_runtime-0.3.20a63.dist-info/LICENSE,sha256=IMF9i4xIpgCADf0U-V1cuf9HBmqWQd3qtI3FSuyW4zE,26526
10
- omnata_plugin_runtime-0.3.20a63.dist-info/METADATA,sha256=oCOEIy9kdmK2QKAfWS3MtmgRiL-rPEiVMj3a3aNzh-E,1604
11
- omnata_plugin_runtime-0.3.20a63.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
12
- omnata_plugin_runtime-0.3.20a63.dist-info/RECORD,,
9
+ omnata_plugin_runtime-0.3.21a64.dist-info/LICENSE,sha256=IMF9i4xIpgCADf0U-V1cuf9HBmqWQd3qtI3FSuyW4zE,26526
10
+ omnata_plugin_runtime-0.3.21a64.dist-info/METADATA,sha256=AN68hzVHcChygxFH76s7iZtvROwge5NqHDNttfkRfko,1604
11
+ omnata_plugin_runtime-0.3.21a64.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
12
+ omnata_plugin_runtime-0.3.21a64.dist-info/RECORD,,