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,552 @@
1
+ import json
2
+ import pprint
3
+ from json import JSONDecodeError
4
+
5
+ import httpx
6
+ from async_lru import alru_cache
7
+ from httpx import HTTPStatusError
8
+ from requests import Response
9
+
10
+ from bb_integrations_lib.models.rita.bucket import Bucket
11
+ from bb_integrations_lib.models.rita.crossroads_mapping import CrossroadsMapping, CrossroadsMappingResult, \
12
+ MappingRequest
13
+ from bb_integrations_lib.models.rita.crossroads_monitoring import CrossroadsMappingError, CrossroadsError
14
+ from bb_integrations_lib.models.rita.reference_data import ReferenceDataMapping
15
+ from typing import Optional, Dict, Union, List, runtime_checkable, Protocol, Self
16
+
17
+ import loguru
18
+ from pydantic import ValidationError, BaseModel
19
+
20
+ from bb_integrations_lib.gravitate.base_api import BaseAPI
21
+ from bb_integrations_lib.secrets import RITACredential
22
+ from bb_integrations_lib.util.utils import CustomJSONEncoder
23
+ from bb_integrations_lib.models.probe.resume_token import ResumeToken
24
+ from bb_integrations_lib.models.rita.audit import CreateReportV2, ProcessReportBaseV2, UpdateReportV2
25
+ from bb_integrations_lib.models.rita.config import FileConfig, Config, GenericConfig, ConfigType, MaxSync
26
+ from bb_integrations_lib.models.rita.email import EmailData
27
+ from bb_integrations_lib.models.rita.issue import IssueBase, UpdateIssue
28
+ from bb_integrations_lib.models.rita.mapping import Map
29
+ from bb_integrations_lib.models.rita.probe import ProbeConfig
30
+ from bb_integrations_lib.protocols.flat_file import TankReading
31
+ from bb_integrations_lib.shared.model import CreateProcess
32
+ from bb_integrations_lib.util.config.model import Config as GConfig
33
+
34
+ def catch_exceptions(func):
35
+ def wrapper(*args, **kwargs):
36
+ try:
37
+ return func(*args, **kwargs)
38
+ except httpx.HTTPStatusError as e:
39
+ loguru.logger.error(f"HTTP status error: {e}")
40
+ raise e
41
+ except httpx.ConnectError as e:
42
+ loguru.logger.error(f"Connection error: {e}")
43
+ raise e
44
+ except httpx.ReadTimeout as e:
45
+ loguru.logger.error(f"Read timeout: {e}")
46
+ raise e
47
+ except Exception as e:
48
+ loguru.logger.error(f"Unexpected error: {e}")
49
+ raise e
50
+ return wrapper
51
+
52
+ @runtime_checkable
53
+ class RitaBackendAPI(Protocol):
54
+ async def get_mappings(self, source_system: Optional[str] = None, mapping_type: Optional[str] = None) -> list[Map]:
55
+ """Gets all mappings by source_system and mapping type from RITA"""
56
+
57
+ async def get_process_reports_sync(self, config_id: str) -> dict:
58
+ """Gets latest sync date for a specific config"""
59
+
60
+ async def get_file_configs(self, name: Optional[str] = None) -> dict[str, FileConfig]:
61
+ """Gets File Configs from RITA"""
62
+
63
+ async def create_process_report(self, new_process: CreateReportV2) -> ProcessReportBaseV2:
64
+ """Creates a new process report in RITA"""
65
+
66
+ async def get_config_by_id(self, id: str) -> Config:
67
+ """Get a config by its id"""
68
+
69
+ async def get_config_by_name(self, bucket_path: str, config_name: str) -> Dict[str, Union[FileConfig, ProbeConfig, GenericConfig]]:
70
+ """Gets a config by its name"""
71
+
72
+ class GetMappings(BaseModel):
73
+ source_system: Optional[str] = None
74
+
75
+
76
+ class GetConfigs(BaseModel):
77
+ name: Optional[str] = None
78
+
79
+
80
+ class GravitateRitaAPI(BaseAPI):
81
+ def __init__(
82
+ self,
83
+ client_id: str | None = None,
84
+ client_secret: str | None = None,
85
+ username: str | None = None,
86
+ password: str | None = None,
87
+ tenant: str | None = None,
88
+ raise_errors: bool = True,
89
+ base_url: str = "https://rita.gravitate.energy/api/"
90
+ ):
91
+ super().__init__(raise_errors)
92
+ self.base_url = base_url
93
+ self.client_id = client_id
94
+ self.client_secret = client_secret
95
+ self.username = username
96
+ self.password = password
97
+ self.tenant = tenant
98
+ self._token = None
99
+
100
+ @classmethod
101
+ def from_global_config(cls, c: GConfig, tenant: str):
102
+ return cls(client_id=c.client_id, client_secret=c.client_secret, username=c.username, password=c.password, base_url=c.base_url, tenant=tenant)
103
+
104
+ async def _get_token(self):
105
+ try:
106
+ if self.username and self.password:
107
+ resp = await self.post(
108
+ url=f"{self.base_url}auth/token",
109
+ data={"username": self.username, "password": self.password},
110
+ )
111
+ elif self.client_id and self.client_secret:
112
+ resp = await self.post(
113
+ url=f"{self.base_url}auth/token",
114
+ data={"client_id": self.client_id, "client_secret": self.client_secret},
115
+ )
116
+ else:
117
+ raise RuntimeError("Missing credentials for token request")
118
+ except Exception:
119
+ raise ValueError(f"Error Getting Token for {self.base_url}")
120
+
121
+ try:
122
+ self._token = resp.json()["access_token"]
123
+ return self._token
124
+ except Exception:
125
+ raise ValueError(f"Could Not Get Token for {self.base_url} -> {resp.status_code}")
126
+
127
+ async def _auth_req(self, method="POST", **kwargs):
128
+ if not self._token:
129
+ await self._get_token()
130
+
131
+ headers = kwargs.pop("headers", {})
132
+ headers["authorization"] = f"Bearer {self._token}"
133
+ headers["X-Tenant-Name"] = self.tenant
134
+ kwargs["headers"] = headers
135
+ kwargs["url"] = f"{self.base_url}{kwargs.get("url", "")}"
136
+
137
+ resp = await self.request(method, **kwargs)
138
+
139
+ if resp.status_code == 401:
140
+ await self._get_token()
141
+ headers["authorization"] = f"Bearer {self._token}"
142
+ kwargs["headers"] = headers
143
+ resp = await self.request(method, **kwargs)
144
+
145
+ if resp.status_code == 422:
146
+ try:
147
+ resp_content = pprint.pformat(resp.json())
148
+ except JSONDecodeError:
149
+ resp_content = resp.text
150
+ raise HTTPStatusError(
151
+ f"Bad request: \n{resp_content}",
152
+ request=resp.request,
153
+ response=resp
154
+ )
155
+
156
+ return resp
157
+
158
+ @classmethod
159
+ def from_credential(cls, credential: RITACredential) -> Self:
160
+ return cls(
161
+ base_url=credential.base_url,
162
+ username=credential.username,
163
+ password=credential.password,
164
+ client_id=credential.client_id,
165
+ client_secret=credential.client_secret,
166
+ tenant=credential.tenant,
167
+ )
168
+
169
+ async def token_post(self, **kwargs):
170
+ return await self._auth_req("POST", **kwargs)
171
+
172
+ async def token_get(self, **kwargs):
173
+ return await self._auth_req("GET", **kwargs)
174
+
175
+ @catch_exceptions
176
+ @alru_cache(maxsize=128)
177
+ async def get_file_configs(self, name: Optional[str] = None) -> dict[str, FileConfig]:
178
+ try:
179
+ _data = (GetConfigs(name=name).model_dump() if name else {})
180
+ resp = await self.token_post(url="config/all", json=_data)
181
+ resp.raise_for_status()
182
+ return self.build_fileconfig_dict(resp.json())
183
+ except Exception as e:
184
+ loguru.logger.error(f"Error while fetching configurations: {e}")
185
+ raise e
186
+
187
+ @catch_exceptions
188
+ @alru_cache(maxsize=128)
189
+ async def get_config_by_name(self, bucket_path: str, config_name: str) -> Dict[str, Union[FileConfig, ProbeConfig, Dict]]:
190
+ try:
191
+ data = {"bucket_path": bucket_path, "name": config_name,
192
+ "bucket_name": bucket_path} # Some older versions of RITA use bucket_name instead of bucket_path
193
+ resp = await self.token_post(url="config/by_name", json=data)
194
+ resp.raise_for_status()
195
+ return self.build_config_dict([resp.json()])
196
+ except Exception as e:
197
+ loguru.logger.error(f"Error while fetching configs from bucket: {e}")
198
+ raise e
199
+
200
+ @catch_exceptions
201
+ @alru_cache(maxsize=128)
202
+ async def get_all_buckets(self) -> list[dict]:
203
+ resp = await self.token_post(url="bucket/all")
204
+ resp.raise_for_status()
205
+ return resp.json()
206
+
207
+ @catch_exceptions
208
+ @alru_cache(maxsize=128)
209
+ async def get_mappings(self, source_system: Optional[str] = None, mapping_type: Optional[str] = None) -> list[Map]:
210
+ try:
211
+ _data = (GetMappings(source_system=source_system).model_dump() if source_system else {})
212
+ resp = await self.token_post(url="integration/mappings/all", json=_data)
213
+ resp.raise_for_status()
214
+ mappings: List[Map] = [Map.model_validate(obj) for obj in resp.json()]
215
+ if mapping_type:
216
+ mappings = list(filter(lambda m: m.type == mapping_type, mappings))
217
+ return mappings
218
+ except Exception as e:
219
+ loguru.logger.error(f"Error while fetching mappings: {e}")
220
+ raise e
221
+
222
+ @catch_exceptions
223
+ @alru_cache(maxsize=128)
224
+ async def get_mappings_by_source_system(self, source_system: str) -> list[Map]:
225
+ def is_valid(obj) -> bool:
226
+ try:
227
+ map = Map.model_validate(obj)
228
+ return map.is_active
229
+ except ValidationError:
230
+ return False
231
+
232
+ try:
233
+ resp = await self.token_post(url="mapping/by_source_system/", params={"source_system": source_system})
234
+ resp.raise_for_status()
235
+ mappings = [Map.model_validate(obj) for obj in resp.json() if is_valid(obj)]
236
+ return mappings
237
+ except Exception as e:
238
+ loguru.logger.error(f"Error while fetching mappings: {e}")
239
+ raise e
240
+
241
+ @catch_exceptions
242
+ @alru_cache(maxsize=128)
243
+ async def get_process_reports_sync(self, config_id: str, status: str = None):
244
+ try:
245
+ data = {
246
+ "config_id": config_id,
247
+ "status": status
248
+ }
249
+ resp = await self.token_post(url="process_report/latest_sync", params=data)
250
+ resp.raise_for_status()
251
+ return resp.json()
252
+ except Exception as e:
253
+ loguru.logger.error(f"Error while fetching process reports sync date: {e}")
254
+ raise e
255
+
256
+ @catch_exceptions
257
+ @alru_cache(maxsize=128)
258
+ async def get_mappings_by_bucket_path(self, bucket_path: str) -> list[Map]:
259
+ try:
260
+ resp = await self.token_post(url="mapping/by_bucket_path", json=bucket_path)
261
+ resp.raise_for_status()
262
+ return [Map.model_validate(obj) for obj in resp.json()]
263
+ except Exception as e:
264
+ loguru.logger.error(f"Error while fetching mappings: {e}")
265
+ raise e
266
+
267
+ @catch_exceptions
268
+ @alru_cache(maxsize=128)
269
+ async def get_fileconfigs_from_bucket(self, bucket_path: str) -> dict[str, FileConfig]:
270
+ try:
271
+ data = {"bucket_path": bucket_path}
272
+ resp = await self.token_post(url="config/from_bucket", json=data)
273
+ resp.raise_for_status()
274
+ return self.build_fileconfig_dict(resp.json())
275
+ except Exception as e:
276
+ loguru.logger.error(f"Error while fetching configs from bucket: {e}")
277
+ raise e
278
+
279
+ @catch_exceptions
280
+ @alru_cache(maxsize=128)
281
+ async def get_config_by_id(self, id: str) -> Config:
282
+ try:
283
+ resp = await self.token_post(url=f"config/{id}")
284
+ resp.raise_for_status()
285
+ return Config.model_validate(resp.json())
286
+ except Exception as e:
287
+ loguru.logger.error(f"Error while fetching config: {e}")
288
+ raise e
289
+
290
+ @catch_exceptions
291
+ @alru_cache(maxsize=128)
292
+ async def get_fileconfig_by_name(self, bucket_path: str, config_name: str) -> dict[str, FileConfig]:
293
+ try:
294
+ data = {"bucket_path": bucket_path, "name": config_name,
295
+ "bucket_name": bucket_path} # Some older versions of RITA use bucket_name instead of bucket_path
296
+ resp = await self.token_post(url="config/by_name", json=data)
297
+ resp.raise_for_status()
298
+ return self.build_fileconfig_dict([resp.json()])
299
+ except Exception as e:
300
+ loguru.logger.error(f"Error while fetching configs from bucket: {e}")
301
+ raise e
302
+
303
+ @catch_exceptions
304
+ @alru_cache(maxsize=128)
305
+ async def get_probeconfig_by_name(self, bucket_path: str, config_name: str) -> dict[str, ProbeConfig]:
306
+ try:
307
+ data = {"bucket_path": bucket_path, "name": config_name}
308
+ resp = await self.token_post(url="config/by_name", json=data)
309
+ resp.raise_for_status()
310
+ configs = [Config.model_validate(resp.json())]
311
+ return self.build_probeconfig_dict(configs)
312
+ except Exception as e:
313
+ loguru.logger.error(f"Error while fetching config '{config_name}' from bucket '{bucket_path}': {e}")
314
+ raise e
315
+
316
+ @catch_exceptions
317
+ async def get_config_max_sync(self, config_id: str) -> Response:
318
+ try:
319
+ resp = await self.token_post(url="config/max_sync", params={"config_id": config_id})
320
+ resp.raise_for_status()
321
+ return resp
322
+ except Exception as e:
323
+ loguru.logger.error(f"Error while fetching configurations: {e}")
324
+ raise e
325
+
326
+ @catch_exceptions
327
+ async def update_config_max_sync(self, config_id: str, max_sync: MaxSync) -> Response:
328
+ try:
329
+ resp = await self.token_post(url="config/update_max_sync", params={"config_id": config_id}, json=max_sync.model_dump(mode="json"))
330
+ resp.raise_for_status()
331
+ return resp
332
+ except Exception as e:
333
+ loguru.logger.error(f"Error while fetching configurations: {e}")
334
+ raise e
335
+
336
+
337
+ @catch_exceptions
338
+ async def get_resume_token(self, probe_id: str) -> ResumeToken | None:
339
+ try:
340
+ resp = await self.token_post(url=f"probe/get_resume_token/{probe_id}")
341
+ resp.raise_for_status()
342
+ resp = resp.json()
343
+ if resp is not None:
344
+ return ResumeToken.model_validate(resp)
345
+ return None
346
+ except Exception as e:
347
+ loguru.logger.error(f"Error while fetching resume token: {e}")
348
+ raise e
349
+
350
+ @catch_exceptions
351
+ async def get_available_tenants(self):
352
+ try:
353
+ resp = await self.token_post(url="/meta/tenant/available")
354
+ resp.raise_for_status()
355
+ return resp.json()
356
+ except Exception as e:
357
+ loguru.logger.error(f"Error while fetching available tenants: {e}")
358
+ raise e
359
+
360
+ @catch_exceptions
361
+ async def set_resume_token(self, resume_token: ResumeToken):
362
+ try:
363
+ resp = await self.token_post(url=f"probe/set_resume_token", json=resume_token.model_dump())
364
+ resp.raise_for_status()
365
+ return resp.json()
366
+ except Exception as e:
367
+ loguru.logger.error(f"Error while setting resume token: {e}, resp={resp} {resp.content()}")
368
+ raise e
369
+
370
+ async def get_estick_wr(self, store_number: str, tank_number: str) -> TankReading:
371
+ try:
372
+ resp = await self.token_post(url="estick/wr", params={"store_number": store_number, "tank_id": tank_number})
373
+ resp.raise_for_status()
374
+ return TankReading(**resp.json())
375
+ except Exception as e:
376
+ loguru.logger.error(f"Error while fetching Warren Rogers estick reading: {e}")
377
+ raise e
378
+
379
+ async def get_estick_wr_many(self, store_number: str, tank_numbers: list[str]) -> list[TankReading]:
380
+ try:
381
+ resp = await self.token_post(url="estick/wr/many",
382
+ json={"store_number": store_number, "tank_ids": tank_numbers})
383
+ resp.raise_for_status()
384
+ return [TankReading(**reading) for reading in resp.json()]
385
+ except Exception as e:
386
+ loguru.logger.error(f"Error while fetching multiple Warren Rogers estick readings: {e}")
387
+ raise e
388
+
389
+ @catch_exceptions
390
+ async def create_process_report(self, new_process: CreateReportV2) -> ProcessReportBaseV2:
391
+ try:
392
+ _data = new_process.model_dump(exclude_none=True)
393
+ payload_json = json.dumps(_data, cls=CustomJSONEncoder)
394
+ # 30s timeout - large files may take a long time to upload
395
+ resp = await self.token_post(url="process_report/create", json=json.loads(payload_json), timeout=30.0)
396
+ resp.raise_for_status()
397
+ return ProcessReportBaseV2(**resp.json())
398
+ except Exception as e:
399
+ loguru.logger.error(f"Error while creating process_report: {e}")
400
+ raise e
401
+
402
+ async def update_process_report(self, update_process: UpdateReportV2) -> ProcessReportBaseV2:
403
+ try:
404
+ _data = update_process.model_dump(exclude_none=True)
405
+ payload_json = json.dumps(_data, cls=CustomJSONEncoder)
406
+ resp = await self.token_post(url="process_report/update", json=json.loads(payload_json))
407
+ resp.raise_for_status()
408
+ return ProcessReportBaseV2(**resp.json())
409
+ except Exception as e:
410
+ loguru.logger.error(f"Error while creating process_report: {e}")
411
+ raise e
412
+
413
+ async def record_many_issues(self, issues: List[IssueBase]):
414
+ try:
415
+ res = await self.token_post(url="issue/record_many", json=[issue.model_dump(mode="json") for issue in issues])
416
+ res.raise_for_status()
417
+ # Endpoint does not return anything other than the status code
418
+ except Exception as e:
419
+ loguru.logger.error(f"Error while recording issue(s): {e}")
420
+ raise e
421
+
422
+ async def record_issue(self, issue: IssueBase):
423
+ await self.record_many_issues([issue])
424
+
425
+ async def update_issue(self, issue: UpdateIssue) -> IssueBase:
426
+ try:
427
+ resp = await self.token_post(url="issue/update", content=issue.model_dump_json(exclude_unset=True))
428
+ resp.raise_for_status()
429
+ return IssueBase(**resp.json())
430
+ except Exception as e:
431
+ loguru.logger.error(f"Error while updating issue {issue.key}: {e}")
432
+ raise e
433
+
434
+ async def get_issue(self, key: str) -> IssueBase:
435
+ try:
436
+ resp = await self.token_post(url=f"issue/get/{key}")
437
+ resp.raise_for_status()
438
+ return IssueBase(**resp.json())
439
+ except Exception as e:
440
+ loguru.logger.error(f"Error while updating issue {key}: {e}")
441
+ raise e
442
+
443
+ async def get_issues_by_config_id(self, config_id: str) -> list[IssueBase]:
444
+ try:
445
+ resp = await self.token_post(url=f"issue/by_config_id/{config_id}")
446
+ resp.raise_for_status()
447
+ content = resp.json()
448
+ return [IssueBase(**item) for item in content]
449
+ except Exception as e:
450
+ loguru.logger.error(f"Error while fetching issues by config id {config_id}: {e}")
451
+ raise e
452
+
453
+ async def call_ep(self, url: str, params: dict = None, json: dict = None, method: str = "POST") -> Response:
454
+ if method == "POST":
455
+ return await self.token_post(url=url, params=params, json=json)
456
+ if json is not None:
457
+ raise ValueError("JSON is not supported for GET requests")
458
+ return await self.token_get(url=url, params=params)
459
+
460
+ async def update_config_value(self, config_id: str, new_value: dict):
461
+ try:
462
+ config = await self.get_config_by_id(config_id)
463
+ config.config = new_value
464
+ update_req = json.loads(config.model_dump_json())
465
+ update_req["_id"] = config_id
466
+ resp = await self.token_post(url=f"config/update", json=update_req)
467
+ resp.raise_for_status()
468
+ return Config.model_validate(resp.json())
469
+ except Exception as e:
470
+ loguru.logger.error(f"Error while updating config {config_id}: {e}")
471
+ raise e
472
+
473
+ @catch_exceptions
474
+ async def send_email(self, email: EmailData, timeout: float = 10.0) -> Response:
475
+ """Sends an email via RITA. Default timeout is 10 seconds. Large emails may take longer to send."""
476
+ return await self.token_post(url="integration/send_email", json=email.model_dump(), timeout=timeout)
477
+
478
+ @alru_cache(maxsize=64)
479
+ async def get_connection(self, connection_id: str) -> dict:
480
+ return await self.token_get(url=f"crossroads/network/connections/{connection_id}")
481
+
482
+ async def get_crossroads_mapping(self, req: MappingRequest) -> MappingRequest:
483
+ try:
484
+ resp = await self.token_post(url=f"crossroads_2/map", json=req.model_dump(mode="json"))
485
+ if resp.status_code == 499:
486
+ # There was an error while mapping. Parse the detail and re-raise the error.
487
+ js = resp.json()["detail"]
488
+ raise CrossroadsMappingError(**js)
489
+ if resp.status_code == 500:
490
+ raise ValueError(resp.content)
491
+ content = resp.json()
492
+ result = MappingRequest.model_validate(content)
493
+ return result
494
+ except Exception as e:
495
+ loguru.logger.error(f"Unexpected error while fetching Crossroads mapping: {e}")
496
+ raise e
497
+
498
+
499
+
500
+ def build_single_fileconfig_dict(self, config: Config) -> FileConfig:
501
+ """
502
+ Converts a config to File Config.
503
+ """
504
+ file_config = FileConfig.model_validate(config.config)
505
+ file_config.config_id = config.id # Add the config's ID to the fileconfig obj, in case it's needed later.
506
+ return file_config
507
+
508
+ def build_single_probeconfig_dict(self, config: Config) -> ProbeConfig:
509
+ """
510
+ converts a config into a ProbeConfig.
511
+ """
512
+ return ProbeConfig.model_validate(config.config)
513
+
514
+
515
+ def build_config_dict(self, configs: List[Dict]) -> Dict[str, Union[FileConfig, ProbeConfig, GenericConfig]]:
516
+ """Builds a config lkp by name and parses each config to its respective model"""
517
+ output = {}
518
+ for c in configs:
519
+ config = Config.model_validate(c)
520
+ if config.type == ConfigType.fileconfig:
521
+ output[config.name] = self.build_single_fileconfig_dict(config)
522
+ elif config.type == ConfigType.probeconfig:
523
+ output[config.name] = self.build_single_probeconfig_dict(config)
524
+ else:
525
+ output[config.name] = GenericConfig(config_id= c.get("_id"), config = config.config)
526
+ return output
527
+
528
+ def build_probeconfig_dict(self, configs: List[Config]) -> Dict[str, ProbeConfig]:
529
+ """
530
+ Converts a list of configs of any type into a dict mapping config name to parsed ProbeConfig. Skips entries
531
+ that are not ProbeConfigs, so the output dict may be smaller than the input list.
532
+ """
533
+ probe_configs = [c for c in configs if c.type == ConfigType.probeconfig]
534
+ output = {}
535
+ for config in probe_configs:
536
+ parsed_probe_config = ProbeConfig.model_validate(config.config)
537
+ output[config.name] = parsed_probe_config
538
+ return output
539
+
540
+ def build_fileconfig_dict(self, configs: list[dict]) -> dict[str, FileConfig]:
541
+ """
542
+ Converts a list of configs of any type into a dict mapping config name to parsed FileConfig. Skips entries
543
+ that are not FileConfigs, so the output dict may be smaller than the input list.
544
+ """
545
+ output = {}
546
+ for c in configs:
547
+ config = Config.model_validate(c)
548
+ if config.type == ConfigType.fileconfig:
549
+ file_config = FileConfig.model_validate(config.config)
550
+ file_config.config_id = c.get("_id") # Add the config's ID to the fileconfig obj, in case it's needed later.
551
+ output[config.name] = file_config
552
+ return output