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,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}")
@@ -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