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,391 @@
1
+ import json
2
+ from loguru import logger
3
+ from typing import Dict, Optional, Any
4
+
5
+ from bb_integrations_lib.mappers.prices.protocol import PriceMapperProtocol
6
+ from bb_integrations_lib.shared.model import File
7
+ from bb_integrations_lib.util.config.model import GlobalConfig, Configs, Config, ClientConstructor, Client
8
+ from bb_integrations_lib.provider.gcp.cloud_secrets.client import CloudSecretsClient
9
+
10
+
11
+ # Base Exception class for GlobalConfigManager
12
+ class GlobalConfigException(Exception):
13
+ """Base exception class for GlobalConfigManager operations."""
14
+ pass
15
+
16
+
17
+ # Environment-related exceptions
18
+ class EnvironmentException(GlobalConfigException):
19
+ """Base exception class for environment-related operations."""
20
+ pass
21
+
22
+
23
+ class EnvironmentAlreadyExistsException(EnvironmentException):
24
+ """Raised when trying to create an environment that already exists."""
25
+ pass
26
+
27
+
28
+ class EnvironmentDoesNotExistException(EnvironmentException):
29
+ """Raised when trying to access an environment that does not exist."""
30
+ pass
31
+
32
+
33
+ # Config-related exceptions
34
+ class ConfigException(GlobalConfigException):
35
+ """Base exception class for configuration-related operations."""
36
+ pass
37
+
38
+
39
+ class ConfigAlreadyExistsException(ConfigException):
40
+ """Raised when trying to create a config that already exists."""
41
+ pass
42
+
43
+
44
+ class ConfigDoesNotExistException(ConfigException):
45
+ """Raised when trying to access a config that does not exist."""
46
+ pass
47
+
48
+
49
+ class GlobalConfigManager:
50
+ """Manages global configurations for clients/customers stored in Google Cloud Secret Manager.
51
+
52
+ This class provides an interface to create, retrieve, update, and delete
53
+ client-specific configuration data in a centralized location. Each client
54
+ (referred to as an "environment") has its own dedicated configuration space
55
+ within a single Google Cloud Secret.
56
+
57
+ All environment names are automatically converted to lowercase for consistency.
58
+ """
59
+
60
+ def __init__(self, file_name_override: Optional[str] = None):
61
+ """Initialize the manager with a file name.
62
+
63
+ Args:
64
+ file_name_override: Custom file name for the configuration.
65
+ Defaults to "global_integration_configurations".
66
+ """
67
+ self.file_name = file_name_override or "global_integration_configurations"
68
+
69
+ def environment_from_name(self, environment_name: str, env_mode: str, sd_basic_auth: bool = False) -> ClientConstructor:
70
+ from bb_integrations_lib.gravitate.rita_api import GravitateRitaAPI
71
+ from bb_integrations_lib.gravitate.sd_api import GravitateSDAPI
72
+ from bb_integrations_lib.gravitate.pe_api import GravitatePEAPI
73
+ from bb_integrations_lib.util.config.model import GlobalConfig, ClientConstructor, Client
74
+ environment: GlobalConfig = self.get_environment(environment_name)
75
+ test_environment = environment.test
76
+ production_environment = environment.prod
77
+ if env_mode.lower() in ["development", "test"]:
78
+ env = test_environment
79
+ else:
80
+ env = production_environment
81
+
82
+ if sd_basic_auth:
83
+ sd_api_client = GravitateSDAPI(
84
+ base_url=env.sd.base_url,
85
+ username=env.sd.username,
86
+ password=env.sd.password,
87
+ )
88
+ else:
89
+ sd_api_client = GravitateSDAPI(
90
+ base_url=env.sd.base_url,
91
+ client_id=env.sd.client_id,
92
+ client_secret=env.sd.client_secret,
93
+ )
94
+
95
+ return ClientConstructor(
96
+ rita=Client(
97
+ config=env.rita,
98
+ api_client=GravitateRitaAPI(
99
+ tenant=environment_name,
100
+ client_id=env.rita.client_id,
101
+ client_secret=env.rita.client_secret,
102
+ )
103
+ ),
104
+ sd=Client(
105
+ config=env.sd,
106
+ api_client=sd_api_client
107
+ ),
108
+ pe=Client(
109
+ config=env.pe,
110
+ api_client=GravitatePEAPI(base_url=env.pe.base_url,
111
+ username=env.pe.username,
112
+ password=env.pe.password,
113
+ )
114
+ )
115
+
116
+ )
117
+
118
+ def get_secret(self, project_id_override: Optional[str] = None) -> Dict[str, Any]:
119
+ """Retrieve the entire secret from Cloud Secret Manager.
120
+
121
+ Gets the complete configuration containing all client/customer environments.
122
+
123
+ Args:
124
+ project_id_override: Optional project ID to use instead of the default.
125
+
126
+ Returns:
127
+ Dictionary containing the complete secret data with all client configurations.
128
+
129
+ Raises:
130
+ ConfigDoesNotExistException: If the secret does not exist.
131
+ GlobalConfigException: If there's an error retrieving the secret.
132
+ """
133
+ try:
134
+ secret = CloudSecretsClient.get_file(path=self.file_name, file_name=self.file_name,
135
+ project_id=project_id_override)
136
+ if not secret:
137
+ raise ConfigDoesNotExistException(f"Config file '{self.file_name}' does not exist")
138
+ data = secret.data
139
+ return json.loads(data)
140
+ except ConfigDoesNotExistException:
141
+ logger.error(f"Config file '{self.file_name}' does not exist")
142
+ raise
143
+ except Exception as e:
144
+ logger.error(f"Failed to retrieve secret {self.file_name}: {str(e)}")
145
+ raise GlobalConfigException(f"Error retrieving secret: {str(e)}")
146
+
147
+ def get_environment(self, environment_name: str, project_id_override: Optional[str] = None) -> GlobalConfig:
148
+ """Retrieve configuration for a specific client/customer.
149
+
150
+ Gets the configuration data for a single environment (client/customer).
151
+ Environment names are case-insensitive and will be converted to lowercase.
152
+
153
+ Args:
154
+ environment_name: Name of the client/customer environment.
155
+ project_id_override: Optional project ID to use instead of the default.
156
+
157
+ Returns:
158
+ GlobalConfig object containing the configuration for the specified environment.
159
+
160
+ Raises:
161
+ EnvironmentDoesNotExistException: If the environment does not exist.
162
+ GlobalConfigException: If there's an error retrieving the secret.
163
+ """
164
+ environment_name = environment_name.lower()
165
+ try:
166
+ secret = self.get_secret(project_id_override)
167
+ if environment_name not in secret:
168
+ raise EnvironmentDoesNotExistException(f"Environment '{environment_name}' does not exist")
169
+ return GlobalConfig.model_validate(secret[environment_name])
170
+ except ConfigDoesNotExistException:
171
+ raise
172
+ except EnvironmentDoesNotExistException:
173
+ logger.error(f"Environment '{environment_name}' does not exist")
174
+ raise
175
+ except Exception as e:
176
+ logger.error(f"Error getting environment '{environment_name}': {str(e)}")
177
+ raise GlobalConfigException(f"Error retrieving environment '{environment_name}': {str(e)}")
178
+
179
+ def dict_has_attribute(self, dictionary: Dict, attribute_name: str) -> bool:
180
+ """Check if a dictionary has a specific key.
181
+
182
+ Performs a case-insensitive check by converting the attribute name to lowercase.
183
+
184
+ Args:
185
+ dictionary: The dictionary to check.
186
+ attribute_name: The key name to look for.
187
+
188
+ Returns:
189
+ True if the key exists, False otherwise.
190
+ """
191
+ attribute_name = attribute_name.lower()
192
+ return attribute_name in dictionary
193
+
194
+ def create_new_config(self, config: Dict[str, Any], project_id_override: Optional[str] = None):
195
+ """Create a completely new configuration, replacing the entire secret.
196
+
197
+ Use with caution as this replaces all existing client configurations.
198
+
199
+ Args:
200
+ config: Complete configuration dictionary to store.
201
+ project_id_override: Optional project ID to use instead of the default.
202
+
203
+ Raises:
204
+ GlobalConfigException: If there's an error uploading the configuration.
205
+ """
206
+ try:
207
+ self._upload_config(config_data=config, project_id_override=project_id_override)
208
+ except Exception as e:
209
+ logger.error(f"Failed to create new config: {str(e)}")
210
+ raise GlobalConfigException(f"Failed to create new config: {str(e)}")
211
+
212
+ def create_new_environment(self, environment_name: str, config: GlobalConfig,
213
+ project_id_override: Optional[str] = None) -> None:
214
+ """Create a new configuration for a client/customer.
215
+
216
+ Adds a new client-specific configuration to the existing secret.
217
+ Environment names are case-insensitive and will be converted to lowercase.
218
+
219
+ Args:
220
+ environment_name: Name of the client/customer environment.
221
+ config: Configuration object to store.
222
+ project_id_override: Optional project ID to use instead of the default.
223
+
224
+ Raises:
225
+ EnvironmentAlreadyExistsException: If an environment with the given name already exists.
226
+ GlobalConfigException: For any other errors during creation.
227
+ """
228
+ environment_name = environment_name.lower()
229
+ try:
230
+ secret_dict = self.get_secret(project_id_override=project_id_override)
231
+ if self.dict_has_attribute(secret_dict, environment_name):
232
+ raise EnvironmentAlreadyExistsException(f"Environment '{environment_name}' already exists")
233
+ secret_dict[environment_name] = config.model_dump()
234
+ self._upload_config(secret_dict)
235
+ except EnvironmentAlreadyExistsException:
236
+ logger.error(f"Environment '{environment_name}' already exists")
237
+ raise
238
+ except ConfigDoesNotExistException:
239
+ # If the config doesn't exist yet, create a new one with just this environment
240
+ try:
241
+ new_config = {environment_name: config.model_dump()}
242
+ self._upload_config(new_config, project_id_override)
243
+ except Exception as e:
244
+ logger.error(f"Failed to create new environment '{environment_name}': {str(e)}")
245
+ raise GlobalConfigException(f"Failed to create new environment '{environment_name}': {str(e)}")
246
+ except Exception as e:
247
+ logger.error(f"Failed to create new environment '{environment_name}': {str(e)}")
248
+ raise GlobalConfigException(f"Failed to create new environment '{environment_name}': {str(e)}")
249
+
250
+ def overwrite_config(self, environment_name: str, config: GlobalConfig,
251
+ project_id_override: Optional[str] = None) -> None:
252
+ """Overwrite an existing client/customer configuration.
253
+
254
+ Updates the configuration for an existing environment.
255
+ Environment names are case-insensitive and will be converted to lowercase.
256
+
257
+ Args:
258
+ environment_name: Name of the client/customer environment to update.
259
+ config: New configuration object.
260
+ project_id_override: Optional project ID to use instead of the default.
261
+
262
+ Raises:
263
+ EnvironmentDoesNotExistException: If the specified environment doesn't exist.
264
+ GlobalConfigException: For any other errors during update.
265
+ """
266
+ environment_name = environment_name.lower()
267
+ try:
268
+ secret_dict = self.get_secret(project_id_override=project_id_override)
269
+ if not self.dict_has_attribute(secret_dict, environment_name):
270
+ raise EnvironmentDoesNotExistException(f"Environment '{environment_name}' does not exist")
271
+ secret_dict[environment_name] = config.model_dump()
272
+ self._upload_config(secret_dict)
273
+ except EnvironmentDoesNotExistException:
274
+ logger.error(f"Environment '{environment_name}' does not exist")
275
+ raise
276
+ except Exception as e:
277
+ logger.error(f"Failed to overwrite environment '{environment_name}': {str(e)}")
278
+ raise GlobalConfigException(f"Failed to overwrite environment '{environment_name}': {str(e)}")
279
+
280
+ def delete_config(self, environment_name: str, project_id_override: Optional[str] = None) -> None:
281
+ """Delete a configuration for a client/customer.
282
+
283
+ Removes a client-specific configuration from the secret.
284
+ Environment names are case-insensitive and will be converted to lowercase.
285
+
286
+ Args:
287
+ environment_name: Name of the client/customer environment to delete.
288
+ project_id_override: Optional project ID to use instead of the default.
289
+
290
+ Raises:
291
+ EnvironmentDoesNotExistException: If the specified environment doesn't exist.
292
+ GlobalConfigException: For any other errors during deletion.
293
+ """
294
+ environment_name = environment_name.lower()
295
+ try:
296
+ secret_dict = self.get_secret(project_id_override=project_id_override)
297
+ if not self.dict_has_attribute(secret_dict, environment_name):
298
+ raise EnvironmentDoesNotExistException(f"Environment '{environment_name}' does not exist")
299
+ del secret_dict[environment_name]
300
+ self._upload_config(secret_dict)
301
+ except EnvironmentDoesNotExistException:
302
+ logger.error(f"Environment '{environment_name}' does not exist")
303
+ raise
304
+ except Exception as e:
305
+ logger.error(f"Failed to delete environment '{environment_name}': {str(e)}")
306
+ raise GlobalConfigException(f"Failed to delete environment '{environment_name}': {str(e)}")
307
+
308
+ def _upload_config(self, config_data: Dict[str, Any], project_id_override: Optional[str] = None) -> None:
309
+ """Helper method to upload configuration to Cloud Secret Manager.
310
+
311
+ Handles the serialization and upload of configuration data to the secret.
312
+ Keep in mind that Google Cloud Secret Manager has a 64 KiB size limitation.
313
+
314
+ Args:
315
+ config_data: Configuration data to upload.
316
+ project_id_override: Optional project ID to use instead of the default.
317
+
318
+ Raises:
319
+ GlobalConfigException: If there's an error uploading the configuration.
320
+ """
321
+ try:
322
+ file = File(file_name=self.file_name, file_data=config_data, project_id=project_id_override)
323
+ CloudSecretsClient.upload_file(path="", file=file)
324
+ logger.info(f"Successfully updated configuration '{self.file_name}'")
325
+ except Exception as e:
326
+ logger.error(f"Failed to upload configuration '{self.file_name}': {str(e)}")
327
+ raise GlobalConfigException(f"Failed to upload configuration: {str(e)}")
328
+
329
+
330
+ if __name__ == "__main__":
331
+ gg = GlobalConfigManager()
332
+ dd = gg.environment_from_name("Loves", "test")
333
+ print(dd)
334
+ config = GlobalConfig(
335
+ prod=Configs(
336
+ rita=Config(
337
+ client_id=None,
338
+ client_secret=None,
339
+ psk=None,
340
+ username=None,
341
+ password=None,
342
+ base_url=None,
343
+ ),
344
+ sd=Config(
345
+ client_id=None,
346
+ client_secret=None,
347
+ psk=None,
348
+ username=None,
349
+ password=None,
350
+ base_url=None,
351
+ ),
352
+ pe=Config(
353
+ client_id=None,
354
+ client_secret=None,
355
+ psk=None,
356
+ username=None,
357
+ password=None,
358
+ base_url=None,
359
+ )
360
+ ),
361
+ test=Configs(
362
+ rita=Config(
363
+ client_id=None,
364
+ client_secret=None,
365
+ psk=None,
366
+ username=None,
367
+ password=None,
368
+ base_url=None,
369
+ ),
370
+ sd=Config(
371
+ client_id=None,
372
+ client_secret=None,
373
+ psk=None,
374
+ username=None,
375
+ password=None,
376
+ base_url=None,
377
+ ),
378
+ pe=Config(
379
+ client_id=None,
380
+ client_secret=None,
381
+ psk=None,
382
+ username=None,
383
+ password=None,
384
+ base_url=None,
385
+ )
386
+ ),
387
+ extra_data={
388
+ "name": "loves"
389
+ }
390
+ )
391
+ # gg.create_new_environment(environment_name="loves", config=config)
@@ -0,0 +1,41 @@
1
+ from typing import Optional, Dict
2
+
3
+ from bb_integrations_lib.gravitate.base_api import BaseAPI
4
+ from pydantic import BaseModel, Field, ConfigDict
5
+
6
+
7
+ class Config(BaseModel):
8
+ client_id: Optional[str] = None
9
+ client_secret: Optional[str] = None
10
+ psk: Optional[str] = None
11
+ username: Optional[str] = None
12
+ password: Optional[str] = None
13
+ base_url: Optional[str] = None
14
+ mongo_conn_str: Optional[str] = None
15
+ mongo_db_name: Optional[str] = None
16
+ extra_data: Optional[Dict] = Field(default_factory=dict)
17
+
18
+
19
+ class Configs(BaseModel):
20
+ rita: Optional[Config] = Field(default_factory=Config)
21
+ sd: Optional[Config] = Field(default_factory=Config)
22
+ pe: Optional[Config] = Field(default_factory=Config)
23
+ crossroads: Optional[Config] = Field(default_factory=Config)
24
+
25
+
26
+ class GlobalConfig(BaseModel):
27
+ prod: Configs = Field(default_factory=Configs)
28
+ test: Configs = Field(default_factory=Configs)
29
+ extra_data: Optional[Dict] = Field(default_factory=dict)
30
+
31
+
32
+ class Client(BaseModel):
33
+ model_config = ConfigDict(arbitrary_types_allowed=True)
34
+ config: Config
35
+ api_client: BaseAPI
36
+
37
+
38
+ class ClientConstructor(BaseModel):
39
+ rita:Client
40
+ sd: Client
41
+ pe: Client
File without changes
@@ -0,0 +1,146 @@
1
+ import functools
2
+ import traceback
3
+ from typing import Dict, Any, Callable, Optional
4
+ import json
5
+ from datetime import datetime
6
+
7
+
8
+ class ExceptionLogger:
9
+ """
10
+ A class that provides a decorator for catching and logging exceptions.
11
+ """
12
+
13
+ def __init__(self):
14
+ """Initialize a new exception logger with an empty log."""
15
+ self._exception_log: Dict[str, Any] = {}
16
+
17
+ def catch_exceptions(self, identifier_param: Optional[str] = None):
18
+ """
19
+ Decorator that catches exceptions and logs them to this logger's dictionary.
20
+
21
+ Args:
22
+ identifier_param (str, optional): The parameter name to use as the identifier in the log.
23
+ If None, will use function name + args as identifier.
24
+ """
25
+
26
+ def decorator(func: Callable) -> Callable:
27
+ @functools.wraps(func)
28
+ def wrapper(*args, **kwargs):
29
+ # Determine the identifier
30
+ identifier = self._get_identifier(func, args, kwargs, identifier_param)
31
+
32
+ try:
33
+ return func(*args, **kwargs)
34
+ except Exception as e:
35
+ # Log the exception
36
+ self._exception_log[identifier] = {
37
+ 'timestamp': datetime.now().isoformat(),
38
+ 'exception_type': type(e).__name__,
39
+ 'exception_message': str(e),
40
+ 'traceback': traceback.format_exc(),
41
+ 'function': func.__name__,
42
+ 'args': self._safe_repr(args),
43
+ 'kwargs': self._safe_repr(kwargs)
44
+ }
45
+ # Re-raise the exception
46
+ raise
47
+
48
+ return wrapper
49
+
50
+ return decorator
51
+
52
+ def _get_identifier(self, func: Callable, args: tuple, kwargs: dict, identifier_param: Optional[str]) -> str:
53
+ """
54
+ Get an identifier for the function call based on the provided parameters.
55
+
56
+ Args:
57
+ func: The function being called
58
+ args: Positional arguments
59
+ kwargs: Keyword arguments
60
+ identifier_param: Parameter name to use as identifier
61
+
62
+ Returns:
63
+ str: An identifier for this function call
64
+ """
65
+ if identifier_param and identifier_param in kwargs:
66
+ return str(kwargs[identifier_param])
67
+
68
+ if identifier_param and args:
69
+ # Try to find the position of the identifier parameter
70
+ try:
71
+ param_names = func.__code__.co_varnames[:func.__code__.co_argcount]
72
+ if identifier_param in param_names:
73
+ idx = param_names.index(identifier_param)
74
+ if idx < len(args):
75
+ return str(args[idx])
76
+ except (AttributeError, IndexError):
77
+ pass
78
+
79
+ # Generate a unique identifier using function name and a hash of arguments
80
+ arg_str = self._safe_repr(args)
81
+ kwarg_str = self._safe_repr(kwargs)
82
+ return f"{func.__name__}_{hash(arg_str + kwarg_str)}_{datetime.now().timestamp()}"
83
+
84
+ def _safe_repr(self, obj: Any) -> str:
85
+ """
86
+ Create a safe string representation of an object.
87
+
88
+ Args:
89
+ obj: The object to represent
90
+
91
+ Returns:
92
+ str: A string representation of the object
93
+ """
94
+ try:
95
+ return str(obj)
96
+ except Exception:
97
+ return f"<unprintable object of type {type(obj).__name__}>"
98
+
99
+ def get_log(self) -> Dict[str, Any]:
100
+ """
101
+ Get the current exception log.
102
+
103
+ Returns:
104
+ Dict[str, Any]: The exception log
105
+ """
106
+ return self._exception_log.copy()
107
+
108
+ def clear_log(self) -> None:
109
+ """Clear the exception log."""
110
+ self._exception_log.clear()
111
+
112
+ def get_formatted_log(self, indent: int = 2) -> str:
113
+ """
114
+ Get the exception log formatted as JSON.
115
+
116
+ Args:
117
+ indent (int): Number of spaces for JSON indentation
118
+
119
+ Returns:
120
+ str: Formatted JSON string
121
+ """
122
+ return json.dumps(self._exception_log, indent=indent)
123
+
124
+ def get_exception_count(self) -> int:
125
+ """
126
+ Get the number of exceptions logged.
127
+
128
+ Returns:
129
+ int: Number of exceptions
130
+ """
131
+ return len(self._exception_log)
132
+
133
+ def get_exceptions_by_type(self) -> Dict[str, int]:
134
+ """
135
+ Get a count of exceptions grouped by exception type.
136
+
137
+ Returns:
138
+ Dict[str, int]: Counts of each exception type
139
+ """
140
+ counts: Dict[str, int] = {}
141
+ for exc_info in self._exception_log.values():
142
+ exc_type = exc_info.get('exception_type', 'Unknown')
143
+ counts[exc_type] = counts.get(exc_type, 0) + 1
144
+ return counts
145
+
146
+
@@ -0,0 +1,114 @@
1
+ from exception_logger import ExceptionLogger
2
+
3
+ # Create a logger instance
4
+ logger = ExceptionLogger()
5
+
6
+
7
+ # Example class using the exception logger
8
+ class DataProcessor:
9
+ def __init__(self, logger):
10
+ self.logger = logger
11
+ self.processed_count = 0
12
+
13
+ @logger.catch_exceptions(identifier_param='record_id')
14
+ def process_record(self, record_id, data):
15
+ """Process a single data record"""
16
+ if not data:
17
+ raise ValueError(f"No data provided for record {record_id}")
18
+
19
+ if 'amount' not in data:
20
+ raise KeyError(f"Missing required field 'amount' in record {record_id}")
21
+
22
+ # Process the record
23
+ result = data['amount'] * 2
24
+ self.processed_count += 1
25
+ return result
26
+
27
+
28
+ # Multiple loggers for different components
29
+ api_logger = ExceptionLogger()
30
+ db_logger = ExceptionLogger()
31
+
32
+
33
+ # Example API function using a different logger
34
+ @api_logger.catch_exceptions()
35
+ def api_request(endpoint, payload=None):
36
+ if not endpoint:
37
+ raise ValueError("Endpoint cannot be empty")
38
+
39
+ if endpoint == "/error":
40
+ raise ConnectionError("Simulated API error")
41
+
42
+ return {"status": "success", "data": payload}
43
+
44
+
45
+ # Example database function using yet another logger
46
+ @db_logger.catch_exceptions(identifier_param='query_id')
47
+ def execute_query(query_id, sql, parameters=None):
48
+ if not sql:
49
+ raise ValueError("SQL query cannot be empty")
50
+
51
+ if "DROP TABLE" in sql.upper():
52
+ raise PermissionError("DROP TABLE operations are not allowed")
53
+
54
+ return [{"id": 1, "name": "Example"}]
55
+
56
+
57
+ # Main demonstration
58
+ def main():
59
+ print("=== Data Processing Example ===")
60
+ processor = DataProcessor(logger)
61
+ records = [
62
+ {'id': '001', 'data': {'amount': 100}},
63
+ {'id': '002', 'data': {}}, # Missing 'amount' field
64
+ {'id': '003', 'data': None}, # None data
65
+ {'id': '004', 'data': {'amount': 200}}
66
+ ]
67
+
68
+ # Process each record and handle exceptions
69
+ for record in records:
70
+ try:
71
+ result = processor.process_record(record['id'], record['data'])
72
+ print(f"Record {record['id']} processed successfully: {result}")
73
+ except Exception as e:
74
+ print(f"Error processing record {record['id']}: {e}")
75
+
76
+ print("\n=== API Example ===")
77
+ endpoints = ["/users", "/error", ""]
78
+ for endpoint in endpoints:
79
+ try:
80
+ result = api_request(endpoint, {"user": "test"})
81
+ print(f"API request to {endpoint} succeeded: {result}")
82
+ except Exception as e:
83
+ print(f"API request to {endpoint} failed: {e}")
84
+
85
+ print("\n=== Database Example ===")
86
+ queries = [
87
+ ("Q1", "SELECT * FROM users", None),
88
+ ("Q2", "", None), # Empty query
89
+ ("Q3", "DROP TABLE users", None) # Not allowed
90
+ ]
91
+
92
+ for query_id, sql, params in queries:
93
+ try:
94
+ result = execute_query(query_id, sql, params)
95
+ print(f"Query {query_id} executed successfully: {result}")
96
+ except Exception as e:
97
+ print(f"Query {query_id} failed: {e}")
98
+
99
+ # Print summary information
100
+ print("\n=== Exception Logs ===")
101
+ print("Data Processing Exceptions:", logger.get_exception_count())
102
+ print("Exception Types:", logger.get_exceptions_by_type())
103
+ print("\nAPI Exceptions:", api_logger.get_exception_count())
104
+ print("Exception Types:", api_logger.get_exceptions_by_type())
105
+ print("\nDatabase Exceptions:", db_logger.get_exception_count())
106
+ print("Exception Types:", db_logger.get_exceptions_by_type())
107
+
108
+ # Print detailed logs
109
+ print("\n=== Detailed Data Processing Exception Log ===")
110
+ print(logger.get_formatted_log())
111
+
112
+
113
+ if __name__ == "__main__":
114
+ main()