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,124 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, Any, AsyncIterator
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from bb_integrations_lib.gravitate.rita_api import GravitateRitaAPI
|
|
10
|
+
from bb_integrations_lib.protocols.pipelines import GeneratorStep
|
|
11
|
+
from bb_integrations_lib.provider.ftp.client import FTPIntegrationClient
|
|
12
|
+
from bb_integrations_lib.secrets.credential_models import FTPCredential
|
|
13
|
+
from bb_integrations_lib.provider.ftp.model import FTPType
|
|
14
|
+
from bb_integrations_lib.shared.model import RawData, FileConfigRawData, ConfigMode, ConfigMatchMode
|
|
15
|
+
from bb_integrations_lib.util.utils import check_if_file_greater_than_date, file_exact_match
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SFTPFileConfigStep(GeneratorStep):
|
|
19
|
+
|
|
20
|
+
def __init__(self, rita_client: GravitateRitaAPI,
|
|
21
|
+
ftp_client: FTPIntegrationClient | dict[str, FTPIntegrationClient], mode: ConfigMode,
|
|
22
|
+
match_mode: ConfigMatchMode = ConfigMatchMode.Partial, bucket_name: str | None = None,
|
|
23
|
+
config_name: str | None = None, min_date: datetime = datetime.min, strip_trailing_digits:bool=False, *args,
|
|
24
|
+
**kwargs) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Imports SFTP files based on the provided or discovered fileconfigs.
|
|
27
|
+
|
|
28
|
+
:param rita_client: The RITA client to use to retrieve fileconfigs.
|
|
29
|
+
:param ftp_client: The FTP client, or a dict of FTP clients with keys matching confignames, to use to retrieve
|
|
30
|
+
data.
|
|
31
|
+
:param mode: How the step should discover fileconfigs.
|
|
32
|
+
:param match_mode: How the step should match fileconfigs to various properties of the files being scanned.
|
|
33
|
+
:param bucket_name: The bucket name which holds fileconfigs, for FromBucket and ByName modes.
|
|
34
|
+
:param config_name: The fileconfig name, if using ByName mode.
|
|
35
|
+
:param min_date: Filter out files with a date before this.
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(*args, **kwargs)
|
|
38
|
+
self.rita_client = rita_client
|
|
39
|
+
self.ftp_client = ftp_client
|
|
40
|
+
self.mode = mode
|
|
41
|
+
|
|
42
|
+
self.match_mode = match_mode
|
|
43
|
+
self.strip_trailing_digits = strip_trailing_digits
|
|
44
|
+
self.bucket_name = bucket_name
|
|
45
|
+
self.config_name = config_name
|
|
46
|
+
self.min_date = min_date
|
|
47
|
+
self.file_configs: dict[str, Any] = {}
|
|
48
|
+
|
|
49
|
+
if self.mode == ConfigMode.FromBucket and not self.bucket_name:
|
|
50
|
+
raise ValueError("Cannot use FromBucket mode without setting a bucket_name")
|
|
51
|
+
if self.mode == ConfigMode.ByName and not self.bucket_name:
|
|
52
|
+
raise ValueError("Cannot use ByName mode without setting a bucket_name")
|
|
53
|
+
if self.mode == ConfigMode.ByName and not self.config_name:
|
|
54
|
+
raise ValueError("Cannot use ByName mode without setting a config_name")
|
|
55
|
+
|
|
56
|
+
async def load_file_configs(self):
|
|
57
|
+
if self.mode == ConfigMode.AllFiltered:
|
|
58
|
+
self.file_configs = await self.rita_client.get_file_configs()
|
|
59
|
+
elif self.mode == ConfigMode.FromBucket:
|
|
60
|
+
self.file_configs = await self.rita_client.get_fileconfigs_from_bucket(self.bucket_name)
|
|
61
|
+
elif self.mode == ConfigMode.ByName:
|
|
62
|
+
self.file_configs = await self.rita_client.get_fileconfig_by_name(self.bucket_name, self.config_name)
|
|
63
|
+
logger.info(f"Loaded {len(self.config_name)} fileconfigs: {self.config_name}")
|
|
64
|
+
|
|
65
|
+
def describe(self) -> str:
|
|
66
|
+
return "Importing SFTP files based on file configs"
|
|
67
|
+
|
|
68
|
+
async def generator(self, i: Any) -> AsyncIterator[RawData]:
|
|
69
|
+
await self.load_file_configs()
|
|
70
|
+
|
|
71
|
+
for config_name, file_config in self.file_configs.items():
|
|
72
|
+
if isinstance(self.ftp_client, dict):
|
|
73
|
+
selected_ftp_client = self.ftp_client[config_name]
|
|
74
|
+
else:
|
|
75
|
+
selected_ftp_client = self.ftp_client
|
|
76
|
+
logger.info(f"Scanning with fileconfig '{config_name}' in directory {file_config.inbound_directory}")
|
|
77
|
+
file_names = list(selected_ftp_client.list_files(file_config.inbound_directory))
|
|
78
|
+
for idx, file_name in enumerate(file_names):
|
|
79
|
+
if self.match_mode == ConfigMatchMode.Exact:
|
|
80
|
+
logger.info(f"Exact Matching file {file_name}")
|
|
81
|
+
if not file_exact_match(file_name, file_config.file_name):
|
|
82
|
+
logger.debug(f"Skipping file {file_name} due to not matching exactly to {file_config.file_name}")
|
|
83
|
+
continue
|
|
84
|
+
elif self.match_mode == ConfigMatchMode.Partial:
|
|
85
|
+
if not file_config.file_name in file_name:
|
|
86
|
+
continue
|
|
87
|
+
elif self.match_mode == ConfigMatchMode.ByExtension:
|
|
88
|
+
if not file_name.endswith(file_config.file_extension):
|
|
89
|
+
continue
|
|
90
|
+
if file_config.date_format != "" and \
|
|
91
|
+
not check_if_file_greater_than_date(file_name, file_config.file_name, file_config.date_format,
|
|
92
|
+
self.min_date, self.strip_trailing_digits):
|
|
93
|
+
logger.debug(f"Skipping file {file_name} due to having date > {self.min_date}")
|
|
94
|
+
continue
|
|
95
|
+
logger.info(f"fetching {idx+1}/{len(file_names)}: {file_name}")
|
|
96
|
+
rd = selected_ftp_client.download_file(str(Path(file_config.inbound_directory) / Path(file_name)))
|
|
97
|
+
self.pipeline_context.file_config = file_config
|
|
98
|
+
yield FileConfigRawData(data=rd.data, file_name=rd.file_name, file_config=file_config)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
async def main():
|
|
103
|
+
s = SFTPFileConfigStep(
|
|
104
|
+
rita_client=GravitateRitaAPI(
|
|
105
|
+
base_url="",
|
|
106
|
+
username="",
|
|
107
|
+
password=""
|
|
108
|
+
),
|
|
109
|
+
ftp_client=FTPIntegrationClient(
|
|
110
|
+
credentials=FTPCredential(
|
|
111
|
+
host="",
|
|
112
|
+
username="",
|
|
113
|
+
password="",
|
|
114
|
+
port=22,
|
|
115
|
+
ftp_type=FTPType.sftp
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
mode=ConfigMode.ByName,
|
|
119
|
+
config_name="my_config"
|
|
120
|
+
)
|
|
121
|
+
async for r in s.generator(None):
|
|
122
|
+
print(r)
|
|
123
|
+
|
|
124
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def file_exact_match(file_name: str, sub_str: str, sep='_') -> bool:
|
|
6
|
+
if not file_name or not sub_str:
|
|
7
|
+
return False
|
|
8
|
+
if not file_name.startswith(sub_str):
|
|
9
|
+
return False
|
|
10
|
+
remainder = file_name[len(sub_str):]
|
|
11
|
+
if not remainder:
|
|
12
|
+
return True
|
|
13
|
+
if remainder.startswith('.'):
|
|
14
|
+
return True
|
|
15
|
+
if remainder.startswith(sep):
|
|
16
|
+
date_part = remainder[1:]
|
|
17
|
+
date_pattern = r'^(\d{4}-\d{2}-\d{2}|\d{2}-\d{2}-\d{4}|\d{8}|\d{4}_\d{2}_\d{2}|' + \
|
|
18
|
+
r'\d{2}_\d{2}_\d{4}|\d{4}\d{2}\d{2})(\.[a-zA-Z0-9]+)?$'
|
|
19
|
+
return bool(re.match(date_pattern, date_part))
|
|
20
|
+
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestFileExactMatch(unittest.TestCase):
|
|
25
|
+
def test_examples_from_requirements(self):
|
|
26
|
+
self.assertFalse(file_exact_match("Gross_contractrack.csv", "contractrack"))
|
|
27
|
+
self.assertTrue(file_exact_match("contractrack.csv", "contractrack"))
|
|
28
|
+
self.assertTrue(file_exact_match("contractrack_2025-04-16.csv", "contractrack"))
|
|
29
|
+
self.assertFalse(file_exact_match("Gross_contractrack_2025-04-16.csv", "contractrack"))
|
|
30
|
+
|
|
31
|
+
def test_valid_date_formats(self):
|
|
32
|
+
self.assertTrue(file_exact_match("contractrack_20250416.csv", "contractrack"))
|
|
33
|
+
self.assertTrue(file_exact_match("contractrack_2025-04-16.csv", "contractrack"))
|
|
34
|
+
self.assertTrue(file_exact_match("contractrack_2025_04_16.csv", "contractrack"))
|
|
35
|
+
|
|
36
|
+
def test_invalid_formats(self):
|
|
37
|
+
# Invalid because there's additional text after the date
|
|
38
|
+
self.assertFalse(file_exact_match("contractrack_2025-04-16_final.csv", "contractrack"))
|
|
39
|
+
|
|
40
|
+
# Invalid because there's text instead of a date
|
|
41
|
+
self.assertFalse(file_exact_match("contractrack_version2.csv", "contractrack"))
|
|
42
|
+
|
|
43
|
+
# Invalid because substring isn't at the start
|
|
44
|
+
self.assertFalse(file_exact_match("prefix_contractrack_2025-04-16.csv", "contractrack"))
|
|
45
|
+
|
|
46
|
+
def test_different_separator(self):
|
|
47
|
+
self.assertTrue(file_exact_match("contractrack-2025-04-16.csv", "contractrack", sep='-'))
|
|
48
|
+
self.assertFalse(file_exact_match("Gross-contractrack-2025-04-16.csv", "contractrack", sep='-'))
|
|
49
|
+
|
|
50
|
+
def test_edge_cases(self):
|
|
51
|
+
self.assertTrue(file_exact_match("contractrack", "contractrack"))
|
|
52
|
+
self.assertFalse(file_exact_match("contractrack_", "contractrack"))
|
|
53
|
+
self.assertFalse(file_exact_match("contractrack_notadate.csv", "contractrack"))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
unittest.main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from bb_integrations_lib.protocols.pipelines import Step, Input, Output
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NullStep(Step[Any, None]):
|
|
7
|
+
"""A step that performs no action. May be useful as the first step in a job pipeline."""
|
|
8
|
+
def __init__(self, *args, **kwargs):
|
|
9
|
+
super().__init__(*args, **kwargs)
|
|
10
|
+
|
|
11
|
+
def describe(self) -> str:
|
|
12
|
+
return "Null Step"
|
|
13
|
+
|
|
14
|
+
async def execute(self, i: Any) -> None:
|
|
15
|
+
return None
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import Dict, Any, Awaitable
|
|
2
|
+
|
|
3
|
+
from bb_integrations_lib.gravitate.pe_api import GravitatePEAPI
|
|
4
|
+
from bb_integrations_lib.mappers.prices.model import Action
|
|
5
|
+
from bb_integrations_lib.protocols.pipelines import Step
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PEIntegrationJobActionStep(Step):
|
|
9
|
+
def __init__(self, pe_client: GravitatePEAPI, action: Action, integration_name: str, source_system_id: str,
|
|
10
|
+
*args, **kwargs):
|
|
11
|
+
super().__init__(*args, **kwargs)
|
|
12
|
+
self.pe_client = pe_client
|
|
13
|
+
self.action = action
|
|
14
|
+
self.integration_name = integration_name
|
|
15
|
+
self.source_system_id = source_system_id
|
|
16
|
+
|
|
17
|
+
def describe(self) -> str:
|
|
18
|
+
return "Start, end or error pricing engine integration job"
|
|
19
|
+
|
|
20
|
+
async def execute(self, _: Any):
|
|
21
|
+
return await self.match_action()
|
|
22
|
+
|
|
23
|
+
async def match_action(self) -> Awaitable:
|
|
24
|
+
match self.action:
|
|
25
|
+
case Action.start:
|
|
26
|
+
return self.pe_client.integration_start
|
|
27
|
+
case Action.stop:
|
|
28
|
+
return self.pe_client.integration_stop
|
|
29
|
+
case Action.error:
|
|
30
|
+
return self.pe_client.integration_error
|
|
31
|
+
case _:
|
|
32
|
+
raise ValueError(f"Unexpected action {self.action}")
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from gcloud.aio.storage import Storage
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from bb_integrations_lib.models.pipeline_structs import BolExportResults
|
|
9
|
+
from bb_integrations_lib.protocols.pipelines import Step
|
|
10
|
+
from bb_integrations_lib.shared.model import RawData, FileConfigRawData
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ArchiveGCSStep(Step):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
gcloud_storage: Storage,
|
|
17
|
+
bucket_path: str,
|
|
18
|
+
field_sep: str = ",",
|
|
19
|
+
content_type: str = "",
|
|
20
|
+
error_on_exists: bool = False,
|
|
21
|
+
*args,
|
|
22
|
+
**kwargs
|
|
23
|
+
) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Archive a file (RawData or BolExportResults) to Google Cloud Storage.
|
|
26
|
+
:param gcloud_storage: A gcloud-aio-storage client.
|
|
27
|
+
:param bucket_path: The bucket and optional directory to upload the file to.
|
|
28
|
+
Example: "my-bucket" or "my-bucket/my-dir/my-dir-2". If a FileConfigRawData is passed to the step, the
|
|
29
|
+
source_system will be appended to the directory, before the file name. (bucket/prefix/source_system/file_name)
|
|
30
|
+
:param field_sep: Field separator when uploading BolExportResults.
|
|
31
|
+
:param content_type: Optional - explicitly specify the content type of the file.
|
|
32
|
+
:param error_on_exists: Whether to raise an exception if the file exists.
|
|
33
|
+
"""
|
|
34
|
+
super().__init__(*args, **kwargs)
|
|
35
|
+
|
|
36
|
+
self.storage = gcloud_storage
|
|
37
|
+
self.bucket = bucket_path
|
|
38
|
+
self.prefix = ""
|
|
39
|
+
self.field_sep = field_sep
|
|
40
|
+
self.content_type = content_type
|
|
41
|
+
self.error_on_exists = error_on_exists
|
|
42
|
+
|
|
43
|
+
if "/" in bucket_path:
|
|
44
|
+
[self.bucket, self.prefix] = bucket_path.split("/", maxsplit=1)
|
|
45
|
+
|
|
46
|
+
def describe(self):
|
|
47
|
+
return "Archiving file in GCS"
|
|
48
|
+
|
|
49
|
+
async def execute(self, i: RawData | FileConfigRawData | BolExportResults) -> RawData:
|
|
50
|
+
def file_path():
|
|
51
|
+
if isinstance(i, FileConfigRawData):
|
|
52
|
+
return os.path.join(self.prefix, i.file_config.source_system, i.file_name)
|
|
53
|
+
else:
|
|
54
|
+
return os.path.join(self.prefix, i.file_name)
|
|
55
|
+
|
|
56
|
+
if isinstance(i, BolExportResults):
|
|
57
|
+
df = pd.DataFrame.from_records(i.orders)
|
|
58
|
+
csv_text = df.to_csv(index=False, sep=self.field_sep)
|
|
59
|
+
# Default to text/csv for BolExportResults if we don't have an explicit content type
|
|
60
|
+
self.content_type = "text/csv" if not self.content_type else self.content_type
|
|
61
|
+
contents = csv_text.encode("utf-8")
|
|
62
|
+
else:
|
|
63
|
+
contents = i.data
|
|
64
|
+
|
|
65
|
+
if await self.storage.get_bucket(self.bucket).blob_exists(file_path()):
|
|
66
|
+
if self.error_on_exists:
|
|
67
|
+
raise Exception(f"File '{self.bucket}/{file_path()}' already exists")
|
|
68
|
+
old_file_name = i.file_name
|
|
69
|
+
i.file_name = f"DUPLICATE_{uuid.uuid4()}_{i.file_name}"
|
|
70
|
+
logger.debug(
|
|
71
|
+
f"Blob named '{old_file_name}' already exists; archiving '{i.file_name}' to GCS (backup attempt)")
|
|
72
|
+
else:
|
|
73
|
+
logger.debug(f"Archiving '{i.file_name}' to GCS")
|
|
74
|
+
await self.storage.upload(self.bucket, file_path(), contents or '', content_type=self.content_type)
|
|
75
|
+
|
|
76
|
+
return i
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import loguru
|
|
6
|
+
|
|
7
|
+
from bb_integrations_lib.protocols.pipelines import Step
|
|
8
|
+
from bb_integrations_lib.provider.ftp.client import FTPIntegrationClient
|
|
9
|
+
from bb_integrations_lib.shared.model import FileConfigRawData, RawData
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ArchiveSFTPStep(Step):
|
|
13
|
+
def __init__(self, ftp_client: FTPIntegrationClient, src_directory: str | None = None,
|
|
14
|
+
archive_directory: str | None = None, *args, **kwargs):
|
|
15
|
+
super().__init__(*args, **kwargs)
|
|
16
|
+
self.ftp_client = ftp_client
|
|
17
|
+
self.src_directory = src_directory
|
|
18
|
+
self.archive_directory = archive_directory
|
|
19
|
+
|
|
20
|
+
def describe(self) -> str:
|
|
21
|
+
return "SFTP Rename Step"
|
|
22
|
+
|
|
23
|
+
async def execute(self, i: Any) -> Any:
|
|
24
|
+
if isinstance(i, FileConfigRawData):
|
|
25
|
+
old_name = os.path.join(i.file_config.inbound_directory, i.file_name)
|
|
26
|
+
new_name = os.path.join(i.file_config.archive_directory, i.file_name)
|
|
27
|
+
elif isinstance(i, RawData):
|
|
28
|
+
if self.src_directory is None or self.archive_directory is None:
|
|
29
|
+
raise RuntimeError("Attempted to archive with a RawData object but src_directory or archive_directory was not provided.")
|
|
30
|
+
old_name = os.path.join(self.src_directory, i.file_name)
|
|
31
|
+
new_name = os.path.join(self.archive_directory, i.file_name)
|
|
32
|
+
else:
|
|
33
|
+
raise NotImplementedError(f"Unsupported input type: {type(i)}")
|
|
34
|
+
try:
|
|
35
|
+
loguru.logger.debug(f"Archiving file {old_name} -> {new_name}")
|
|
36
|
+
self.ftp_client.rename_file(old_name, new_name)
|
|
37
|
+
except Exception as e:
|
|
38
|
+
try:
|
|
39
|
+
loguru.logger.debug(f"Archiving file failed...")
|
|
40
|
+
# this file may already exist. Give the file a randomized name and then archive.
|
|
41
|
+
new_name = new_name.replace(i.file_name, f"DUPLICATE_{uuid.uuid4()}_{i.file_name}")
|
|
42
|
+
loguru.logger.debug(f"Archiving file (backup attempt) {old_name} -> {new_name}")
|
|
43
|
+
self.ftp_client.rename_file(old_name, new_name)
|
|
44
|
+
except:
|
|
45
|
+
loguru.logger.warning(f"Archiving backup file failed. Deleting source file to prevent duplicate readings.")
|
|
46
|
+
self.ftp_client.delete_file(old_name)
|
|
47
|
+
loguru.logger.debug(f"Archived file {old_name} -> {new_name}")
|
|
48
|
+
return i
|