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,364 @@
1
+ import json
2
+ import os
3
+ import uuid
4
+ from contextlib import contextmanager, asynccontextmanager
5
+ from datetime import datetime, UTC
6
+ from functools import lru_cache
7
+ from typing import Optional, Any, Iterable, Generator
8
+ import re
9
+ import loguru
10
+ import yaml
11
+ from bson import ObjectId
12
+ from bson.raw_bson import RawBSONDocument
13
+ from loguru import logger
14
+ from pydantic import ValidationError, BaseModel
15
+ from pymongo import MongoClient
16
+ from pymongo.asynchronous.database import AsyncDatabase
17
+ from pymongo.synchronous.database import Database
18
+
19
+ from bb_integrations_lib.secrets.credential_models import FTPCredential, AWSCredential, GoogleCredential, IMAPCredential
20
+ from bb_integrations_lib.shared.model import CredentialType
21
+
22
+
23
+ class DateTimeEncoder(json.JSONEncoder):
24
+ def default(self, obj):
25
+ if isinstance(obj, datetime):
26
+ return obj.isoformat()
27
+ return super(DateTimeEncoder, self).default(obj)
28
+
29
+ class CustomJSONEncoder(json.JSONEncoder):
30
+ def default(self, obj):
31
+ if isinstance(obj, datetime):
32
+ return obj.isoformat() # Convert datetime to ISO 8601 string
33
+ if isinstance(obj, ObjectId):
34
+ return str(obj)
35
+ if isinstance(obj, BaseModel):
36
+ return obj.model_dump(mode="json")
37
+ return super().default(obj)
38
+
39
+ class ClientConfig(BaseModel):
40
+ client_name: str
41
+ client_url: str
42
+ client_psk: str
43
+ conn_str: str
44
+ account_username: str = 'costco-integration'
45
+
46
+
47
+ def safe_index(file_name: str, sub_str: str, default: int = 1000) -> int:
48
+ """
49
+ :param file_name: file name.
50
+ :param sub_str: sub string.
51
+ :param default: default value.
52
+ :return: integer index of sub_str.
53
+ """
54
+ if sub_str in file_name:
55
+ return file_name.index(sub_str)
56
+ return default
57
+
58
+
59
+ def file_time(file_name: str, sub_str: str, sep='_', strip_trailing: bool = False) -> str:
60
+ """
61
+ :param file_name: file name.
62
+ :param sub_str: sub string.
63
+ :param sep: string separator.
64
+ :param strip_trailing: if True, strips trailing _digits pattern (e.g., _30380357).
65
+ :return: string time of file.
66
+ """
67
+ if sub_str in file_name:
68
+ start = safe_index(file_name=file_name, sub_str=sub_str) + len(sub_str)
69
+ end = file_name.rfind(".")
70
+ result = file_name[start:end]
71
+
72
+ if strip_trailing:
73
+ result = re.sub(r'_\d+$', '', result)
74
+
75
+ if sep in result:
76
+ return result.strip(sep)
77
+ return result
78
+ raise IndexError(f"SubString: {sub_str} not found on file: {file_name}")
79
+
80
+
81
+
82
+ def file_exact_match(file_name: str, sub_str: str, sep='_') -> bool:
83
+ """
84
+ Check if filename follows this pattern:
85
+ - Starts with the exact substring
86
+ - Optionally followed by separator and date
87
+ - Ends with file extension
88
+ - No other content before or after the substring
89
+
90
+ Args:
91
+ file_name: The filename to check
92
+ sub_str: The substring to look for
93
+ sep: The separator character (default '_')
94
+
95
+ Returns:
96
+ bool: True if the pattern matches, False otherwise
97
+ """
98
+ if not file_name or not sub_str:
99
+ return False
100
+ if not file_name.startswith(sub_str):
101
+ return False
102
+ remainder = file_name[len(sub_str):]
103
+ if not remainder:
104
+ return True
105
+ if remainder.startswith('.'):
106
+ return True
107
+ if remainder.startswith(sep):
108
+ date_part = remainder[1:]
109
+ date_pattern = r'^(\d{4}-\d{2}-\d{2}|\d{2}-\d{2}-\d{4}|\d{8}|\d{4}_\d{2}_\d{2}|' + \
110
+ r'\d{2}_\d{2}_\d{4}|\d{4}\d{2}\d{2})(\.[a-zA-Z0-9]+)?$'
111
+
112
+ return bool(re.match(date_pattern, date_part))
113
+
114
+ return False
115
+
116
+
117
+ def check_if_file_greater_than_date(file_name: str, sub_str: str, date_format: str,
118
+ min_date: datetime = datetime.now(UTC),
119
+ strip_trailing: bool = False) -> bool:
120
+ """
121
+ Parses a datetime out of a file name and compares it against the min_date (or current UTC, by default).
122
+
123
+ :param file_name: File name to parse.
124
+ :param sub_str: Prefix to strip, if found.
125
+ :param date_format: strptime-compatible date format specifier.
126
+ :param min_date: Date to compare the parsed date against.
127
+ The timezone provided in this object will be used for the parsed timezone if it is not datetime aware.
128
+ :param strip_trailing: if True, strips trailing _digits pattern (e.g., _30380357).
129
+ :return: True if the date parsed from file_name is newer than the min_date.
130
+ """
131
+ try:
132
+ file_date = file_time(file_name=file_name, sub_str=sub_str, strip_trailing=strip_trailing)
133
+ date = datetime.strptime(file_date, date_format)
134
+ # Check if the parsed tzinfo is naive. If so, assume it's in the same tz as min_date.
135
+ if date.tzinfo is None or date.tzinfo.utcoffset(date) is None:
136
+ date = date.replace(tzinfo=min_date.tzinfo)
137
+ return date > min_date
138
+ except IndexError as e:
139
+ logger.error(f"Error: {e}")
140
+ return False
141
+
142
+
143
+ def find_file_in_parent_directories(filename: str, max_levels: int = 20,
144
+ secrets_folder: Optional[str] = None) -> str | None:
145
+ """
146
+ Searches for a file in the current directory and up to max_levels of parent directories.
147
+
148
+ :param filename: The name of the file to search for.
149
+ :param max_levels: The maximum number of parent directory levels to search.
150
+ :param secrets_folder: Optional parameter defining a secrets folder name.
151
+ :return: The full path to the file if found, else None.
152
+ """
153
+ current_dir = os.getcwd()
154
+ for _ in range(max_levels):
155
+ potential_path = os.path.join(current_dir, filename)
156
+ if os.path.exists(potential_path):
157
+ return potential_path
158
+ # Check in a 'secrets' subdirectory of the current directory.
159
+ if secrets_folder:
160
+ potential_secrets_path = os.path.join(current_dir, secrets_folder, filename)
161
+ if os.path.exists(potential_secrets_path):
162
+ return potential_secrets_path
163
+ current_dir = os.path.dirname(current_dir)
164
+ return None
165
+
166
+
167
+ @lru_cache(maxsize=None)
168
+ def load_credentials(credential_type: str = CredentialType.ftp,
169
+ max_levels: int = 5,
170
+ secrets_folder_name: str = 'secrets') -> FTPCredential | AWSCredential | GoogleCredential | IMAPCredential:
171
+ """
172
+ :param credential_type: credential type.
173
+ :param max_levels: The maximum number of parent directory levels to search for the credentials file.
174
+ :param secrets_folder_name: The name of a secrets folder. Defaults to secrets.
175
+ :return: Dictionary containing the credentials.
176
+ raise:
177
+ - ValueError if credential type is not supported.
178
+ - FileNotFound if path is not found.
179
+ """
180
+ filename = f'{credential_type}.json'
181
+ path = find_file_in_parent_directories(filename, max_levels, secrets_folder_name)
182
+
183
+ if not path:
184
+ logger.error(f"Credentials file not found: {filename}")
185
+ raise FileNotFoundError(f"Credentials file not found: {filename}")
186
+
187
+ with open(path, 'r') as file:
188
+ json_credentials = json.load(file)
189
+
190
+ match credential_type:
191
+ case CredentialType.ftp:
192
+ return FTPCredential(**json_credentials)
193
+ case CredentialType.aws:
194
+ return AWSCredential(**json_credentials)
195
+ case CredentialType.google:
196
+ return GoogleCredential(**json_credentials)
197
+ case CredentialType.imap:
198
+ return IMAPCredential(**json_credentials)
199
+ case _:
200
+ for CredentialModel in (FTPCredential, AWSCredential, GoogleCredential, IMAPCredential):
201
+ try:
202
+ return CredentialModel(**json_credentials)
203
+ except (TypeError, ValidationError):
204
+ continue
205
+ raise TypeError(f'Unable to open: {filename}')
206
+
207
+
208
+ def get_client_config(base_dir: str, config_directory: str = 'deployment_configs',
209
+ client_name: str = 'coleman') -> ClientConfig:
210
+ try:
211
+ dirs = os.listdir(f'{base_dir}/{config_directory}')
212
+ client_dir = [f for f in dirs if f == client_name][0]
213
+ file_path = f"{base_dir}/{config_directory}/{client_dir}/env-cm.yaml"
214
+ with open(file_path, 'r') as file:
215
+ data = yaml.safe_load(file)
216
+ file_data = data['data']
217
+ return ClientConfig(
218
+ client_name=client_name,
219
+ client_url=file_data['BASE_URL'],
220
+ client_psk=file_data['SYSTEM_PSK'],
221
+ conn_str=build_conn_str(file_data['DB_CONNECT_STR']),
222
+ )
223
+ except IndexError:
224
+ loguru.logger.error(f'Unable to find config file for {client_name}')
225
+
226
+
227
+ def build_conn_str(conn_str) -> str:
228
+ """
229
+ Method to build mongo conn strings w/o pri safely
230
+ :param conn_str: original conn string
231
+ :return: formatted conn string
232
+ """
233
+ if 'localhost' in conn_str:
234
+ return conn_str
235
+ # Determine start of cluster name
236
+ start_cluster = safe_index(file_name=conn_str, sub_str="@") + 1
237
+ # Determine end of cluster name
238
+ end_cluster = safe_index(file_name=conn_str, sub_str='pri') - 1
239
+ # Determine start of connection string
240
+ start = 0
241
+ # Determine end of connection string
242
+ end = safe_index(file_name=conn_str, sub_str="@")
243
+ cluster = conn_str[start_cluster:end_cluster]
244
+ left = conn_str[start:end]
245
+ if 'bbdev' in conn_str:
246
+ return f"{left}@{cluster}.4f2iw.gcp.mongodb.net/"
247
+ return f"{left}@{cluster}.z7gyv.mongodb.net/"
248
+
249
+
250
+ def nested_lookup(iterable: Iterable, key_path: str):
251
+ def get_nested_value(item, path):
252
+ parts = path.split('.')
253
+ value = item
254
+ for part in parts:
255
+ if isinstance(value, dict):
256
+ value = value.get(part)
257
+ else:
258
+ value = getattr(value, part, None)
259
+ if value is None:
260
+ return None
261
+ return value
262
+
263
+ return {get_nested_value(i, key_path): i for i in iterable}
264
+
265
+
266
+ def lookup(iterable: Iterable, key: callable):
267
+ return {key(i): i for i in iterable}
268
+
269
+
270
+ @contextmanager
271
+ def init_db(connection_str: str, db_name: str):
272
+ from pymongo import MongoClient
273
+ client = MongoClient(connection_str)
274
+ db = client[db_name]
275
+ yield db
276
+
277
+
278
+ @contextmanager
279
+ def init_db_async(connection_str: str, db_name: str):
280
+ from pymongo import AsyncMongoClient
281
+ client = AsyncMongoClient(connection_str)
282
+ db = client[db_name]
283
+ try:
284
+ yield db
285
+ finally:
286
+ client.close()
287
+
288
+
289
+ def mongo_client(
290
+ connection_string,
291
+ read_preference='primaryPreferred',
292
+ server_timeout_ms=60000,
293
+ socket_timeout_ms=30000,
294
+ connect_timeout_ms=30000,
295
+ **kwargs
296
+ ):
297
+ """
298
+ Args:
299
+ connection_string: MongoDB connection URI
300
+ read_preference: 'primary', 'primaryPreferred', 'secondary', 'secondaryPreferred', 'nearest'
301
+ server_timeout_ms: Server selection timeout
302
+ socket_timeout_ms: Socket timeout
303
+ connect_timeout_ms: Connection timeout
304
+ **kwargs: Any other MongoClient options
305
+ """
306
+ return MongoClient(
307
+ connection_string,
308
+ serverSelectionTimeoutMS=server_timeout_ms,
309
+ socketTimeoutMS=socket_timeout_ms,
310
+ connectTimeoutMS=connect_timeout_ms,
311
+ readPreference=read_preference,
312
+ retryWrites=True,
313
+ retryReads=True,
314
+ **kwargs
315
+ )
316
+
317
+
318
+ def gen_lookup(db: Database, collection_name: str, find_params: dict = None, as_raw: bool = False
319
+ ) -> dict[str, dict | RawBSONDocument]:
320
+ """
321
+ Generate a lookup table mapping the _id field of each document in the MongoDB collection to its data.
322
+ :param db: An already-connected pymongo database.
323
+ :param collection_name: The name of the collection.
324
+ :param find_params: Optional parameters to filter the collection on, in Mongo query format
325
+ (will be passed to collection.find).
326
+ :param as_raw: Return RawBSONDocuments, which are decompressed on-the-fly to save resources, instead of plain dicts.
327
+ :return: A dict where the key is the _id field of each document and the value is the whole document.
328
+ """
329
+ collection = db[collection_name]
330
+ if as_raw:
331
+ collection = collection.with_options(
332
+ codec_options=collection.codec_options.with_options(document_class=RawBSONDocument))
333
+ data = list(collection.find(find_params or {}))
334
+ return {str(o['_id']): o for o in data}
335
+
336
+ class MongoJSONEncoder(json.JSONEncoder):
337
+ def default(self, obj):
338
+ if isinstance(obj, ObjectId):
339
+ return str(obj)
340
+ return super().default(obj)
341
+
342
+
343
+ def is_uuid(s: str):
344
+ """Returns True if string is a valid UUID."""
345
+ try:
346
+ uuid.UUID(s)
347
+ return True
348
+ except ValueError:
349
+ return False
350
+
351
+
352
+ def is_valid_goid(goid: str, prefix: str):
353
+ """Returns true if the string is a valid GOID with the specified prefix."""
354
+ if (not goid.startswith(prefix) or not goid.split(":")[-1].isnumeric()) and not is_uuid(goid):
355
+ return False
356
+ return True
357
+
358
+
359
+
360
+
361
+
362
+ if __name__ == "__main__":
363
+ credentials = load_credentials("google.credentials")
364
+ print(credentials)
File without changes
@@ -0,0 +1,13 @@
1
+ WORKER_BACKGROUND_GROUP = "workers.background"
2
+ """Kafka group where background workers consume cooperatively."""
3
+ WORKER_IMMEDIATE_GROUP = "workers.immediate"
4
+ """Kafka group where immediate workers consume cooperatively."""
5
+ WORKER_CROSSROADS_GROUP = "workers.crossroads"
6
+ """Kafka group where crossroads workers consume cooperatively."""
7
+
8
+ def build_group(namespace: str, group: str) -> str:
9
+ """
10
+ Builds a Kafka group ID string in a consistent way using a namespace (usually rita or rita-test) and a group name.
11
+ See the constants in bb_integrations_lib.workers.groups for group names.
12
+ """
13
+ return f"{namespace}.{group}"
@@ -0,0 +1,50 @@
1
+ from asyncio import Future, wait_for
2
+ from uuid import uuid4
3
+
4
+ from faststream.kafka.fastapi import KafkaRouter, Context
5
+
6
+ from bb_integrations_lib.models.rita.workers import WorkerRequest, WorkerResponse
7
+ from bb_integrations_lib.workers.topics import WORKER_IMMEDIATE_REQUEST, WORKER_IMMEDIATE_RESPONSE, build_topic
8
+
9
+
10
+ class RPCWorker:
11
+ """An async interface for sending awaitable remote procedure calls over Kafka to workers."""
12
+ def __init__(self, router: KafkaRouter, namespace: str) -> None:
13
+ self.responses: dict[str, Future[WorkerResponse]] = {}
14
+ self.reply_topic = build_topic(namespace, WORKER_IMMEDIATE_RESPONSE)
15
+ self.send_topic = build_topic(namespace, WORKER_IMMEDIATE_REQUEST)
16
+
17
+ self.router = router
18
+ self.subscriber = self.router.subscriber(self.reply_topic)
19
+ self.subscriber(self._handle_responses)
20
+
21
+ async def _handle_responses(self, response: WorkerResponse, message = Context("message")) -> None:
22
+ if future := self.responses.pop(message.correlation_id, None):
23
+ future.set_result(response)
24
+
25
+ async def request(
26
+ self,
27
+ runnable_name: str,
28
+ tenant_name: str,
29
+ send_topic: str | None = None,
30
+ reply_topic: str | None = None,
31
+ runnable_kwargs: dict | None = None,
32
+ timeout: float = 30.0,
33
+ ) -> WorkerResponse:
34
+ """Send a request to be executed on a worker, but wait for the response with a configurable timeout."""
35
+ send_topic = send_topic or self.send_topic
36
+ reply_topic = reply_topic or self.reply_topic
37
+
38
+ correlation_id = str(uuid4())
39
+ future = self.responses[correlation_id] = Future[WorkerResponse]()
40
+
41
+ # Use an originator of "other" so that status reports don't get reported to the backend task tracker.
42
+ request = WorkerRequest(runnable_name=runnable_name, tenant_name=tenant_name, originator="other", runnable_kwargs=runnable_kwargs)
43
+ await self.router.broker.publish(request, topic=send_topic, reply_to=reply_topic, correlation_id=correlation_id)
44
+
45
+ try:
46
+ response: WorkerResponse= await wait_for(future, timeout=timeout)
47
+ except TimeoutError:
48
+ _ = self.responses.pop(correlation_id, None)
49
+ raise
50
+ return response
@@ -0,0 +1,20 @@
1
+ WORKER_BACKGROUND_REQUEST = "worker.background.request"
2
+ """The Kafka topic that is used to send requests to background workers."""
3
+ WORKER_BACKGROUND_RESPONSE = "worker.background.response"
4
+ """The Kafka topic that background workers will use to reply with status updates."""
5
+ WORKER_IMMEDIATE_REQUEST = "worker.immediate.request"
6
+ """The Kafka topic that immediate (semi-synchronous / RPC) worker requests are sent to."""
7
+ WORKER_IMMEDIATE_RESPONSE = "worker.immediate.response"
8
+ """The Kafka topic that immediate (semi-synchronous / RPC) worker responses are sent to."""
9
+ WORKER_CROSSROADS_REQUEST = "worker.crossroads.request"
10
+ """The Kafka topic that crossroads worker requests are sent to."""
11
+ WORKER_CROSSROADS_RESPONSE = "worker.crossroads.response"
12
+ """The Kafka topic that crossroads workers will send responses to."""
13
+
14
+
15
+ def build_topic(namespace: str, topic: str) -> str:
16
+ """
17
+ Builds a Kafka topic string in a consistent way using a namespace (usually rita or rita-test) and a topic name.
18
+ See the constants in bb_integrations_lib.workers.topics for topic names.
19
+ """
20
+ return f"{namespace}.{topic}"
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.3
2
+ Name: bb-integrations-library
3
+ Version: 3.0.11
4
+ Summary: Provides common logic for all types of integration jobs.
5
+ Author: Alejandro Jordan, Ben Allen, Nicholas De Nova
6
+ Author-email: Alejandro Jordan <ajordan@capspire.com>, Ben Allen <ben.allen@capspire.com>, Nicholas De Nova <nicholas.denova@gravitate.energy>
7
+ Requires-Dist: boto3
8
+ Requires-Dist: email-validator
9
+ Requires-Dist: fastapi
10
+ Requires-Dist: google-cloud-run
11
+ Requires-Dist: google-cloud-secret-manager
12
+ Requires-Dist: google-cloud-storage
13
+ Requires-Dist: google-cloud-tasks
14
+ Requires-Dist: httpx
15
+ Requires-Dist: loguru
16
+ Requires-Dist: openpyxl
17
+ Requires-Dist: pandas
18
+ Requires-Dist: pydantic
19
+ Requires-Dist: pymongo
20
+ Requires-Dist: python-dotenv
21
+ Requires-Dist: sqlalchemy
22
+ Requires-Dist: pyodbc
23
+ Requires-Dist: more-itertools
24
+ Requires-Dist: async-lru
25
+ Requires-Dist: pydantic-xml[lxml]>=2.17.0
26
+ Requires-Dist: tenacity>=9.1.2
27
+ Requires-Dist: faststream[kafka]>=0.5.42
28
+ Requires-Dist: uv-build>=0.7.19
29
+ Requires-Dist: datamodel-code-generator>=0.31.2
30
+ Requires-Dist: paramiko>=3.5.1
31
+ Requires-Dist: pandas-gbq>=0.29.2
32
+ Requires-Dist: polars>=1.35.1
33
+ Requires-Dist: onepasswordconnectsdk>=2.0.0
34
+ Requires-Dist: gcloud-aio-storage>=9.6.1
35
+ Requires-Dist: injegg>=0.1.3
36
+ Requires-Python: >=3.11
37
+ Description-Content-Type: text/markdown
38
+
39
+ # BB Integrations Library
40
+
41
+ A standard integrations library designed for **Gravitate** to manage and interact with various external services.
42
+
43
+ ## Installation
44
+
45
+ Using pip:
46
+ ```bash
47
+ pip install bb-integrations-library
48
+ ```
49
+
50
+ Using uv:
51
+ ```bash
52
+ uv add bb-integrations-library
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ```python
58
+ import bb_integrations_lib
59
+ ```