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,418 @@
1
+ from zipfile import BadZipFile
2
+
3
+ import json
4
+ import math
5
+ import pandas as pd
6
+ from bb_integrations_lib.util.utils import CustomJSONEncoder
7
+ from loguru import logger
8
+ from pydantic import BaseModel
9
+ from typing import AsyncGenerator, Union
10
+ from typing import Tuple, List, Dict, Any, TypeVar
11
+
12
+ from bb_integrations_lib.gravitate.rita_api import GravitateRitaAPI
13
+ from bb_integrations_lib.models.rita.config import FileConfig, ConfigAction
14
+ from bb_integrations_lib.protocols.pipelines import Step, ParserBase, Parser
15
+ from bb_integrations_lib.shared.exceptions import FileParsingError
16
+ from bb_integrations_lib.shared.model import FileConfigRawData, MappingMode, RawData
17
+
18
+ AnyParser = TypeVar("AnyParser", bound=ParserBase | Parser)
19
+
20
+
21
+ class FileConfigParserJSONEncoder(json.JSONEncoder):
22
+ def default(self, o):
23
+ if isinstance(o, BaseModel):
24
+ return o.model_dump()
25
+ return super().default(o)
26
+
27
+
28
+ class FileConfigParserV2(Step):
29
+ def __init__(self, rita_client: GravitateRitaAPI, mapping_type: MappingMode = MappingMode.full,
30
+ parser: type[AnyParser] | None = None, parser_kwargs: dict | None = None, *args, **kwargs):
31
+ """
32
+ Parse an input file using a FileConfig, and optionally, a custom parser.
33
+
34
+ :param rita_client: GravitateRitaAPI instance
35
+ :param mapping_type: How the parser should use mappings while processing rows. Defaults to "full"
36
+ :param parser: Custom parser to pass translated rows through. Can be used to process many different types of
37
+ files, such as tank readings or price tables.
38
+ :param parser_kwargs: Additional keyword arguments to pass to the custom parser's init method.
39
+ """
40
+ super().__init__(*args, **kwargs)
41
+ if parser:
42
+ self.custom_parser: type[ParserBase] = parser
43
+ self.custom_parser_kwargs = parser_kwargs or {}
44
+
45
+ self.rita_client = rita_client
46
+ self.mapping_type = mapping_type
47
+
48
+ def describe(self) -> str:
49
+ """Return a description of this step for logging."""
50
+ return "Parse and translate file data using FileConfig"
51
+
52
+ async def execute(self, rd: FileConfigRawData) -> Union[List[Dict], RawData]:
53
+ """
54
+ Execute file parsing and translation.
55
+ Args:
56
+ rd: FileConfigRawData containing file data and configuration
57
+
58
+ Returns:
59
+ List of translated records, or RawData if the custom parser returns RawData
60
+ """
61
+ file_name = rd.file_name or "<unknown filename>"
62
+ self.pipeline_context.included_files[f"{file_name}"] = rd.data.read()
63
+ translated_records, errors = self.get_translated_records(rd)
64
+ if errors:
65
+ logger.warning(f"Found {len(errors)} translation errors during parsing")
66
+ self.pipeline_context.included_files["File Config Parser Step Translation Errors"] = json.dumps(errors)
67
+ if not hasattr(self, "custom_parser"):
68
+ self.pipeline_context.included_files["Parsed Results"] = json.dumps(translated_records)
69
+ return translated_records
70
+ else:
71
+ logger.info(f"Using custom parser: {self.custom_parser.__name__}")
72
+ parser = self.custom_parser(
73
+ source_system=rd.file_config.source_system,
74
+ file_name=rd.file_name,
75
+ **self.custom_parser_kwargs
76
+ )
77
+ result = parser.parse(translated_records, self.mapping_type)
78
+ if isinstance(result, AsyncGenerator):
79
+ parser_results = [item async for item in result]
80
+ else:
81
+ parser_results = await result
82
+
83
+ if isinstance(parser_results, RawData):
84
+ logger.info(f"Parser returned RawData: {parser_results.file_name}")
85
+ return parser_results
86
+
87
+ self.pipeline_context.included_files[f"{self.custom_parser.__name__} Results"] = json.dumps(
88
+ parser_results, cls=CustomJSONEncoder
89
+ )
90
+ return parser_results
91
+
92
+ def _clean_dataframe(self, df: pd.DataFrame) -> pd.DataFrame:
93
+ """
94
+ Clean and normalize dataframe:
95
+ - Strip whitespace from column names
96
+ - Strip whitespace from all string values
97
+ - Remove duplicate rows
98
+
99
+ Args:
100
+ df: Input DataFrame to clean
101
+
102
+ Returns:
103
+ Cleaned DataFrame
104
+ """
105
+ df = df.rename(columns=lambda x: x.strip() if isinstance(x, str) else x)
106
+ df = df.map(lambda x: x.strip() if isinstance(x, str) else x)
107
+ df = df.drop_duplicates()
108
+ return df
109
+
110
+ def get_records(self, rd: FileConfigRawData) -> List[Dict]:
111
+ """
112
+ Parse file data into records based on file extension configuration.
113
+
114
+ Supports multiple file formats:
115
+ - csv: Standard CSV with headers
116
+ - csv1: CSV with first row skipped
117
+ - csv_headless: CSV without headers (generates col 1, col 2, etc.)
118
+ - xls/xlsx: Excel files (tries openpyxl first, falls back to xlrd)
119
+ - html: HTML tables
120
+
121
+ Args:
122
+ rd: FileConfigRawData containing file data and configuration
123
+
124
+ Returns:
125
+ List of dictionaries, each representing a row from the file
126
+
127
+ Raises:
128
+ FileParsingError: If file parsing fails for any reason
129
+ """
130
+ try:
131
+ if hasattr(rd.data, 'seek'):
132
+ rd.data.seek(0)
133
+
134
+ match rd.file_config.file_extension:
135
+ case "csv1":
136
+ temp_df = pd.read_csv(
137
+ rd.data,
138
+ index_col=False,
139
+ dtype=str,
140
+ skiprows=1,
141
+ keep_default_na=False
142
+ )
143
+
144
+ case "csv":
145
+ temp_df = pd.read_csv(
146
+ rd.data,
147
+ index_col=False,
148
+ dtype=str,
149
+ keep_default_na=False
150
+ )
151
+
152
+ case "csv_headless":
153
+ temp_df = pd.read_csv(
154
+ rd.data,
155
+ index_col=False,
156
+ dtype=str,
157
+ header=None,
158
+ keep_default_na=False
159
+ )
160
+ temp_df.columns = [f"col {i + 1}" for i in range(temp_df.shape[1])]
161
+
162
+ case "xls" | "xlsx":
163
+ try:
164
+ temp_df = pd.read_excel(
165
+ rd.data,
166
+ engine="openpyxl",
167
+ dtype=str,
168
+ keep_default_na=False
169
+ )
170
+ except (OSError, BadZipFile):
171
+ temp_df = pd.read_excel(
172
+ rd.data,
173
+ engine="xlrd",
174
+ dtype=str,
175
+ keep_default_na=False
176
+ )
177
+
178
+ case "html":
179
+ data = pd.read_html(rd.data, header=0, keep_default_na=False)
180
+ temp_df = pd.concat(data)
181
+ temp_df = temp_df.astype(str)
182
+
183
+ case "override_header":
184
+ temp_df = pd.read_csv(
185
+ rd.data,
186
+ index_col=False,
187
+ dtype=str,
188
+ skiprows=1,
189
+ keep_default_na=False
190
+ )
191
+ temp_df.columns = [f"col {i + 1}" for i in range(temp_df.shape[1])]
192
+
193
+ case _:
194
+ raise ValueError(
195
+ f"File extension '{rd.file_config.file_extension}' is not supported"
196
+ )
197
+
198
+ temp_df = self._clean_dataframe(temp_df)
199
+ records = temp_df.to_dict(orient="records")
200
+ return records
201
+
202
+ except Exception as e:
203
+ msg = f"Failed to parse file with extension '{rd.file_config.file_extension}': {e}"
204
+ logger.error(msg)
205
+ raise FileParsingError(msg) from e
206
+
207
+ def get_translated_records(self, rd: FileConfigRawData) -> Tuple[List[Dict], List[Dict]]:
208
+ """
209
+ Parse and translate file records using the file configuration.
210
+
211
+ Extracts records from the file and applies column transformations
212
+ defined in the file configuration. Collects translation errors
213
+ without stopping processing.
214
+
215
+ Args:
216
+ rd: FileConfigRawData containing file data and configuration
217
+
218
+ Returns:
219
+ Tuple of (successfully_translated_records, translation_errors)
220
+ - successfully_translated_records: List of translated record dictionaries
221
+ - translation_errors: List of error dictionaries with 'row' and 'error' keys
222
+ """
223
+ translated_records = []
224
+ translation_errors = []
225
+ records = self.get_records(rd)
226
+ for row in records:
227
+ try:
228
+ translated = FileConfigParserV2.translate_row(rd.file_config, row)
229
+ except Exception as e:
230
+ logger.warning(f"Translation failed for record {row}: {e}")
231
+ translation_errors.append({
232
+ "row": row,
233
+ "error": str(e)
234
+ })
235
+ continue
236
+ translated_records.append(translated)
237
+ return translated_records, translation_errors
238
+
239
+ @staticmethod
240
+ def translate_row(file_config: FileConfig, row: dict) -> Dict:
241
+ """
242
+ Transform a single row based on file configuration column mappings.
243
+
244
+ Applies various transformation actions to row data:
245
+ - concat: Concatenate multiple columns/literals
246
+ - parse_date: Parse date strings using pandas
247
+ - concat_date: Concatenate multiple columns/literals with spaces, then parse as date
248
+ - add: Add literal values
249
+ - remove_leading_zeros/remove_trailing_zeros: Strip zeros
250
+ - wesroc_volume_formula: Calculate Wesroc volume using the formula: (val1 * val2) / 100
251
+ - default: Direct column mapping
252
+
253
+ Sets failed column transformations to None and logs warnings.
254
+
255
+ Args:
256
+ file_config: FileConfig containing column transformation rules
257
+ row: Dictionary representing a single data row
258
+
259
+ Returns:
260
+ Transformed row dictionary with mapped column names
261
+ """
262
+ output_row = {}
263
+ for column in file_config.cols:
264
+ try:
265
+ if len(column.file_columns) == 0:
266
+ output_row[column.column_name] = None
267
+ elif column.action == ConfigAction.concat:
268
+ concatenated = ""
269
+ for entry in column.file_columns:
270
+ stripped_entry = entry.strip()
271
+ if stripped_entry in row:
272
+ value = row[stripped_entry]
273
+ if value is None or (isinstance(value, float) and math.isnan(value) or pd.isna(value)):
274
+ concatenated += ""
275
+ else:
276
+ concatenated += str(value)
277
+ else:
278
+ # entry is not in col, concat it literally, don't strip it to avoid issues with intentional spaces
279
+ concatenated += str(entry)
280
+ output_row[column.column_name] = concatenated
281
+ elif column.action == ConfigAction.parse_date:
282
+ if column.file_columns[0] not in row:
283
+ raise KeyError(f"Column '{column.file_columns[0]}' not found in row")
284
+ try:
285
+ output_row[column.column_name] = FileConfigParserV2._parse_datetime_to_string(
286
+ row[column.file_columns[0]],
287
+ column.format
288
+ )
289
+ except Exception as e:
290
+ raise ValueError(f"Failed to parse date from '{row[column.file_columns[0]]}': {e}")
291
+ elif column.action == ConfigAction.concat_date:
292
+ concatenated_parts = []
293
+ for entry in column.file_columns:
294
+ stripped_entry = entry.strip()
295
+ if stripped_entry in row:
296
+ value = row[stripped_entry]
297
+ if value is None or (isinstance(value, float) and math.isnan(value) or pd.isna(value)):
298
+ continue
299
+ else:
300
+ concatenated_parts.append(str(value))
301
+ else:
302
+ concatenated_parts.append(str(entry))
303
+
304
+ concatenated = " ".join(concatenated_parts)
305
+ try:
306
+ output_row[column.column_name] = FileConfigParserV2._parse_datetime_to_string(
307
+ concatenated,
308
+ column.format
309
+ )
310
+ except Exception as e:
311
+ raise ValueError(f"Failed to parse date from concatenated string '{concatenated}': {e}")
312
+ elif column.action == ConfigAction.add:
313
+ output_row[column.column_name] = column.file_columns[0]
314
+ elif column.action == ConfigAction.remove_leading_zeros:
315
+ if column.file_columns[0] not in row:
316
+ raise KeyError(f"Column '{column.file_columns[0]}' not found in row")
317
+ output_row[column.column_name] = FileConfigParserV2.strip_leading_zeroes(
318
+ str(row[column.file_columns[0]]))
319
+ elif column.action == ConfigAction.remove_trailing_zeros:
320
+ if column.file_columns[0] not in row:
321
+ raise KeyError(f"Column '{column.file_columns[0]}' not found in row")
322
+ output_row[column.column_name] = FileConfigParserV2.strip_trailing_zeroes(
323
+ str(row[column.file_columns[0]]))
324
+ elif column.action == ConfigAction.wesroc_volume_formula:
325
+ if len(column.file_columns) != 2:
326
+ raise ValueError(
327
+ f"Wesroc volume formula action requires exactly 2 columns, got {len(column.file_columns)}")
328
+ if column.file_columns[0] not in row:
329
+ raise KeyError(f"Column '{column.file_columns[0]}' not found in row")
330
+ if column.file_columns[1] not in row:
331
+ raise KeyError(f"Column '{column.file_columns[1]}' not found in row")
332
+ output_row[column.column_name] = FileConfigParserV2.calculate_wesroc_volume(
333
+ row[column.file_columns[0]],
334
+ row[column.file_columns[1]],
335
+ column.file_columns[0],
336
+ column.file_columns[1]
337
+ )
338
+ else:
339
+ if column.file_columns[0] not in row:
340
+ raise KeyError(f"Column '{column.file_columns[0]}' not found in row")
341
+ output_row[column.column_name] = str(row[column.file_columns[0]])
342
+ except Exception as e:
343
+ logger.warning(f"Failed to translate column '{column.column_name}': {e}")
344
+ output_row[column.column_name] = None
345
+ return output_row
346
+
347
+ @staticmethod
348
+ def _parse_datetime_to_string(value: str, date_format: str | None) -> str:
349
+ """
350
+ Parse a datetime value and convert it to a string.
351
+
352
+ Args:
353
+ value: The value to parse as a datetime
354
+ date_format: Optional format string for parsing (None means auto-detect)
355
+
356
+ Returns:
357
+ String representation of the parsed datetime
358
+ """
359
+ return str(
360
+ pd.to_datetime(
361
+ value,
362
+ format=date_format # None means auto-detect
363
+ )
364
+ )
365
+
366
+ @staticmethod
367
+ def strip_leading_zeroes(row: str) -> str:
368
+ """
369
+ Remove leading zeros from a string.
370
+
371
+ Args:
372
+ row: Input string to process
373
+
374
+ Returns:
375
+ String with leading zeros removed
376
+ """
377
+ return row.lstrip('0')
378
+
379
+ @staticmethod
380
+ def strip_trailing_zeroes(row: str) -> str:
381
+ """
382
+ Remove trailing zeros from a string.
383
+
384
+ Args:
385
+ row: Input string to process
386
+
387
+ Returns:
388
+ String with trailing zeros removed
389
+ """
390
+ return row.rstrip('0')
391
+
392
+ @staticmethod
393
+ def calculate_wesroc_volume(val1: Any, val2: Any, col1_name: str, col2_name: str) -> float:
394
+ """
395
+ Calculate Wesroc volume using the formula: (val1 * val2) / 100
396
+
397
+ Args:
398
+ val1: First value (typically quantity or percentage)
399
+ val2: Second value (typically quantity or percentage)
400
+ col1_name: Name of first column (for error messages)
401
+ col2_name: Name of second column (for error messages)
402
+
403
+ Returns:
404
+ Calculated volume as float
405
+
406
+ Raises:
407
+ ValueError: If values are None/NaN or cannot be converted to float
408
+ """
409
+ if val1 is None or (isinstance(val1, float) and math.isnan(val1)) or pd.isna(val1):
410
+ raise ValueError(f"Column '{col1_name}' contains null/NaN value")
411
+
412
+ if val2 is None or (isinstance(val2, float) and math.isnan(val2)) or pd.isna(val2):
413
+ raise ValueError(f"Column '{col2_name}' contains null/NaN value")
414
+
415
+ try:
416
+ return (float(val1) * float(val2)) / 100
417
+ except (ValueError, TypeError) as e:
418
+ raise ValueError(f"Failed to calculate Wesroc volume from '{val1}' and '{val2}': {e}")
@@ -0,0 +1,105 @@
1
+ import json
2
+ from _datetime import datetime, timedelta
3
+ from typing import Dict, Any, List
4
+
5
+ from dateutil.parser import parse
6
+
7
+ from bb_integrations_lib.mappers.prices.model import PricingIntegrationConfig, EntityConfig, IntegrationMappingConfig
8
+ from bb_integrations_lib.protocols.pipelines import Step
9
+ from bb_integrations_lib.shared.exceptions import MappingNotFoundException
10
+ from bb_integrations_lib.shared.model import PEPriceData, SupplyPriceUpdateManyRequest
11
+
12
+
13
+ class PEParsePricesToSDRequestStep(Step):
14
+ def __init__(self, config: PricingIntegrationConfig, *args, **kwargs):
15
+ super().__init__(*args, **kwargs)
16
+ self.config = config
17
+ self.pricing_strategy = self.config.strategy
18
+
19
+ def describe(self) -> str:
20
+ return f"Parse PE Prices -> SD Price Request"
21
+
22
+ async def execute(self, rows: List[PEPriceData]) -> List[SupplyPriceUpdateManyRequest]:
23
+ return await self.get_price_request(rows)
24
+
25
+ def get_entity_config(self, key: str) -> EntityConfig:
26
+ return self.config.entity_config.get(key)
27
+
28
+ def filter_based_on_strategy(self, rows: List[PEPriceData]) -> List[PEPriceData]:
29
+ filter_criteria = self.pricing_strategy.strategy_includes
30
+ return list(filter(lambda r: r.Rank <= filter_criteria, rows))
31
+
32
+ async def get_price_request(self, rows: List[PEPriceData]) -> List[SupplyPriceUpdateManyRequest]:
33
+ res: List = []
34
+ error_dict = {}
35
+ product_entity: IntegrationMappingConfig = self.get_entity_config("products").external_system_integration
36
+ location_entity: IntegrationMappingConfig = self.get_entity_config("locations").external_system_integration
37
+ supplier_entity: IntegrationMappingConfig = self.get_entity_config("suppliers").external_system_integration
38
+ product_key = product_entity.external_id_field
39
+ location_key = location_entity.external_id_field
40
+ supplier_key = supplier_entity.external_id_field
41
+ rows = self.filter_based_on_strategy(rows)
42
+ for idx, row in enumerate(rows):
43
+ try:
44
+ price = row.CurvePointPrices[0].Value
45
+ row_dump = row.model_dump(mode='json')
46
+ product_source_id = row_dump.get(product_key)
47
+ effective_to = PEParsePricesToSDRequestStep.extend_effective_from(
48
+ effective_from=row.EffectiveFromDateTime,
49
+ effective_to=row.EffectiveToDateTime,
50
+ extend_by=row.ExtendByDays)
51
+ if not product_source_id:
52
+ raise MappingNotFoundException(f"Product is missing source data")
53
+ supplier_source_id = row_dump.get(supplier_key)
54
+ if not supplier_source_id:
55
+ raise MappingNotFoundException(f"Supplier is missing source data")
56
+ terminal_source_id = row_dump.get(location_key)
57
+ if not terminal_source_id:
58
+ raise MappingNotFoundException(f"Location is missing source data")
59
+ res.append(
60
+ SupplyPriceUpdateManyRequest
61
+ (
62
+ source_id=str(row.PriceInstrumentId), # TODO: document this change to use the price id
63
+ source_system_id=self.config.source_system,
64
+ terminal_source_id=terminal_source_id,
65
+ effective_from=row.EffectiveFromDateTime,
66
+ effective_to=effective_to,
67
+ price=price,
68
+ price_type=row.PriceType,
69
+ product_source_id=product_source_id,
70
+ supplier_source_id=supplier_source_id,
71
+ timezone=None, # New PE Fix
72
+ curve_id=row.CurvePointId,
73
+ contract=str(row_dump.get("SourceContractId")) if self.config.use_contract_id else None,
74
+ price_publisher=str(row.PricePublisherId),
75
+ )
76
+ )
77
+ except MappingNotFoundException as mnfe:
78
+ error_dict[idx] = {
79
+ "error_type": "MappingNotFoundException",
80
+ "message": str(mnfe),
81
+ "row_data": row.model_dump()
82
+ }
83
+ log = {
84
+ "parsed_rows": [row.model_dump(mode="json") for row in res],
85
+ "errors": error_dict,
86
+ }
87
+ self.pipeline_context.included_files["parse pricing engine prices to SD schema step"] = json.dumps(log)
88
+ return res
89
+
90
+ @staticmethod
91
+ def extend_effective_from(effective_from: str | datetime,
92
+ effective_to: str | datetime,
93
+ extend_by: int | None) -> str:
94
+ if not extend_by:
95
+ if isinstance(effective_to, datetime):
96
+ return effective_to.isoformat()
97
+ return effective_to
98
+ if isinstance(effective_from, datetime):
99
+ return (effective_from + timedelta(days=extend_by)).isoformat()
100
+ elif isinstance(effective_from, str):
101
+ dt = parse(effective_from)
102
+ return (dt + timedelta(days=extend_by)).isoformat()
103
+
104
+ else:
105
+ raise ValueError(f'Unsupported type {type(effective_from)}')
@@ -0,0 +1,39 @@
1
+ from datetime import datetime
2
+
3
+ import httpx
4
+ from loguru import logger
5
+
6
+ from bb_integrations_lib.protocols.pipelines import Step
7
+ from bb_integrations_lib.provider.api.keyvu.model import KeyVuDeliveryPlan, default_serialization_options
8
+ from bb_integrations_lib.shared.model import RawData
9
+
10
+
11
+ class KeyVuUploadDeliveryPlanStep(Step):
12
+ def __init__(self, endpoint_url: str, keyvu_api_key: str, *args, **kwargs) -> None:
13
+ super().__init__(*args, **kwargs)
14
+ self.endpoint_url = endpoint_url
15
+ self.keyvu_api_key = keyvu_api_key
16
+
17
+ def describe(self) -> str:
18
+ return "Upload a KeyVu DeliveryPlan XML file to KeyVu"
19
+
20
+ async def execute(self, i: KeyVuDeliveryPlan) -> RawData:
21
+ logger.info("Serializing delivery plan")
22
+ dp_file = i.to_xml(**default_serialization_options)
23
+
24
+ logger.info(f"Uploading to {self.endpoint_url} ({len(dp_file)} bytes)")
25
+ res = httpx.post(
26
+ url=self.endpoint_url,
27
+ content=dp_file,
28
+ headers={"KeyVu-Api-Key": self.keyvu_api_key}
29
+ )
30
+ res.raise_for_status()
31
+ logger.debug(f"Response code: {res.status_code}, response body: {res.content}")
32
+ logger.info("Done")
33
+
34
+ self.pipeline_context.included_files["Delivery Plan"] = dp_file
35
+
36
+ return RawData(
37
+ data=dp_file,
38
+ file_name=f"plan_file{datetime.now().isoformat()}.xml"
39
+ )