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,131 @@
1
+ from datetime import datetime, UTC, timedelta
2
+ from typing import Dict
3
+ from dateutil.parser import parse
4
+
5
+ import loguru
6
+
7
+ from bb_integrations_lib.provider.api.pc_miler.client import PCMilerClient
8
+ from bb_integrations_lib.provider.api.pc_miler.model import TokenData
9
+
10
+
11
+ class _RoutingAPI:
12
+ def __init__(self, client: PCMilerClient):
13
+ self.client = client
14
+
15
+ def _custom_headers(self):
16
+ return {
17
+ 'Authorization': f'{self.client.api_key}',
18
+ 'Accept': 'application/json',
19
+ 'Content-type': 'application/json'
20
+ }
21
+
22
+ async def get_route_reports(self, url: str, params: Dict):
23
+ custom_headers = self._custom_headers()
24
+ return await self.client.get(url, params=params, headers=custom_headers)
25
+
26
+ @property
27
+ def service_name(self):
28
+ return """ROUTING_API"""
29
+
30
+
31
+
32
+ class RoutingAPI(_RoutingAPI):
33
+ def __init__(self, client: PCMilerClient):
34
+ super().__init__(client)
35
+
36
+
37
+ class _SingleSearchAPI:
38
+ def __init__(self, client: PCMilerClient):
39
+ pass
40
+
41
+
42
+ class SingleSearchAPI(_SingleSearchAPI):
43
+ def __init__(self, client: PCMilerClient):
44
+ super().__init__(client)
45
+
46
+
47
+ class _PlacesAPI:
48
+ def __init__(self, client: PCMilerClient):
49
+ self.client = client
50
+ self.tokendata: TokenData | None = None
51
+
52
+ async def _custom_headers(self):
53
+ if not (self.tokendata and self.tokendata.token and datetime.now(UTC) < parse(self.tokendata.expires)):
54
+ self.tokendata = await self.client.get_token("https://api.trimblemaps.com/places/v1/authenticate")
55
+ return {
56
+ 'Authorization': f'Bearer {self.tokendata.token}',
57
+ }
58
+
59
+ @property
60
+ def service_name(self):
61
+ return """PLACES_API"""
62
+
63
+ class PlacesAPI(_PlacesAPI):
64
+ def __init__(self, client: PCMilerClient):
65
+ super().__init__(client)
66
+
67
+ async def get_place_by_id(self, id: str):
68
+ custom_headers = await self._custom_headers()
69
+ result = await self.client.get(
70
+ url=f"https://api.trimblemaps.com/places/v1/place/{id}/details",
71
+ headers=custom_headers
72
+ )
73
+ return result
74
+
75
+ async def get_updated_places(self, last_modified_timestamp: str) -> list[dict] | None:
76
+ custom_headers = await self._custom_headers()
77
+ # last_modified_record=None
78
+ all_results = []
79
+ retries = 3
80
+ while True:
81
+ result = await self.client.get(
82
+ url=f"https://api.trimblemaps.com/places/v1/place/updatedPlaces",
83
+ params={
84
+ "lastModifiedDate": last_modified_timestamp,
85
+ # "lastModifiedRecord": last_modified_record,
86
+ "pageSize": 100
87
+ },
88
+ headers=custom_headers
89
+ )
90
+ if result.status_code != 200:
91
+ retries -= 1
92
+ if retries == 0:
93
+ return None
94
+ continue
95
+ data = result.json()
96
+ items = data.get("items", [])
97
+ all_results.extend(items)
98
+ if len(items) < 100:
99
+ break
100
+ # last_modified_record = data.get("lastModifiedRecord", None)
101
+ new_last_modified_timestamp = data.get("lastModifiedTimestamp", datetime(1970, 1, 1).isoformat())
102
+ if new_last_modified_timestamp == last_modified_timestamp:
103
+ # WARNING: This is a workaround for a bug in the Trimble API.
104
+ # In short, the API only returns 100 datapoints at a time, and if a timestamp has more than 100 datapoints
105
+ # associated it's not possible to get any datapoints after the 100th; the API ONLY lets me pick by timestamp
106
+ # and not by a generic page number or anything. Bulk operations seem to trigger this worse than most.
107
+ # As a workaround I will check if the timestamp we're going to "next" is the same as the current one, and add
108
+ # 1 millisecond if it is.
109
+ #
110
+ # This can skip over an unknown number of events! So far I've only seen problems due to mass deletions,
111
+ # but if there's a mass-edit functionality it could be an issue there too.
112
+ new_last_modified_timestamp = datetime.fromisoformat(new_last_modified_timestamp) + timedelta(milliseconds=1)
113
+ new_last_modified_timestamp = new_last_modified_timestamp.isoformat()
114
+ last_modified_timestamp = new_last_modified_timestamp
115
+ loguru.logger.debug(f"Fetching page from date {last_modified_timestamp}")
116
+ return all_results
117
+
118
+ async def create_place(self, place_req: dict, dry_run: bool = False):
119
+ custom_headers = await self._custom_headers()
120
+ if dry_run:
121
+ loguru.logger.debug(f"Dry run: Would create place {place_req}")
122
+ return
123
+
124
+ result = await self.client.post(
125
+ url=f"https://api.trimblemaps.com/places/v1/place",
126
+ json=place_req,
127
+ headers=custom_headers
128
+ )
129
+ loguru.logger.debug(result.status_code)
130
+ return result
131
+
@@ -0,0 +1,147 @@
1
+ from datetime import datetime, UTC
2
+ from typing import Self
3
+
4
+ from httpx import Response
5
+
6
+ from bb_integrations_lib.gravitate.base_api import BaseAPI
7
+ from bb_integrations_lib.provider.api.platform_science.model import JobDefinition, LoadDefinition
8
+ from bb_integrations_lib.secrets.credential_models import PlatformScienceCredential
9
+
10
+
11
+ class PlatformScienceClient(BaseAPI):
12
+ def __init__(self,
13
+ base_url: str,
14
+ client_id: str,
15
+ client_secret: str
16
+ ):
17
+ super().__init__()
18
+ self.base_url = base_url
19
+ self.client_id = client_id
20
+ self.client_secret = client_secret
21
+ self._token = None
22
+
23
+ def __repr__(self):
24
+ return "Platform Science API client"
25
+
26
+ @classmethod
27
+ def from_credential(cls, credential: PlatformScienceCredential) -> Self:
28
+ return cls(
29
+ base_url=credential.base_url,
30
+ client_id=credential.client_id,
31
+ client_secret=credential.client_secret,
32
+ )
33
+
34
+ async def _get_token(self):
35
+ try:
36
+ resp = await self.post(
37
+ url=f"{self.base_url}oauth/token",
38
+ headers={
39
+ "Content-Type": "application/json",
40
+ "Accept": "application/json",
41
+ "api-version": "2.0"
42
+ },
43
+ json={
44
+ "grant_type": "client_credentials",
45
+ "scope": "admin workflow",
46
+ "client_id": self.client_id,
47
+ "client_secret": self.client_secret,
48
+ }
49
+ )
50
+ except Exception:
51
+ raise ValueError(f"Error getting token from {self.base_url}")
52
+
53
+ try:
54
+ self._token = resp.json()["access_token"]
55
+ return self._token
56
+ except Exception:
57
+ raise ValueError(f"Token response invalid from {self.base_url} -> {resp.status_code}")
58
+
59
+ async def _auth_request(self, method: str, **kwargs):
60
+ if not self._token:
61
+ await self._get_token()
62
+
63
+ headers = kwargs.pop("headers", {})
64
+ headers["Authorization"] = f"Bearer {self._token}"
65
+ headers["api-version"] = "2.0"
66
+ headers["Content-Type"] = "application/json"
67
+ kwargs["headers"] = headers
68
+ kwargs["url"] = f"{self.base_url}{kwargs.get('url', '')}"
69
+ kwargs["timeout"] = kwargs.get("timeout", 90)
70
+
71
+ response = await getattr(self, method)(**kwargs)
72
+
73
+ if response.status_code == 401:
74
+ await self._get_token() # refresh token
75
+ headers["authorization"] = f"Bearer {self._token}"
76
+ kwargs["headers"] = headers
77
+ response = await getattr(self, method)(**kwargs)
78
+
79
+ return response
80
+
81
+ async def auth_post(self, **kwargs) -> Response:
82
+ return await self._auth_request("post", **kwargs)
83
+
84
+ async def auth_get(self, **kwargs) -> Response:
85
+ return await self._auth_request("get", **kwargs)
86
+
87
+ async def auth_patch(self, **kwargs) -> Response:
88
+ return await self._auth_request("patch", **kwargs)
89
+
90
+ async def auth_put(self, **kwargs) -> Response:
91
+ return await self._auth_request("put", **kwargs)
92
+
93
+ async def auth_delete(self, **kwargs) -> Response:
94
+ return await self._auth_request("delete", **kwargs)
95
+
96
+ async def paginated_get(self, url: str, params: dict | None = None) -> list[dict]:
97
+ params = params or {}
98
+ first_page = await self.auth_get(url=url, params=params)
99
+ if first_page.status_code != 200:
100
+ raise RuntimeError(f"Error getting assets from {self.base_url}")
101
+ body = first_page.json()
102
+ data = body["data"]
103
+ total_pages = body["meta"]["pagination"]["total_pages"]
104
+ while body["meta"]["pagination"]["current_page"] < total_pages:
105
+ params["page"] = body["meta"]["pagination"]["current_page"] + 1
106
+ next_page = await self.auth_get(url="admin/assets", params=params)
107
+ body = next_page.json()
108
+ data.extend(body["data"])
109
+ return data
110
+
111
+ async def get_assets(self) -> list[dict]:
112
+ return await self.paginated_get(url="admin/assets")
113
+
114
+ async def get_drivers(self) -> list[dict]:
115
+ return await self.paginated_get(url="admin/drivers")
116
+
117
+ async def create_assets(self, req: list[dict]) -> Response:
118
+ return await self.auth_post(url="admin/assets", json=req)
119
+
120
+ async def get_amqp_connection(self, channel_name: str):
121
+ vhost = ""
122
+ conn_str = f"amqps://{self.client_id}:{self.client_secret}@amqp.pltsci.com:5671/{vhost}"
123
+
124
+ async def create_workflow_job(self, driver_id: str, job_definition: JobDefinition) -> Response:
125
+ return await self.auth_post(url=f"drivers/{driver_id}/jobs", json={
126
+ "job": job_definition.model_dump(mode="json", exclude_unset=True)
127
+ })
128
+
129
+ async def update_workflow_job(self, driver_id: str, job_id: str, job_definition: JobDefinition) -> Response:
130
+ return await self.auth_put(url=f"drivers/{driver_id}/jobs/{job_id}", json={
131
+ "job": job_definition.model_dump(mode="json", exclude_unset=True)
132
+ })
133
+
134
+ async def complete_workflow_job(self, job_id: str, completed_at: datetime | None = None) -> Response:
135
+ return await self.auth_patch(url=f"jobs/{job_id}/status", json={
136
+ "status": "tms_completed",
137
+ "completed_at": (completed_at or datetime.now(tz=UTC)).astimezone(tz=UTC).isoformat()
138
+ })
139
+
140
+ async def delete_workflow_job(self, job_id: str) -> Response:
141
+ return await self.auth_delete(url=f"jobs/{job_id}/status")
142
+
143
+ async def create_load(self, driver_id: str, load_definition: LoadDefinition) -> Response:
144
+ return await self.auth_post(
145
+ url=f"admin/drivers/{driver_id}/loads",
146
+ json=load_definition.model_dump(mode="json", exclude_none=False)
147
+ )
@@ -0,0 +1,82 @@
1
+ from datetime import datetime, date
2
+ from typing import Literal
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class JobLocation(BaseModel):
8
+ external_id: str
9
+ name: str
10
+ address: str
11
+ latitude: str
12
+ longitude: str
13
+ city: str
14
+ state: str = Field(min_length=2, max_length=2)
15
+ country_code: str = Field(min_length=2, max_length=2)
16
+ postal_code: str | None = None
17
+ postal_splc: str | None = None
18
+ time_zone: Literal["US/Pacific", "US/Arizona", "US/Mountain", "US/Central", "US/Eastern"] | None = None
19
+
20
+ class JobTaskAppointment(BaseModel):
21
+ start_time: datetime
22
+ end_time: datetime
23
+ ready_time: datetime
24
+ late_time: datetime | None = None
25
+
26
+ class JobTask(BaseModel):
27
+ # external_data: JobTaskExternalData
28
+ remarks: list[str]
29
+ fields: dict[str, str]
30
+ id: str
31
+ external_id: str
32
+ order: int = Field(description="Index of the task within the step.")
33
+ type: str
34
+ status: str
35
+ name: str
36
+ completed: bool
37
+ completed_at: datetime | None = None
38
+ assets: str | None = None
39
+
40
+ class JobStep(BaseModel):
41
+ tasks: list[JobTask]
42
+ order: int = Field(description="Index of the step within the job.")
43
+ type: str
44
+ name: str
45
+ external_id: str
46
+ location_external_id: str
47
+ completed: bool
48
+ completed_at: datetime | None = None
49
+ is_disabled: bool | None = None
50
+ is_bypassable: bool | None = None
51
+ is_bypassed: bool | None = None
52
+ bypass_reason: str | None = None
53
+ is_reorderable: bool | None = None
54
+ reorder_reason: str | None = None
55
+ is_skippable: bool | None = None
56
+ is_skipped: bool | None = None
57
+ skip_reason: str | None = None
58
+
59
+ class ValueWithUnit(BaseModel):
60
+ value: float
61
+ uom: str
62
+
63
+ class ShipmentDetails(BaseModel):
64
+ total_distance: ValueWithUnit
65
+
66
+ class JobDefinition(BaseModel):
67
+ status: str # TODO: What statuses are possible?
68
+ external_id: str
69
+ locations: list[JobLocation]
70
+ steps: list[JobStep]
71
+ shipment_details: ShipmentDetails
72
+
73
+ class LoadEntity(BaseModel):
74
+ type: Literal["trailer", "bill_of_lading"]
75
+ value: str
76
+
77
+ class LoadDefinition(BaseModel):
78
+ start_date: date
79
+ end_date: date
80
+ load: str | None = None
81
+ user_external_id: str
82
+ entities: list[LoadEntity]
File without changes
@@ -0,0 +1,52 @@
1
+ from typing import Self
2
+ import httpx
3
+
4
+ from bb_integrations_lib.gravitate.base_api import BaseAPI
5
+ from bb_integrations_lib.secrets.credential_models import QTCredential
6
+
7
+
8
+ class QTApiClient(BaseAPI):
9
+ def __init__(self,
10
+ base_url: str,
11
+ qt_id: str,
12
+ carrier_id: str,
13
+ authorization: str):
14
+ super().__init__()
15
+ self.base_url = base_url
16
+ self.qt_id = qt_id
17
+ self.carrier_id = carrier_id
18
+ self.authorization = authorization
19
+
20
+ @classmethod
21
+ def from_credential(cls, credential: QTCredential) -> Self:
22
+ return cls(**credential.model_dump(exclude={"type_tag"}))
23
+
24
+ async def get_inventory(self) -> list[dict]:
25
+ params = {
26
+ "qtId": self.qt_id,
27
+ "carrierId": self.carrier_id,
28
+ "authorization": self.authorization
29
+ }
30
+ response = await self.get(
31
+ f"{self.base_url}/api/ExternalEndpoint/GetLatestCarrierInventory",
32
+ params=params
33
+ )
34
+ response.raise_for_status()
35
+ return response.json()
36
+
37
+
38
+
39
+ if __name__ == "__main__":
40
+ import asyncio
41
+
42
+ async def main():
43
+ client = QTApiClient(
44
+ base_url="https://petrocorpapi.quiktrip.com",
45
+ qt_id="EAGN",
46
+ carrier_id="41",
47
+ authorization="07572AD2B28DC42410D7CAD4ED126499FDE84F73A7D4A56CE2A1ADBE80117FDD10DC7923E9CFB06BF03FF9251CADD2F2163F7C341E954656FE046F3EC932CD7F"
48
+ )
49
+ inventory = await client.get_inventory()
50
+ print(inventory)
51
+
52
+ asyncio.run(main())
File without changes
@@ -0,0 +1,68 @@
1
+ import httpx
2
+ import loguru
3
+ from bb_integrations_lib.provider.api.telapoint.model import CheckConnectionMethod, SoapEnvelope, OrderGetByOrderNumberMethod, TelapointNewOrder, OrderAdd
4
+
5
+ namespaces = {
6
+ 'soapenv': "http://schemas.xmlsoap.org/soap/envelope/",
7
+ 'tel': "http://schemas.datacontract.org/2004/07/TelaPoint.Api.TelaFuel.Models",
8
+ 'v2': "http://api.telapoint.com/TelaFuel/v2",
9
+ 'i': "http://www.w3.org/2001/XMLSchema-instance",
10
+ 'wsse': "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
11
+ 'wsu': "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
12
+ }
13
+
14
+
15
+ class TelapointClient(httpx.AsyncClient):
16
+ def __init__(self, username: str, password: str):
17
+ super().__init__(
18
+ base_url="https://api.telapoint.com/APIv2/TelaFuelService.svc",
19
+ headers={"Content-Type": "text/xml"}
20
+ )
21
+ self.base_url = "https://api.telapoint.com/APIv2/TelaFuelService.svc"
22
+ self.username = username
23
+ self.password = password
24
+
25
+ async def check_connection(self):
26
+ req = SoapEnvelope.build(self.username, self.password, CheckConnectionMethod)
27
+
28
+ response = await super().post(
29
+ url="",
30
+ headers={"SOAPAction": "http://api.telapoint.com/TelaFuel/v2/ITelaFuelService/CheckConnection"},
31
+ content=req.to_xml()
32
+ )
33
+ loguru.logger.debug(response.status_code)
34
+ return response
35
+
36
+ async def get_order_by_order_number(self, order_number: str):
37
+ req = SoapEnvelope.build(self.username, self.password, OrderGetByOrderNumberMethod, order_number=order_number)
38
+
39
+ response = await super().post(
40
+ url="",
41
+ headers={"SOAPAction": "http://api.telapoint.com/TelaFuel/v2/ITelaFuelService/OrderGetByOrderNumber"},
42
+ content=req.to_xml()
43
+ )
44
+ loguru.logger.debug(response.status_code)
45
+ return response
46
+
47
+ async def create_order(self, new_order: TelapointNewOrder):
48
+ req = SoapEnvelope.build_with_body(self.username, self.password, OrderAdd, OrderAdd(new_order=new_order))
49
+
50
+ response = await super().post(
51
+ url="",
52
+ headers={"SOAPAction": "http://api.telapoint.com/TelaFuel/v2/ITelaFuelService/OrderAdd"},
53
+ content=req.to_xml()
54
+ )
55
+ loguru.logger.debug(response.status_code)
56
+ return response
57
+
58
+
59
+ if __name__ == "__main__":
60
+ import asyncio
61
+
62
+ async def main():
63
+ tc = TelapointClient("<>", "<>")
64
+ resp = await tc.check_connection()
65
+ resp = await tc.get_order_by_order_number(order_number="51412828")
66
+ pass
67
+
68
+ asyncio.run(main())
@@ -0,0 +1,178 @@
1
+ from typing import Generic, TypeVar, Type
2
+
3
+ from pydantic_xml import BaseXmlModel, element, attr
4
+
5
+ _nsmap = {
6
+ "soapenv": "http://schemas.xmlsoap.org/soap/envelope/",
7
+ "tel": "http://schemas.datacontract.org/2004/07/TelaPoint.Api.TelaFuel.Models",
8
+ "v2": "http://api.telapoint.com/TelaFuel/v2",
9
+ "i": "http://www.w3.org/2001/XMLSchema-instance",
10
+ "wsse": "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
11
+ "wsu": "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
12
+ }
13
+
14
+ ####################### Copied from pydantic-xml docs for SOAP envelope tech. Probably don't touch.
15
+
16
+ AuthType = TypeVar('AuthType')
17
+
18
+
19
+ class SoapHeader(
20
+ BaseXmlModel, Generic[AuthType],
21
+ tag='Header',
22
+ ns='soapenv',
23
+ nsmap=_nsmap,
24
+ ):
25
+ auth: AuthType
26
+
27
+
28
+ class SoapMethod(BaseXmlModel):
29
+ pass
30
+
31
+
32
+ MethodType = TypeVar('MethodType', bound=SoapMethod)
33
+
34
+
35
+ class SoapBody(
36
+ BaseXmlModel, Generic[MethodType],
37
+ tag='Body',
38
+ ns='soapenv',
39
+ nsmap=_nsmap,
40
+ ):
41
+ call: MethodType
42
+
43
+
44
+ HeaderType = TypeVar('HeaderType', bound=SoapHeader)
45
+ BodyType = TypeVar('BodyType', bound=SoapBody)
46
+
47
+
48
+ class _Password(BaseXmlModel, tag="Password", ns="wsse", nsmap=_nsmap):
49
+ type: str = attr("Type",
50
+ default="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText")
51
+ value: str
52
+
53
+
54
+ class _UsernameToken(BaseXmlModel, tag="UsernameToken", ns="wsse", nsmap=_nsmap):
55
+ id: str = attr("Id", ns="wsu", default="uuid-3FE90A72-14E4-29C8-0782-232001AA1234-5")
56
+ username: str = element("Username", ns="wsse", text=True)
57
+ password: _Password
58
+
59
+
60
+ class Security(
61
+ BaseXmlModel,
62
+ tag='Security',
63
+ ns='wsse',
64
+ nsmap=_nsmap,
65
+ ):
66
+ must_understand: str = attr("mustUnderstand", ns="wsu", default="1")
67
+ username_token: _UsernameToken
68
+
69
+
70
+ class SoapEnvelope(
71
+ BaseXmlModel,
72
+ Generic[MethodType],
73
+ tag='Envelope',
74
+ ns='soapenv',
75
+ nsmap=_nsmap,
76
+ ):
77
+ header: SoapHeader[Security]
78
+ body: SoapBody[MethodType]
79
+
80
+ @classmethod
81
+ def build(cls, username: str, password: str, method: Type[MethodType], **method_kwargs):
82
+ return cls[method](
83
+ header=SoapHeader[Security](
84
+ auth=Security(
85
+ username_token=_UsernameToken(
86
+ username=username,
87
+ password=_Password(value=password)
88
+ )
89
+ )
90
+ ),
91
+ body=SoapBody(call=method(**method_kwargs))
92
+ )
93
+
94
+ @classmethod
95
+ def build_with_body(cls, username: str, password: str, method: Type[MethodType], body: MethodType):
96
+ return cls[method](
97
+ header=SoapHeader[Security](
98
+ auth=Security(
99
+ username_token=_UsernameToken(
100
+ username=username,
101
+ password=_Password(value=password)
102
+ )
103
+ )
104
+ ),
105
+ body=SoapBody(call=body)
106
+ )
107
+
108
+
109
+ ################################### End copy from pydantic-xml.
110
+
111
+
112
+ """
113
+ Below this point you can define new methods. These are the things that go in the SOAP body in the request. They are
114
+ regular Pydantic XML models.
115
+ """
116
+
117
+
118
+ class CheckConnectionMethod(SoapMethod, tag="CheckConnection", ns="v2", nsmap=_nsmap):
119
+ pass
120
+
121
+
122
+ class OrderGetByOrderNumberMethod(SoapMethod, tag="OrderGetByOrderNumber", ns="v2", nsmap=_nsmap):
123
+ order_number: str = element("orderNumber", ns="v2", text=True)
124
+
125
+
126
+ class DropProduct(BaseXmlModel, tag="DropProduct", ns="tel", nsmap=_nsmap):
127
+ logical_tank_guid: str = element("LogicalTankGUID", ns="tel", text=True)
128
+ order_quantity: int = element("OrderQuantity", ns="tel", text=True)
129
+ product_guid: str = element("ProductGUID", ns="tel", text=True)
130
+ source_lift_sequence_number: int = element("SourceLiftSequenceNumber", ns="tel", text=True, default=1)
131
+
132
+ class DropProducts(BaseXmlModel, tag="OrderDrops", ns="tel", nsmap=_nsmap):
133
+ drop_products: list[DropProduct] = element("DropProduct", ns="tel")
134
+
135
+ class OrderDrop(BaseXmlModel, tag="OrderDrop", ns="tel", nsmap=_nsmap):
136
+ drop_duration: int = element("DropDuration", ns="tel", text=True)
137
+ drop_products: DropProducts = element("DropProducts", ns="tel")
138
+ earliest_date_time: str = element("EarliestDateTime", ns="tel", text=True)
139
+ latest_date_time: str = element("LatestDateTime", ns="tel", text=True)
140
+ order_type_id: int = element("OrderTypeID", ns="tel", text=True)
141
+ sequence_number: int = element("SequenceNumber", ns="tel", text=True, default=1)
142
+ site_guid: str = element("SiteGUID", ns="tel", text=True)
143
+
144
+ class OrderDrops(BaseXmlModel, tag="OrderDrops", ns="tel", nsmap=_nsmap):
145
+ order_drops: list[OrderDrop] = element("OrderDrop", ns="tel")
146
+
147
+ class LiftProduct(BaseXmlModel, tag="LiftProduct", ns="tel", nsmap=_nsmap):
148
+ order_quantity: int = element("OrderQuantity", ns="tel", text=True)
149
+ product_guid: str = element("ProductGUID", ns="tel", text=True)
150
+
151
+ class LiftProducts(BaseXmlModel, tag="LiftProducts", ns="tel", nsmap=_nsmap):
152
+ lift_products: list[LiftProduct] = element("LiftProduct", ns="tel")
153
+
154
+ class OrderLift(BaseXmlModel, tag="OrderLift", ns="tel", nsmap=_nsmap):
155
+ lift_products: LiftProducts = element("LiftProducts", ns="tel")
156
+ planned_lift_date_time: str = element("PlannedLiftDateTime", ns="tel", text=True)
157
+ sequence_number: int = element("SequenceNumber", ns="tel", text=True, default=1)
158
+
159
+ class OrderLifts(BaseXmlModel, tag="OrderLifts", ns="tel", nsmap=_nsmap):
160
+ order_lifts: list[OrderLift] = element("OrderLift", ns="tel")
161
+
162
+ class TelapointNewOrder(BaseXmlModel, tag="newOrder", ns="v2", nsmap=_nsmap):
163
+ bill_to_guid: str = element("BillToGUID", ns="tel", text=True)
164
+ bill_to_type: str = element("BillToType", ns="tel", text=True)
165
+ carrier_guid: str = element("CarrierGUID", ns="tel", text=True)
166
+ freight_lane_guid: str = element("FreightLaneGUID", ns="tel", text=True)
167
+ order_drops: OrderDrops = element("OrderDrops", ns="tel")
168
+ order_lifts: OrderLifts = element("OrderLifts", ns="tel")
169
+ order_status: str = element("OrderStatus", ns="tel", text=True)
170
+ order_type: str = element("OrderType", ns="tel", text=True)
171
+ po_number: str = element("PONumber", ns="tel", text=True)
172
+ parent_company_guid: str = element("ParentCompanyGUID", ns="tel", text=True)
173
+
174
+ class OrderAdd(SoapMethod, tag="OrderAdd", ns="v2", nsmap=_nsmap):
175
+ new_order: TelapointNewOrder
176
+
177
+ class OrderAddResponse(SoapMethod, tag="OrderAddResponse", ns="v2", nsmap=_nsmap):
178
+ order_add_result: str = element("OrderAddResult", text=True)