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.
- bb_integrations_lib/__init__.py +0 -0
- bb_integrations_lib/converters/__init__.py +0 -0
- bb_integrations_lib/gravitate/__init__.py +0 -0
- bb_integrations_lib/gravitate/base_api.py +20 -0
- bb_integrations_lib/gravitate/model.py +29 -0
- bb_integrations_lib/gravitate/pe_api.py +122 -0
- bb_integrations_lib/gravitate/rita_api.py +552 -0
- bb_integrations_lib/gravitate/sd_api.py +572 -0
- bb_integrations_lib/gravitate/testing/TTE/sd/models.py +1398 -0
- bb_integrations_lib/gravitate/testing/TTE/sd/tests/test_models.py +2987 -0
- bb_integrations_lib/gravitate/testing/__init__.py +0 -0
- bb_integrations_lib/gravitate/testing/builder.py +55 -0
- bb_integrations_lib/gravitate/testing/openapi.py +70 -0
- bb_integrations_lib/gravitate/testing/util.py +274 -0
- bb_integrations_lib/mappers/__init__.py +0 -0
- bb_integrations_lib/mappers/prices/__init__.py +0 -0
- bb_integrations_lib/mappers/prices/model.py +106 -0
- bb_integrations_lib/mappers/prices/price_mapper.py +127 -0
- bb_integrations_lib/mappers/prices/protocol.py +20 -0
- bb_integrations_lib/mappers/prices/util.py +61 -0
- bb_integrations_lib/mappers/rita_mapper.py +523 -0
- bb_integrations_lib/models/__init__.py +0 -0
- bb_integrations_lib/models/dtn_supplier_invoice.py +487 -0
- bb_integrations_lib/models/enums.py +28 -0
- bb_integrations_lib/models/pipeline_structs.py +76 -0
- bb_integrations_lib/models/probe/probe_event.py +20 -0
- bb_integrations_lib/models/probe/request_data.py +431 -0
- bb_integrations_lib/models/probe/resume_token.py +7 -0
- bb_integrations_lib/models/rita/audit.py +113 -0
- bb_integrations_lib/models/rita/auth.py +30 -0
- bb_integrations_lib/models/rita/bucket.py +17 -0
- bb_integrations_lib/models/rita/config.py +188 -0
- bb_integrations_lib/models/rita/constants.py +19 -0
- bb_integrations_lib/models/rita/crossroads_entities.py +293 -0
- bb_integrations_lib/models/rita/crossroads_mapping.py +428 -0
- bb_integrations_lib/models/rita/crossroads_monitoring.py +78 -0
- bb_integrations_lib/models/rita/crossroads_network.py +41 -0
- bb_integrations_lib/models/rita/crossroads_rules.py +80 -0
- bb_integrations_lib/models/rita/email.py +39 -0
- bb_integrations_lib/models/rita/issue.py +63 -0
- bb_integrations_lib/models/rita/mapping.py +227 -0
- bb_integrations_lib/models/rita/probe.py +58 -0
- bb_integrations_lib/models/rita/reference_data.py +110 -0
- bb_integrations_lib/models/rita/source_system.py +9 -0
- bb_integrations_lib/models/rita/workers.py +76 -0
- bb_integrations_lib/models/sd/bols_and_drops.py +241 -0
- bb_integrations_lib/models/sd/get_order.py +301 -0
- bb_integrations_lib/models/sd/orders.py +18 -0
- bb_integrations_lib/models/sd_api.py +115 -0
- bb_integrations_lib/pipelines/__init__.py +0 -0
- bb_integrations_lib/pipelines/parsers/__init__.py +0 -0
- bb_integrations_lib/pipelines/parsers/distribution_report/__init__.py +0 -0
- bb_integrations_lib/pipelines/parsers/distribution_report/order_by_site_product_parser.py +50 -0
- bb_integrations_lib/pipelines/parsers/distribution_report/tank_configs_parser.py +47 -0
- bb_integrations_lib/pipelines/parsers/dtn/__init__.py +0 -0
- bb_integrations_lib/pipelines/parsers/dtn/dtn_price_parser.py +102 -0
- bb_integrations_lib/pipelines/parsers/dtn/model.py +79 -0
- bb_integrations_lib/pipelines/parsers/price_engine/__init__.py +0 -0
- bb_integrations_lib/pipelines/parsers/price_engine/parse_accessorials_prices_parser.py +67 -0
- bb_integrations_lib/pipelines/parsers/price_engine/price_file_upload/__init__.py +0 -0
- bb_integrations_lib/pipelines/parsers/price_engine/price_file_upload/price_merge_parser.py +111 -0
- bb_integrations_lib/pipelines/parsers/price_engine/price_file_upload/price_sync_parser.py +107 -0
- bb_integrations_lib/pipelines/parsers/price_engine/price_file_upload/shared.py +81 -0
- bb_integrations_lib/pipelines/parsers/tank_reading_parser.py +155 -0
- bb_integrations_lib/pipelines/parsers/tank_sales_parser.py +144 -0
- bb_integrations_lib/pipelines/shared/__init__.py +0 -0
- bb_integrations_lib/pipelines/shared/allocation_matching.py +227 -0
- bb_integrations_lib/pipelines/shared/bol_allocation.py +2793 -0
- bb_integrations_lib/pipelines/steps/__init__.py +0 -0
- bb_integrations_lib/pipelines/steps/create_accessorials_step.py +80 -0
- bb_integrations_lib/pipelines/steps/distribution_report/__init__.py +0 -0
- bb_integrations_lib/pipelines/steps/distribution_report/distribution_report_datafram_to_raw_data.py +33 -0
- bb_integrations_lib/pipelines/steps/distribution_report/get_model_history_step.py +50 -0
- bb_integrations_lib/pipelines/steps/distribution_report/get_order_by_site_product_step.py +62 -0
- bb_integrations_lib/pipelines/steps/distribution_report/get_tank_configs_step.py +40 -0
- bb_integrations_lib/pipelines/steps/distribution_report/join_distribution_order_dos_step.py +85 -0
- bb_integrations_lib/pipelines/steps/distribution_report/upload_distribution_report_datafram_to_big_query.py +47 -0
- bb_integrations_lib/pipelines/steps/echo_step.py +14 -0
- bb_integrations_lib/pipelines/steps/export_dataframe_to_rawdata_step.py +28 -0
- bb_integrations_lib/pipelines/steps/exporting/__init__.py +0 -0
- bb_integrations_lib/pipelines/steps/exporting/bbd_export_payroll_file_step.py +107 -0
- bb_integrations_lib/pipelines/steps/exporting/bbd_export_readings_step.py +236 -0
- bb_integrations_lib/pipelines/steps/exporting/cargas_wholesale_bundle_upload_step.py +33 -0
- bb_integrations_lib/pipelines/steps/exporting/dataframe_flat_file_export.py +29 -0
- bb_integrations_lib/pipelines/steps/exporting/gcs_bucket_export_file_step.py +34 -0
- bb_integrations_lib/pipelines/steps/exporting/keyvu_export_step.py +356 -0
- bb_integrations_lib/pipelines/steps/exporting/pe_price_export_step.py +238 -0
- bb_integrations_lib/pipelines/steps/exporting/platform_science_order_sync_step.py +500 -0
- bb_integrations_lib/pipelines/steps/exporting/save_rawdata_to_disk.py +15 -0
- bb_integrations_lib/pipelines/steps/exporting/sftp_export_file_step.py +60 -0
- bb_integrations_lib/pipelines/steps/exporting/sftp_export_many_files_step.py +23 -0
- bb_integrations_lib/pipelines/steps/exporting/update_exported_orders_table_step.py +64 -0
- bb_integrations_lib/pipelines/steps/filter_step.py +22 -0
- bb_integrations_lib/pipelines/steps/get_latest_sync_date.py +34 -0
- bb_integrations_lib/pipelines/steps/importing/bbd_import_payroll_step.py +30 -0
- bb_integrations_lib/pipelines/steps/importing/get_order_numbers_to_export_step.py +138 -0
- bb_integrations_lib/pipelines/steps/importing/load_file_to_dataframe_step.py +46 -0
- bb_integrations_lib/pipelines/steps/importing/load_imap_attachment_step.py +172 -0
- bb_integrations_lib/pipelines/steps/importing/pe_bulk_sync_price_structure_step.py +68 -0
- bb_integrations_lib/pipelines/steps/importing/pe_price_merge_step.py +86 -0
- bb_integrations_lib/pipelines/steps/importing/sftp_file_config_step.py +124 -0
- bb_integrations_lib/pipelines/steps/importing/test_exact_file_match.py +57 -0
- bb_integrations_lib/pipelines/steps/null_step.py +15 -0
- bb_integrations_lib/pipelines/steps/pe_integration_job_step.py +32 -0
- bb_integrations_lib/pipelines/steps/processing/__init__.py +0 -0
- bb_integrations_lib/pipelines/steps/processing/archive_gcs_step.py +76 -0
- bb_integrations_lib/pipelines/steps/processing/archive_sftp_step.py +48 -0
- bb_integrations_lib/pipelines/steps/processing/bbd_format_tank_readings_step.py +492 -0
- bb_integrations_lib/pipelines/steps/processing/bbd_upload_prices_step.py +54 -0
- bb_integrations_lib/pipelines/steps/processing/bbd_upload_tank_sales_step.py +124 -0
- bb_integrations_lib/pipelines/steps/processing/bbd_upload_tankreading_step.py +80 -0
- bb_integrations_lib/pipelines/steps/processing/convert_bbd_order_to_cargas_step.py +226 -0
- bb_integrations_lib/pipelines/steps/processing/delete_sftp_step.py +33 -0
- bb_integrations_lib/pipelines/steps/processing/dtn/__init__.py +2 -0
- bb_integrations_lib/pipelines/steps/processing/dtn/convert_dtn_invoice_to_sd_model.py +145 -0
- bb_integrations_lib/pipelines/steps/processing/dtn/parse_dtn_invoice_step.py +38 -0
- bb_integrations_lib/pipelines/steps/processing/file_config_parser_step.py +720 -0
- bb_integrations_lib/pipelines/steps/processing/file_config_parser_step_v2.py +418 -0
- bb_integrations_lib/pipelines/steps/processing/get_sd_price_price_request.py +105 -0
- bb_integrations_lib/pipelines/steps/processing/keyvu_upload_deliveryplan_step.py +39 -0
- bb_integrations_lib/pipelines/steps/processing/mark_orders_exported_in_bbd_step.py +185 -0
- bb_integrations_lib/pipelines/steps/processing/pe_price_rows_processing_step.py +174 -0
- bb_integrations_lib/pipelines/steps/processing/send_process_report_step.py +47 -0
- bb_integrations_lib/pipelines/steps/processing/sftp_renamer_step.py +61 -0
- bb_integrations_lib/pipelines/steps/processing/tank_reading_touchup_steps.py +75 -0
- bb_integrations_lib/pipelines/steps/processing/upload_supplier_invoice_step.py +16 -0
- bb_integrations_lib/pipelines/steps/send_attached_in_rita_email_step.py +44 -0
- bb_integrations_lib/pipelines/steps/send_rita_email_step.py +34 -0
- bb_integrations_lib/pipelines/steps/sleep_step.py +24 -0
- bb_integrations_lib/pipelines/wrappers/__init__.py +0 -0
- bb_integrations_lib/pipelines/wrappers/accessorials_transformation.py +104 -0
- bb_integrations_lib/pipelines/wrappers/distribution_report.py +191 -0
- bb_integrations_lib/pipelines/wrappers/export_tank_readings.py +237 -0
- bb_integrations_lib/pipelines/wrappers/import_tank_readings.py +192 -0
- bb_integrations_lib/pipelines/wrappers/wrapper.py +81 -0
- bb_integrations_lib/protocols/__init__.py +0 -0
- bb_integrations_lib/protocols/flat_file.py +210 -0
- bb_integrations_lib/protocols/gravitate_client.py +104 -0
- bb_integrations_lib/protocols/pipelines.py +697 -0
- bb_integrations_lib/provider/__init__.py +0 -0
- bb_integrations_lib/provider/api/__init__.py +0 -0
- bb_integrations_lib/provider/api/cargas/__init__.py +0 -0
- bb_integrations_lib/provider/api/cargas/client.py +43 -0
- bb_integrations_lib/provider/api/cargas/model.py +49 -0
- bb_integrations_lib/provider/api/cargas/protocol.py +23 -0
- bb_integrations_lib/provider/api/dtn/__init__.py +0 -0
- bb_integrations_lib/provider/api/dtn/client.py +128 -0
- bb_integrations_lib/provider/api/dtn/protocol.py +9 -0
- bb_integrations_lib/provider/api/keyvu/__init__.py +0 -0
- bb_integrations_lib/provider/api/keyvu/client.py +30 -0
- bb_integrations_lib/provider/api/keyvu/model.py +149 -0
- bb_integrations_lib/provider/api/macropoint/__init__.py +0 -0
- bb_integrations_lib/provider/api/macropoint/client.py +28 -0
- bb_integrations_lib/provider/api/macropoint/model.py +40 -0
- bb_integrations_lib/provider/api/pc_miler/__init__.py +0 -0
- bb_integrations_lib/provider/api/pc_miler/client.py +130 -0
- bb_integrations_lib/provider/api/pc_miler/model.py +6 -0
- bb_integrations_lib/provider/api/pc_miler/web_services_apis.py +131 -0
- bb_integrations_lib/provider/api/platform_science/__init__.py +0 -0
- bb_integrations_lib/provider/api/platform_science/client.py +147 -0
- bb_integrations_lib/provider/api/platform_science/model.py +82 -0
- bb_integrations_lib/provider/api/quicktrip/__init__.py +0 -0
- bb_integrations_lib/provider/api/quicktrip/client.py +52 -0
- bb_integrations_lib/provider/api/telapoint/__init__.py +0 -0
- bb_integrations_lib/provider/api/telapoint/client.py +68 -0
- bb_integrations_lib/provider/api/telapoint/model.py +178 -0
- bb_integrations_lib/provider/api/warren_rogers/__init__.py +0 -0
- bb_integrations_lib/provider/api/warren_rogers/client.py +207 -0
- bb_integrations_lib/provider/aws/__init__.py +0 -0
- bb_integrations_lib/provider/aws/s3/__init__.py +0 -0
- bb_integrations_lib/provider/aws/s3/client.py +126 -0
- bb_integrations_lib/provider/ftp/__init__.py +0 -0
- bb_integrations_lib/provider/ftp/client.py +140 -0
- bb_integrations_lib/provider/ftp/interface.py +273 -0
- bb_integrations_lib/provider/ftp/model.py +76 -0
- bb_integrations_lib/provider/imap/__init__.py +0 -0
- bb_integrations_lib/provider/imap/client.py +228 -0
- bb_integrations_lib/provider/imap/model.py +3 -0
- bb_integrations_lib/provider/sqlserver/__init__.py +0 -0
- bb_integrations_lib/provider/sqlserver/client.py +106 -0
- bb_integrations_lib/secrets/__init__.py +4 -0
- bb_integrations_lib/secrets/adapters.py +98 -0
- bb_integrations_lib/secrets/credential_models.py +222 -0
- bb_integrations_lib/secrets/factory.py +85 -0
- bb_integrations_lib/secrets/providers.py +160 -0
- bb_integrations_lib/shared/__init__.py +0 -0
- bb_integrations_lib/shared/exceptions.py +25 -0
- bb_integrations_lib/shared/model.py +1039 -0
- bb_integrations_lib/shared/shared_enums.py +510 -0
- bb_integrations_lib/storage/README.md +236 -0
- bb_integrations_lib/storage/__init__.py +0 -0
- bb_integrations_lib/storage/aws/__init__.py +0 -0
- bb_integrations_lib/storage/aws/s3.py +8 -0
- bb_integrations_lib/storage/defaults.py +72 -0
- bb_integrations_lib/storage/gcs/__init__.py +0 -0
- bb_integrations_lib/storage/gcs/client.py +8 -0
- bb_integrations_lib/storage/gcsmanager/__init__.py +0 -0
- bb_integrations_lib/storage/gcsmanager/client.py +8 -0
- bb_integrations_lib/storage/setup.py +29 -0
- bb_integrations_lib/util/__init__.py +0 -0
- bb_integrations_lib/util/cache/__init__.py +0 -0
- bb_integrations_lib/util/cache/custom_ttl_cache.py +75 -0
- bb_integrations_lib/util/cache/protocol.py +9 -0
- bb_integrations_lib/util/config/__init__.py +0 -0
- bb_integrations_lib/util/config/manager.py +391 -0
- bb_integrations_lib/util/config/model.py +41 -0
- bb_integrations_lib/util/exception_logger/__init__.py +0 -0
- bb_integrations_lib/util/exception_logger/exception_logger.py +146 -0
- bb_integrations_lib/util/exception_logger/test.py +114 -0
- bb_integrations_lib/util/utils.py +364 -0
- bb_integrations_lib/workers/__init__.py +0 -0
- bb_integrations_lib/workers/groups.py +13 -0
- bb_integrations_lib/workers/rpc_worker.py +50 -0
- bb_integrations_lib/workers/topics.py +20 -0
- bb_integrations_library-3.0.11.dist-info/METADATA +59 -0
- bb_integrations_library-3.0.11.dist-info/RECORD +217 -0
- 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
|
+
)
|