bb-integrations-library 3.0.11__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.
Files changed (217) hide show
  1. bb_integrations_lib/__init__.py +0 -0
  2. bb_integrations_lib/converters/__init__.py +0 -0
  3. bb_integrations_lib/gravitate/__init__.py +0 -0
  4. bb_integrations_lib/gravitate/base_api.py +20 -0
  5. bb_integrations_lib/gravitate/model.py +29 -0
  6. bb_integrations_lib/gravitate/pe_api.py +122 -0
  7. bb_integrations_lib/gravitate/rita_api.py +552 -0
  8. bb_integrations_lib/gravitate/sd_api.py +572 -0
  9. bb_integrations_lib/gravitate/testing/TTE/sd/models.py +1398 -0
  10. bb_integrations_lib/gravitate/testing/TTE/sd/tests/test_models.py +2987 -0
  11. bb_integrations_lib/gravitate/testing/__init__.py +0 -0
  12. bb_integrations_lib/gravitate/testing/builder.py +55 -0
  13. bb_integrations_lib/gravitate/testing/openapi.py +70 -0
  14. bb_integrations_lib/gravitate/testing/util.py +274 -0
  15. bb_integrations_lib/mappers/__init__.py +0 -0
  16. bb_integrations_lib/mappers/prices/__init__.py +0 -0
  17. bb_integrations_lib/mappers/prices/model.py +106 -0
  18. bb_integrations_lib/mappers/prices/price_mapper.py +127 -0
  19. bb_integrations_lib/mappers/prices/protocol.py +20 -0
  20. bb_integrations_lib/mappers/prices/util.py +61 -0
  21. bb_integrations_lib/mappers/rita_mapper.py +523 -0
  22. bb_integrations_lib/models/__init__.py +0 -0
  23. bb_integrations_lib/models/dtn_supplier_invoice.py +487 -0
  24. bb_integrations_lib/models/enums.py +28 -0
  25. bb_integrations_lib/models/pipeline_structs.py +76 -0
  26. bb_integrations_lib/models/probe/probe_event.py +20 -0
  27. bb_integrations_lib/models/probe/request_data.py +431 -0
  28. bb_integrations_lib/models/probe/resume_token.py +7 -0
  29. bb_integrations_lib/models/rita/audit.py +113 -0
  30. bb_integrations_lib/models/rita/auth.py +30 -0
  31. bb_integrations_lib/models/rita/bucket.py +17 -0
  32. bb_integrations_lib/models/rita/config.py +188 -0
  33. bb_integrations_lib/models/rita/constants.py +19 -0
  34. bb_integrations_lib/models/rita/crossroads_entities.py +293 -0
  35. bb_integrations_lib/models/rita/crossroads_mapping.py +428 -0
  36. bb_integrations_lib/models/rita/crossroads_monitoring.py +78 -0
  37. bb_integrations_lib/models/rita/crossroads_network.py +41 -0
  38. bb_integrations_lib/models/rita/crossroads_rules.py +80 -0
  39. bb_integrations_lib/models/rita/email.py +39 -0
  40. bb_integrations_lib/models/rita/issue.py +63 -0
  41. bb_integrations_lib/models/rita/mapping.py +227 -0
  42. bb_integrations_lib/models/rita/probe.py +58 -0
  43. bb_integrations_lib/models/rita/reference_data.py +110 -0
  44. bb_integrations_lib/models/rita/source_system.py +9 -0
  45. bb_integrations_lib/models/rita/workers.py +76 -0
  46. bb_integrations_lib/models/sd/bols_and_drops.py +241 -0
  47. bb_integrations_lib/models/sd/get_order.py +301 -0
  48. bb_integrations_lib/models/sd/orders.py +18 -0
  49. bb_integrations_lib/models/sd_api.py +115 -0
  50. bb_integrations_lib/pipelines/__init__.py +0 -0
  51. bb_integrations_lib/pipelines/parsers/__init__.py +0 -0
  52. bb_integrations_lib/pipelines/parsers/distribution_report/__init__.py +0 -0
  53. bb_integrations_lib/pipelines/parsers/distribution_report/order_by_site_product_parser.py +50 -0
  54. bb_integrations_lib/pipelines/parsers/distribution_report/tank_configs_parser.py +47 -0
  55. bb_integrations_lib/pipelines/parsers/dtn/__init__.py +0 -0
  56. bb_integrations_lib/pipelines/parsers/dtn/dtn_price_parser.py +102 -0
  57. bb_integrations_lib/pipelines/parsers/dtn/model.py +79 -0
  58. bb_integrations_lib/pipelines/parsers/price_engine/__init__.py +0 -0
  59. bb_integrations_lib/pipelines/parsers/price_engine/parse_accessorials_prices_parser.py +67 -0
  60. bb_integrations_lib/pipelines/parsers/price_engine/price_file_upload/__init__.py +0 -0
  61. bb_integrations_lib/pipelines/parsers/price_engine/price_file_upload/price_merge_parser.py +111 -0
  62. bb_integrations_lib/pipelines/parsers/price_engine/price_file_upload/price_sync_parser.py +107 -0
  63. bb_integrations_lib/pipelines/parsers/price_engine/price_file_upload/shared.py +81 -0
  64. bb_integrations_lib/pipelines/parsers/tank_reading_parser.py +155 -0
  65. bb_integrations_lib/pipelines/parsers/tank_sales_parser.py +144 -0
  66. bb_integrations_lib/pipelines/shared/__init__.py +0 -0
  67. bb_integrations_lib/pipelines/shared/allocation_matching.py +227 -0
  68. bb_integrations_lib/pipelines/shared/bol_allocation.py +2793 -0
  69. bb_integrations_lib/pipelines/steps/__init__.py +0 -0
  70. bb_integrations_lib/pipelines/steps/create_accessorials_step.py +80 -0
  71. bb_integrations_lib/pipelines/steps/distribution_report/__init__.py +0 -0
  72. bb_integrations_lib/pipelines/steps/distribution_report/distribution_report_datafram_to_raw_data.py +33 -0
  73. bb_integrations_lib/pipelines/steps/distribution_report/get_model_history_step.py +50 -0
  74. bb_integrations_lib/pipelines/steps/distribution_report/get_order_by_site_product_step.py +62 -0
  75. bb_integrations_lib/pipelines/steps/distribution_report/get_tank_configs_step.py +40 -0
  76. bb_integrations_lib/pipelines/steps/distribution_report/join_distribution_order_dos_step.py +85 -0
  77. bb_integrations_lib/pipelines/steps/distribution_report/upload_distribution_report_datafram_to_big_query.py +47 -0
  78. bb_integrations_lib/pipelines/steps/echo_step.py +14 -0
  79. bb_integrations_lib/pipelines/steps/export_dataframe_to_rawdata_step.py +28 -0
  80. bb_integrations_lib/pipelines/steps/exporting/__init__.py +0 -0
  81. bb_integrations_lib/pipelines/steps/exporting/bbd_export_payroll_file_step.py +107 -0
  82. bb_integrations_lib/pipelines/steps/exporting/bbd_export_readings_step.py +236 -0
  83. bb_integrations_lib/pipelines/steps/exporting/cargas_wholesale_bundle_upload_step.py +33 -0
  84. bb_integrations_lib/pipelines/steps/exporting/dataframe_flat_file_export.py +29 -0
  85. bb_integrations_lib/pipelines/steps/exporting/gcs_bucket_export_file_step.py +34 -0
  86. bb_integrations_lib/pipelines/steps/exporting/keyvu_export_step.py +356 -0
  87. bb_integrations_lib/pipelines/steps/exporting/pe_price_export_step.py +238 -0
  88. bb_integrations_lib/pipelines/steps/exporting/platform_science_order_sync_step.py +500 -0
  89. bb_integrations_lib/pipelines/steps/exporting/save_rawdata_to_disk.py +15 -0
  90. bb_integrations_lib/pipelines/steps/exporting/sftp_export_file_step.py +60 -0
  91. bb_integrations_lib/pipelines/steps/exporting/sftp_export_many_files_step.py +23 -0
  92. bb_integrations_lib/pipelines/steps/exporting/update_exported_orders_table_step.py +64 -0
  93. bb_integrations_lib/pipelines/steps/filter_step.py +22 -0
  94. bb_integrations_lib/pipelines/steps/get_latest_sync_date.py +34 -0
  95. bb_integrations_lib/pipelines/steps/importing/bbd_import_payroll_step.py +30 -0
  96. bb_integrations_lib/pipelines/steps/importing/get_order_numbers_to_export_step.py +138 -0
  97. bb_integrations_lib/pipelines/steps/importing/load_file_to_dataframe_step.py +46 -0
  98. bb_integrations_lib/pipelines/steps/importing/load_imap_attachment_step.py +172 -0
  99. bb_integrations_lib/pipelines/steps/importing/pe_bulk_sync_price_structure_step.py +68 -0
  100. bb_integrations_lib/pipelines/steps/importing/pe_price_merge_step.py +86 -0
  101. bb_integrations_lib/pipelines/steps/importing/sftp_file_config_step.py +124 -0
  102. bb_integrations_lib/pipelines/steps/importing/test_exact_file_match.py +57 -0
  103. bb_integrations_lib/pipelines/steps/null_step.py +15 -0
  104. bb_integrations_lib/pipelines/steps/pe_integration_job_step.py +32 -0
  105. bb_integrations_lib/pipelines/steps/processing/__init__.py +0 -0
  106. bb_integrations_lib/pipelines/steps/processing/archive_gcs_step.py +76 -0
  107. bb_integrations_lib/pipelines/steps/processing/archive_sftp_step.py +48 -0
  108. bb_integrations_lib/pipelines/steps/processing/bbd_format_tank_readings_step.py +492 -0
  109. bb_integrations_lib/pipelines/steps/processing/bbd_upload_prices_step.py +54 -0
  110. bb_integrations_lib/pipelines/steps/processing/bbd_upload_tank_sales_step.py +124 -0
  111. bb_integrations_lib/pipelines/steps/processing/bbd_upload_tankreading_step.py +80 -0
  112. bb_integrations_lib/pipelines/steps/processing/convert_bbd_order_to_cargas_step.py +226 -0
  113. bb_integrations_lib/pipelines/steps/processing/delete_sftp_step.py +33 -0
  114. bb_integrations_lib/pipelines/steps/processing/dtn/__init__.py +2 -0
  115. bb_integrations_lib/pipelines/steps/processing/dtn/convert_dtn_invoice_to_sd_model.py +145 -0
  116. bb_integrations_lib/pipelines/steps/processing/dtn/parse_dtn_invoice_step.py +38 -0
  117. bb_integrations_lib/pipelines/steps/processing/file_config_parser_step.py +720 -0
  118. bb_integrations_lib/pipelines/steps/processing/file_config_parser_step_v2.py +418 -0
  119. bb_integrations_lib/pipelines/steps/processing/get_sd_price_price_request.py +105 -0
  120. bb_integrations_lib/pipelines/steps/processing/keyvu_upload_deliveryplan_step.py +39 -0
  121. bb_integrations_lib/pipelines/steps/processing/mark_orders_exported_in_bbd_step.py +185 -0
  122. bb_integrations_lib/pipelines/steps/processing/pe_price_rows_processing_step.py +174 -0
  123. bb_integrations_lib/pipelines/steps/processing/send_process_report_step.py +47 -0
  124. bb_integrations_lib/pipelines/steps/processing/sftp_renamer_step.py +61 -0
  125. bb_integrations_lib/pipelines/steps/processing/tank_reading_touchup_steps.py +75 -0
  126. bb_integrations_lib/pipelines/steps/processing/upload_supplier_invoice_step.py +16 -0
  127. bb_integrations_lib/pipelines/steps/send_attached_in_rita_email_step.py +44 -0
  128. bb_integrations_lib/pipelines/steps/send_rita_email_step.py +34 -0
  129. bb_integrations_lib/pipelines/steps/sleep_step.py +24 -0
  130. bb_integrations_lib/pipelines/wrappers/__init__.py +0 -0
  131. bb_integrations_lib/pipelines/wrappers/accessorials_transformation.py +104 -0
  132. bb_integrations_lib/pipelines/wrappers/distribution_report.py +191 -0
  133. bb_integrations_lib/pipelines/wrappers/export_tank_readings.py +237 -0
  134. bb_integrations_lib/pipelines/wrappers/import_tank_readings.py +192 -0
  135. bb_integrations_lib/pipelines/wrappers/wrapper.py +81 -0
  136. bb_integrations_lib/protocols/__init__.py +0 -0
  137. bb_integrations_lib/protocols/flat_file.py +210 -0
  138. bb_integrations_lib/protocols/gravitate_client.py +104 -0
  139. bb_integrations_lib/protocols/pipelines.py +697 -0
  140. bb_integrations_lib/provider/__init__.py +0 -0
  141. bb_integrations_lib/provider/api/__init__.py +0 -0
  142. bb_integrations_lib/provider/api/cargas/__init__.py +0 -0
  143. bb_integrations_lib/provider/api/cargas/client.py +43 -0
  144. bb_integrations_lib/provider/api/cargas/model.py +49 -0
  145. bb_integrations_lib/provider/api/cargas/protocol.py +23 -0
  146. bb_integrations_lib/provider/api/dtn/__init__.py +0 -0
  147. bb_integrations_lib/provider/api/dtn/client.py +128 -0
  148. bb_integrations_lib/provider/api/dtn/protocol.py +9 -0
  149. bb_integrations_lib/provider/api/keyvu/__init__.py +0 -0
  150. bb_integrations_lib/provider/api/keyvu/client.py +30 -0
  151. bb_integrations_lib/provider/api/keyvu/model.py +149 -0
  152. bb_integrations_lib/provider/api/macropoint/__init__.py +0 -0
  153. bb_integrations_lib/provider/api/macropoint/client.py +28 -0
  154. bb_integrations_lib/provider/api/macropoint/model.py +40 -0
  155. bb_integrations_lib/provider/api/pc_miler/__init__.py +0 -0
  156. bb_integrations_lib/provider/api/pc_miler/client.py +130 -0
  157. bb_integrations_lib/provider/api/pc_miler/model.py +6 -0
  158. bb_integrations_lib/provider/api/pc_miler/web_services_apis.py +131 -0
  159. bb_integrations_lib/provider/api/platform_science/__init__.py +0 -0
  160. bb_integrations_lib/provider/api/platform_science/client.py +147 -0
  161. bb_integrations_lib/provider/api/platform_science/model.py +82 -0
  162. bb_integrations_lib/provider/api/quicktrip/__init__.py +0 -0
  163. bb_integrations_lib/provider/api/quicktrip/client.py +52 -0
  164. bb_integrations_lib/provider/api/telapoint/__init__.py +0 -0
  165. bb_integrations_lib/provider/api/telapoint/client.py +68 -0
  166. bb_integrations_lib/provider/api/telapoint/model.py +178 -0
  167. bb_integrations_lib/provider/api/warren_rogers/__init__.py +0 -0
  168. bb_integrations_lib/provider/api/warren_rogers/client.py +207 -0
  169. bb_integrations_lib/provider/aws/__init__.py +0 -0
  170. bb_integrations_lib/provider/aws/s3/__init__.py +0 -0
  171. bb_integrations_lib/provider/aws/s3/client.py +126 -0
  172. bb_integrations_lib/provider/ftp/__init__.py +0 -0
  173. bb_integrations_lib/provider/ftp/client.py +140 -0
  174. bb_integrations_lib/provider/ftp/interface.py +273 -0
  175. bb_integrations_lib/provider/ftp/model.py +76 -0
  176. bb_integrations_lib/provider/imap/__init__.py +0 -0
  177. bb_integrations_lib/provider/imap/client.py +228 -0
  178. bb_integrations_lib/provider/imap/model.py +3 -0
  179. bb_integrations_lib/provider/sqlserver/__init__.py +0 -0
  180. bb_integrations_lib/provider/sqlserver/client.py +106 -0
  181. bb_integrations_lib/secrets/__init__.py +4 -0
  182. bb_integrations_lib/secrets/adapters.py +98 -0
  183. bb_integrations_lib/secrets/credential_models.py +222 -0
  184. bb_integrations_lib/secrets/factory.py +85 -0
  185. bb_integrations_lib/secrets/providers.py +160 -0
  186. bb_integrations_lib/shared/__init__.py +0 -0
  187. bb_integrations_lib/shared/exceptions.py +25 -0
  188. bb_integrations_lib/shared/model.py +1039 -0
  189. bb_integrations_lib/shared/shared_enums.py +510 -0
  190. bb_integrations_lib/storage/README.md +236 -0
  191. bb_integrations_lib/storage/__init__.py +0 -0
  192. bb_integrations_lib/storage/aws/__init__.py +0 -0
  193. bb_integrations_lib/storage/aws/s3.py +8 -0
  194. bb_integrations_lib/storage/defaults.py +72 -0
  195. bb_integrations_lib/storage/gcs/__init__.py +0 -0
  196. bb_integrations_lib/storage/gcs/client.py +8 -0
  197. bb_integrations_lib/storage/gcsmanager/__init__.py +0 -0
  198. bb_integrations_lib/storage/gcsmanager/client.py +8 -0
  199. bb_integrations_lib/storage/setup.py +29 -0
  200. bb_integrations_lib/util/__init__.py +0 -0
  201. bb_integrations_lib/util/cache/__init__.py +0 -0
  202. bb_integrations_lib/util/cache/custom_ttl_cache.py +75 -0
  203. bb_integrations_lib/util/cache/protocol.py +9 -0
  204. bb_integrations_lib/util/config/__init__.py +0 -0
  205. bb_integrations_lib/util/config/manager.py +391 -0
  206. bb_integrations_lib/util/config/model.py +41 -0
  207. bb_integrations_lib/util/exception_logger/__init__.py +0 -0
  208. bb_integrations_lib/util/exception_logger/exception_logger.py +146 -0
  209. bb_integrations_lib/util/exception_logger/test.py +114 -0
  210. bb_integrations_lib/util/utils.py +364 -0
  211. bb_integrations_lib/workers/__init__.py +0 -0
  212. bb_integrations_lib/workers/groups.py +13 -0
  213. bb_integrations_lib/workers/rpc_worker.py +50 -0
  214. bb_integrations_lib/workers/topics.py +20 -0
  215. bb_integrations_library-3.0.11.dist-info/METADATA +59 -0
  216. bb_integrations_library-3.0.11.dist-info/RECORD +217 -0
  217. bb_integrations_library-3.0.11.dist-info/WHEEL +4 -0
@@ -0,0 +1,61 @@
1
+ from typing import Dict, Union, Protocol, Type, runtime_checkable
2
+ from bb_integrations_lib.gravitate.rita_api import GravitateRitaAPI, RitaBackendAPI
3
+ from bb_integrations_lib.mappers.prices.model import IntegrationType, PricingIntegrationConfig
4
+ from bb_integrations_lib.mappers.prices.protocol import ExternalPriceMapperIntegration
5
+ from bb_integrations_lib.provider.sqlserver.client import SQLServerClient
6
+ from bb_integrations_lib.util.config.manager import GlobalConfigManager
7
+ from bb_integrations_lib.util.config.model import GlobalConfig
8
+
9
+
10
+ @runtime_checkable
11
+ class PricingIntegrationGetterProtocol(Protocol):
12
+ def get_integration_mapping_client_by_entity(self, entity_key: str) -> Union[
13
+ ExternalPriceMapperIntegration, RitaBackendAPI]:
14
+ """Gets integration by entity key."""
15
+
16
+ def init_class(self, integration_type: IntegrationType) -> Union[
17
+ ExternalPriceMapperIntegration, RitaBackendAPI]:
18
+ """Initializes integration class given a key"""
19
+
20
+
21
+ class PricingIntegrationGetter:
22
+ def __init__(
23
+ self,
24
+ config: PricingIntegrationConfig
25
+ ):
26
+ self.config: PricingIntegrationConfig = config
27
+ self.entity_config = config.entity_config
28
+ self.config_manager = GlobalConfigManager()
29
+ self.client_secret: GlobalConfig = self.config_manager.get_environment(config.environment)
30
+
31
+ def get_integration_mapping_client_by_entity(self, entity_key: str) -> Union[
32
+ ExternalPriceMapperIntegration, RitaBackendAPI]:
33
+ try:
34
+ integration_type = self.entity_config.get(entity_key).mapping_integration.type
35
+ if integration_type is None:
36
+ raise KeyError(f"No integration type found for entity key: {entity_key}")
37
+ client_class = self.init_class(integration_type)
38
+ if client_class is None:
39
+ raise KeyError(f"No client class found for integration type: {integration_type}")
40
+ return client_class
41
+ except KeyError as e:
42
+ raise NotImplementedError(str(e))
43
+
44
+ def init_class(self, integration_type: IntegrationType) -> Union[
45
+ ExternalPriceMapperIntegration, RitaBackendAPI]:
46
+ if integration_type == IntegrationType.sql:
47
+ return SQLServerClient(
48
+ server=self.client_secret.extra_data.get("server"),
49
+ database=self.client_secret.extra_data.get("database"),
50
+ username=self.client_secret.extra_data.get("username"),
51
+ password=self.client_secret.extra_data.get("password"),
52
+ )
53
+ if integration_type == IntegrationType.rita:
54
+ return GravitateRitaAPI(
55
+ client_id=self.client_secret.prod.rita.client_id,
56
+ client_secret=self.client_secret.prod.rita.client_secret,
57
+ tenant=self.config.environment
58
+ )
59
+ else:
60
+ raise NotImplementedError(f"Integration type {integration_type} is not supported")
61
+
@@ -0,0 +1,523 @@
1
+ from abc import abstractmethod, ABC
2
+ from collections import defaultdict
3
+ from enum import Enum
4
+ from typing import Optional, Union
5
+
6
+ from bb_integrations_lib.gravitate.rita_api import GravitateRitaAPI
7
+ from bb_integrations_lib.models.rita.mapping import Map, MappingType, Children, CompositeMapKey
8
+ from bb_integrations_lib.secrets import RITACredential
9
+ from bb_integrations_lib.util.cache.custom_ttl_cache import CustomAsyncTTLCache
10
+
11
+
12
+ class MappingsNotLoadedException(Exception):
13
+ pass
14
+
15
+
16
+ class FastMapDirection(Enum):
17
+ source_to_grav = 1
18
+ grav_to_source = 2
19
+
20
+
21
+ class FastMapKey:
22
+ """
23
+ A hashable key that can be used to identify mappings quickly if enough properties are known.
24
+ Supports both simple string IDs and composite keys for entity identity (from_id).
25
+
26
+ Note: parent_id is always a simple string. Composite keys are for entity identity resolution,
27
+ not for hierarchical relationships. Children mappings only support string identifiers.
28
+ """
29
+ from_id: Union[str, CompositeMapKey]
30
+ type: Optional[MappingType] = None
31
+ parent_id: Optional[str] = None
32
+ direction: FastMapDirection
33
+
34
+ def __init__(
35
+ self,
36
+ from_id: Union[str, CompositeMapKey, dict],
37
+ type: MappingType,
38
+ parent_id: Optional[str],
39
+ direction: FastMapDirection
40
+ ):
41
+ # Normalize dict inputs to CompositeMapKey for entity identity
42
+ if isinstance(from_id, dict):
43
+ self.from_id = CompositeMapKey(key=from_id)
44
+ else:
45
+ self.from_id = from_id
46
+ self.type = type
47
+ # parent_id is always a simple string (hierarchy uses string IDs)
48
+ self.parent_id = parent_id
49
+ self.direction = direction
50
+
51
+ # Cache normalized values for hash/equality
52
+ self._from_id_normalized = self._normalize_from_id(self.from_id)
53
+ self._parent_id_normalized = self.parent_id or ""
54
+
55
+ @staticmethod
56
+ def _normalize_from_id(value: Union[str, CompositeMapKey]) -> str:
57
+ """Convert from_id to hashable string representation."""
58
+ if isinstance(value, CompositeMapKey):
59
+ return value.to_cache_key()
60
+ return value
61
+
62
+ def __hash__(self):
63
+ return hash((
64
+ self._from_id_normalized,
65
+ self.type,
66
+ self._parent_id_normalized,
67
+ self.direction.value
68
+ ))
69
+
70
+ def __eq__(self, other):
71
+ if not isinstance(other, FastMapKey):
72
+ return False
73
+ return (
74
+ self._from_id_normalized == other._from_id_normalized and
75
+ self.type == other.type and
76
+ self._parent_id_normalized == other._parent_id_normalized and
77
+ self.direction == other.direction
78
+ )
79
+
80
+ def __repr__(self):
81
+ type_name = self.type.name if self.type else None
82
+ parent_id_str = self._parent_id_normalized or None
83
+ return f"FastMap<from_id={self._from_id_normalized}, type_name={type_name}, parent_id={parent_id_str}, direction={self.direction.name}>"
84
+
85
+
86
+ class MappingProvider(ABC):
87
+ """An abstract base class for implementing synchronous mapping providers for the Mapper class."""
88
+
89
+ def __init__(self):
90
+ pass
91
+
92
+ @abstractmethod
93
+ def get_mappings_by_source_system(self, source_system: str) -> list[Map]:
94
+ pass
95
+
96
+
97
+ class AsyncMappingProvider(ABC):
98
+ """An abstract base class for implementing asynchronous mapping providers for the Mapper class."""
99
+
100
+ def __init__(self):
101
+ pass
102
+
103
+ @abstractmethod
104
+ async def get_mappings_by_source_system(self, source_system: str) -> list[Map]:
105
+ pass
106
+
107
+
108
+ class MockMappingProvider(MappingProvider):
109
+ """A mapping provider that uses a constant list of mappings, intended for testing."""
110
+
111
+ def __init__(self, mappings: list[Map]):
112
+ super().__init__()
113
+ self.mappings = mappings
114
+
115
+ def get_mappings_by_source_system(self, source_system: str) -> list[Map]:
116
+ """Returns mappings originally passed to init, filtered by source system."""
117
+ return list(filter(lambda m: m.source_system == source_system, self.mappings))
118
+
119
+
120
+ class MockAsyncMappingProvider(AsyncMappingProvider):
121
+ """A mapping provider that uses a constant list of mappings, intended for testing."""
122
+
123
+ def __init__(self, mappings: list[Map]):
124
+ super().__init__()
125
+ self.mappings = mappings
126
+
127
+ async def get_mappings_by_source_system(self, source_system: str) -> list[Map]:
128
+ """Returns mappings originally passed to init, filtered by source system."""
129
+ return list(filter(lambda m: m.source_system == source_system, self.mappings))
130
+
131
+
132
+ class RitaAPIMappingProvider(AsyncMappingProvider):
133
+ """Accesses the RITA API to retrieve mappings, providing them to a Mapper."""
134
+
135
+ def __init__(self, rita_client: GravitateRitaAPI):
136
+ super().__init__()
137
+ self.rita_client = rita_client
138
+
139
+ async def from_credential(self, credential: RITACredential):
140
+ self.rita_client = GravitateRitaAPI.from_credential(credential)
141
+
142
+ async def get_mappings_by_source_system(self, source_system: str) -> list[Map]:
143
+ return await self.rita_client.get_mappings_by_source_system(source_system)
144
+
145
+
146
+ class RitaAPICachedMappingProvider(RitaAPIMappingProvider):
147
+ def __init__(
148
+ self,
149
+ rita_client: GravitateRitaAPI,
150
+ ttl_seconds: int = 120
151
+ ):
152
+ super().__init__(rita_client)
153
+ self.ttl_seconds = ttl_seconds
154
+ self.cache: CustomAsyncTTLCache = CustomAsyncTTLCache(verbose=False)
155
+
156
+ async def get_mappings_by_source_system(self, source_system: str) -> list[Map]:
157
+ return await self.cache.get_or_set(
158
+ source_system, self.ttl_seconds, super().get_mappings_by_source_system, source_system
159
+ )
160
+
161
+
162
+ class RitaMapperCore(ABC):
163
+ """
164
+ Provides the core logic of mapping lookups. Entirely synchronous; the concrete implementations can use these
165
+ methods directly, and should generally only implement loading mappings.
166
+ """
167
+
168
+ def __init__(self):
169
+ self.loaded = False
170
+ self.fast_maps = defaultdict(list)
171
+
172
+ def _raise_if_not_loaded(self):
173
+ if not self.loaded:
174
+ raise MappingsNotLoadedException("Mappings used before getting loaded")
175
+
176
+ def _load_mappings(self, mappings: list[Map]):
177
+ """
178
+ Load the mappings from mappings into the mapper's internal data structures, making the get_ calls available
179
+ for use. Supports both simple string IDs and composite keys for entity identity.
180
+
181
+ Note: parent_id in FastMapKey is always a string. If a parent Map has a composite source_id,
182
+ it's converted to string for hierarchical lookups.
183
+ """
184
+
185
+ def _get_identifier(map_obj: Map | Children, direction: FastMapDirection):
186
+ """Extract the appropriate identifier based on a map direction."""
187
+ if direction == FastMapDirection.grav_to_source:
188
+ return map_obj.gravitate_id
189
+ return map_obj.source_id
190
+
191
+ def _get_parent_id_str(parent: Map | None, direction: FastMapDirection) -> str | None:
192
+ """Get parent identifier as string (hierarchy always uses string IDs)."""
193
+ if parent is None:
194
+ return None
195
+ parent_id = _get_identifier(parent, direction)
196
+ # Convert composite keys to string for parent_id (hierarchy uses strings)
197
+ if isinstance(parent_id, CompositeMapKey):
198
+ return parent_id.to_cache_key()
199
+ return parent_id
200
+
201
+ def _reg_map(map: Map | Children, direction: FastMapDirection, parent: Map | None = None):
202
+ from_id = _get_identifier(map, direction)
203
+ parent_id = _get_parent_id_str(parent, direction)
204
+ map_key = FastMapKey(from_id, map.type, parent_id, direction)
205
+ self.fast_maps[map_key].append(map)
206
+
207
+ for direction in FastMapDirection:
208
+ for m in mappings:
209
+ for child in m.children:
210
+ _reg_map(child, direction, m)
211
+ _reg_map(m, direction, None)
212
+
213
+ self.loaded = True
214
+
215
+ # noinspection PyUnreachableCode
216
+ def get_mappings(self, from_id: str, direction: FastMapDirection, mapping_type: MappingType,
217
+ parent_from_id: str | None = None) -> list[Map | Children]:
218
+ """
219
+ Retrieve mappings meeting the criteria. Base method for many of the get_gravitate/source* methods - prefer using
220
+ those for a more expressive syntax.
221
+
222
+ :arg str from_id: The original value we're looking to map. If direction is source_to_grav, this matches on the
223
+ source_id field; if direction is grav_to_source, this matches on the gravitate_id field.
224
+ :arg FastMapDirection direction: The direction of the mapping (gravitate->source or source->gravitate).
225
+ :arg MappingType mapping_type: The type of the mapping.
226
+ :arg str | None parent_from_id: The from_id of the child mapping's parent, if searching for a child. To get
227
+ parent mappings instead, use None (the default).
228
+ :raises MappingsNotLoadedException: If mappings have not been loaded yet.
229
+ """
230
+ self._raise_if_not_loaded()
231
+ if mapping_type is not None and not isinstance(mapping_type, MappingType):
232
+ raise TypeError(
233
+ "mapping_type must be of type bb_integrations_lib.models.rita.mapping.MappingType "
234
+ "(check whether you imported the right MappingType)"
235
+ )
236
+
237
+ return self.fast_maps.get(FastMapKey(from_id, mapping_type, parent_from_id, direction), [])
238
+
239
+ def get_mappings_guaranteed(
240
+ self, from_id: str, direction: FastMapDirection, mapping_type: MappingType,
241
+ parent_from_id: str | None = None
242
+ ) -> list[Map | Children]:
243
+ """Like get_mappings, but additionally raises a KeyError if no mappings are found."""
244
+ self._raise_if_not_loaded()
245
+ mappings = self.get_mappings(from_id, direction, mapping_type, parent_from_id)
246
+ if not mappings:
247
+ from_name = "gravitate" if direction == FastMapDirection.grav_to_source else "source"
248
+ child_slug = f", parent_{from_name}_id='{parent_from_id}'" if parent_from_id else ""
249
+ mapping_type_name = "None" if mapping_type is None else mapping_type.name
250
+ raise KeyError(
251
+ f"No mappings found for {from_name}_id='{from_id}'{child_slug} with type='{mapping_type_name}'"
252
+ )
253
+ return mappings
254
+
255
+ def get_single_mapping_guaranteed(
256
+ self, from_id: str, direction: FastMapDirection, mapping_type: MappingType,
257
+ parent_from_id: str | None = None
258
+ ) -> Map | Children:
259
+ """Like get_mappings_guaranteed, but additionally raises a KeyError if more than 1 mapping is found."""
260
+ mappings = self.get_mappings_guaranteed(from_id, direction, mapping_type, parent_from_id)
261
+ if len(mappings) > 1:
262
+ from_name = "gravitate" if direction == FastMapDirection.grav_to_source else "source"
263
+ child_slug = f", parent_{from_name}_id='{parent_from_id}'" if parent_from_id else ""
264
+ raise KeyError(
265
+ f"Too many mappings ({len(mappings)}) for {from_name}_id='{from_id}'{child_slug} "
266
+ f"with type='{mapping_type.name}', expected 1"
267
+ )
268
+ return mappings[0]
269
+
270
+ def get_gravitate_parent_id(
271
+ self,
272
+ source_id: str | CompositeMapKey,
273
+ mapping_type: MappingType | None
274
+ ) -> str | CompositeMapKey:
275
+ return self.get_single_mapping_guaranteed(source_id, FastMapDirection.source_to_grav, mapping_type).gravitate_id
276
+
277
+ def get_gravitate_child_id(
278
+ self,
279
+ source_parent_id: str,
280
+ source_child_id: str,
281
+ mapping_type: MappingType | None
282
+ ) -> str:
283
+ """
284
+ Get gravitate_id for a child mapping.
285
+
286
+ Note: Child lookups use string IDs only. Children mappings don't support composite keys.
287
+ For composite key lookups (without hierarchy), use get_gravitate_id_by_composite().
288
+ """
289
+ return self.get_single_mapping_guaranteed(
290
+ source_child_id, FastMapDirection.source_to_grav, mapping_type, source_parent_id
291
+ ).gravitate_id
292
+
293
+ def get_source_parent_id(
294
+ self,
295
+ gravitate_id: str | CompositeMapKey,
296
+ mapping_type: MappingType | None
297
+ ) -> str | CompositeMapKey:
298
+ return self.get_single_mapping_guaranteed(gravitate_id, FastMapDirection.grav_to_source, mapping_type).source_id
299
+
300
+ def get_source_child_id(
301
+ self,
302
+ gravitate_parent_id: str,
303
+ gravitate_child_id: str,
304
+ mapping_type: MappingType | None
305
+ ) -> str:
306
+ """
307
+ Get source_id for a child mapping.
308
+
309
+ Note: Child lookups use string IDs only. Children mappings don't support composite keys.
310
+ For composite key lookups (without hierarchy), use get_source_id_by_composite().
311
+ """
312
+ return self.get_single_mapping_guaranteed(
313
+ gravitate_child_id, FastMapDirection.grav_to_source, mapping_type, gravitate_parent_id
314
+ ).source_id
315
+
316
+ def get_gravitate_parent_ids(
317
+ self,
318
+ source_id: str | CompositeMapKey,
319
+ mapping_type: MappingType | None
320
+ ) -> list[str | CompositeMapKey]:
321
+ return [
322
+ x.gravitate_id for x in
323
+ self.get_mappings_guaranteed(source_id, FastMapDirection.source_to_grav, mapping_type)
324
+ ]
325
+
326
+ def get_gravitate_child_ids(
327
+ self,
328
+ source_parent_id: str,
329
+ source_child_id: str,
330
+ mapping_type: MappingType | None
331
+ ) -> list[str]:
332
+ """
333
+ Get all gravitate_ids for child mappings matching the criteria.
334
+
335
+ Note: Child lookups use string IDs only. Children mappings don't support composite keys.
336
+ """
337
+ return [
338
+ x.gravitate_id for x in
339
+ self.get_mappings_guaranteed(
340
+ source_child_id, FastMapDirection.source_to_grav, mapping_type, source_parent_id)
341
+ ]
342
+
343
+ def get_source_parent_ids(
344
+ self,
345
+ gravitate_id: str | CompositeMapKey,
346
+ mapping_type: MappingType | None
347
+ ) -> list[str | CompositeMapKey]:
348
+ return [
349
+ x.source_id for x in
350
+ self.get_mappings_guaranteed(
351
+ gravitate_id, FastMapDirection.grav_to_source, mapping_type)
352
+ ]
353
+
354
+ def get_source_child_ids(
355
+ self,
356
+ gravitate_parent_id: str,
357
+ gravitate_child_id: str,
358
+ mapping_type: MappingType | None
359
+ ) -> list[str]:
360
+ """
361
+ Get all source_ids for child mappings matching the criteria.
362
+
363
+ Note: Child lookups use string IDs only. Children mappings don't support composite keys.
364
+ """
365
+ return [
366
+ x.source_id for x in
367
+ self.get_mappings_guaranteed(
368
+ gravitate_child_id, FastMapDirection.grav_to_source, mapping_type, gravitate_parent_id)
369
+ ]
370
+
371
+ def get_gravitate_parent_id_str(
372
+ self,
373
+ source_id: str,
374
+ mapping_type: MappingType | None
375
+ ) -> str:
376
+ """Get gravitate_id as string. Raises TypeError if mapping uses composite key."""
377
+ result = self.get_gravitate_parent_id(source_id, mapping_type)
378
+ if isinstance(result, CompositeMapKey):
379
+ raise TypeError(f"Mapping has composite gravitate_id: {result}")
380
+ return result
381
+
382
+ def get_source_parent_id_str(
383
+ self,
384
+ gravitate_id: str,
385
+ mapping_type: MappingType | None
386
+ ) -> str:
387
+ """Get source_id as string. Raises TypeError if mapping uses composite key."""
388
+ result = self.get_source_parent_id(gravitate_id, mapping_type)
389
+ if isinstance(result, CompositeMapKey):
390
+ raise TypeError(f"Mapping has composite source_id: {result}")
391
+ return result
392
+
393
+ # Composite key lookup methods
394
+
395
+ def get_mappings_by_composite(
396
+ self,
397
+ from_key: Union[dict, CompositeMapKey],
398
+ direction: FastMapDirection,
399
+ mapping_type: MappingType | None,
400
+ parent_from_id: Optional[str] = None
401
+ ) -> list[Map | Children]:
402
+ """
403
+ Retrieve mappings using a composite key for entity identity.
404
+
405
+ :param from_key: Dict or CompositeMapKey for lookup (entity identity)
406
+ :param direction: Mapping direction
407
+ :param mapping_type: Type of mapping
408
+ :param parent_from_id: Parent identifier (string only - hierarchy uses simple IDs)
409
+
410
+ Note: Composite keys are for entity identity, not hierarchy. If looking up children,
411
+ use string parent_from_id. Children mappings don't support composite keys.
412
+ """
413
+ self._raise_if_not_loaded()
414
+ if isinstance(from_key, dict):
415
+ from_key = CompositeMapKey(key=from_key)
416
+ return self.fast_maps.get(
417
+ FastMapKey(from_key, mapping_type, parent_from_id, direction), []
418
+ )
419
+
420
+ def get_gravitate_id_by_composite(
421
+ self,
422
+ source_key: Union[dict, CompositeMapKey],
423
+ mapping_type: MappingType | None
424
+ ) -> str | CompositeMapKey:
425
+ """Get gravitate_id using a composite source key."""
426
+ mappings = self.get_mappings_by_composite(
427
+ source_key, FastMapDirection.source_to_grav, mapping_type
428
+ )
429
+ if not mappings:
430
+ raise KeyError(f"No mapping found for composite key: {source_key}")
431
+ if len(mappings) > 1:
432
+ raise KeyError(f"Multiple mappings ({len(mappings)}) found for: {source_key}")
433
+ return mappings[0].gravitate_id
434
+
435
+ def get_source_id_by_composite(
436
+ self,
437
+ gravitate_key: Union[dict, CompositeMapKey],
438
+ mapping_type: MappingType | None
439
+ ) -> str | CompositeMapKey:
440
+ """Get source_id using a composite gravitate key."""
441
+ mappings = self.get_mappings_by_composite(
442
+ gravitate_key, FastMapDirection.grav_to_source, mapping_type
443
+ )
444
+ if not mappings:
445
+ raise KeyError(f"No mapping found for composite key: {gravitate_key}")
446
+ if len(mappings) > 1:
447
+ raise KeyError(f"Multiple mappings ({len(mappings)}) found for: {gravitate_key}")
448
+ return mappings[0].source_id
449
+
450
+ def get_mapping_by_composite(
451
+ self,
452
+ from_key: Union[dict, CompositeMapKey],
453
+ direction: FastMapDirection,
454
+ mapping_type: MappingType | None
455
+ ) -> Map | Children:
456
+ """Get single mapping by composite key, raises if not found or multiple."""
457
+ mappings = self.get_mappings_by_composite(from_key, direction, mapping_type)
458
+ if not mappings:
459
+ raise KeyError(f"No mapping found for composite key: {from_key}")
460
+ if len(mappings) > 1:
461
+ raise KeyError(f"Multiple mappings ({len(mappings)}) found for: {from_key}")
462
+ return mappings[0]
463
+
464
+ def find_mappings_by_partial_key(
465
+ self,
466
+ partial_key: dict[str, str],
467
+ direction: FastMapDirection,
468
+ mapping_type: MappingType | None = None
469
+ ) -> list[Map | Children]:
470
+ """
471
+ Find all mappings where the composite key contains the partial key fields.
472
+
473
+ Example: partial_key={"product": "X"} matches {"product": "X", "terminal": "Y"}
474
+ """
475
+ self._raise_if_not_loaded()
476
+ results = []
477
+ for fast_key, mappings in self.fast_maps.items():
478
+ if fast_key.direction != direction:
479
+ continue
480
+ if mapping_type is not None and fast_key.type != mapping_type:
481
+ continue
482
+ # Check if from_id is a CompositeMapKey and matches partial
483
+ if isinstance(fast_key.from_id, CompositeMapKey):
484
+ if fast_key.from_id.matches(partial_key):
485
+ results.extend(mappings)
486
+ return results
487
+
488
+
489
+ class RitaMapper(RitaMapperCore):
490
+ def __init__(self, provider: MappingProvider | AsyncMappingProvider, source_system: str):
491
+ super().__init__()
492
+ self.provider = provider
493
+ self.source_system = source_system
494
+
495
+ async def load_mappings_async(self):
496
+ """
497
+ Load mappings from the provider asynchronously.
498
+
499
+ :raises TypeError: If the provider is not async.
500
+
501
+ Note that this method does not check whether mappings are already loaded; calling it will overwrite any stored
502
+ mappings with a fresh set from the provider.
503
+ """
504
+ if isinstance(self.provider, AsyncMappingProvider):
505
+ mappings = await self.provider.get_mappings_by_source_system(self.source_system)
506
+ else:
507
+ raise TypeError("Provider is not async")
508
+ super()._load_mappings(mappings)
509
+
510
+ def load_mappings(self):
511
+ """
512
+ Load mappings from the provider synchronously.
513
+
514
+ :raises TypeError: If the provider is async.
515
+
516
+ Note that this method does not check whether mappings are already loaded; calling it will overwrite any stored
517
+ mappings with a fresh set from the provider.
518
+ """
519
+ if isinstance(self.provider, MappingProvider):
520
+ mappings = self.provider.get_mappings_by_source_system(self.source_system)
521
+ else:
522
+ raise TypeError("Provider is not synchronous")
523
+ super()._load_mappings(mappings)
File without changes