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,188 @@
1
+ from datetime import datetime, UTC
2
+ from enum import Enum
3
+ from typing import Optional, Union, Dict, Any, List
4
+
5
+ from pydantic import BaseModel, ValidationError, model_validator, Field, ConfigDict
6
+
7
+ from bb_integrations_lib.models.rita.probe import ProbeConfig
8
+
9
+
10
+ class MaxSync(BaseModel):
11
+ """
12
+ This class tracks the most recent synchronization timestamp for a job config
13
+ along with additional contextual information needed for resuming sync operations.
14
+
15
+ Attributes:
16
+ max_sync_date (datetime): The timestamp of the most recent successful
17
+ synchronization. Defaults to the current UTC time. Serves as a checkpoint
18
+ to determine where to resume data synchronization from in later
19
+ sync operations.
20
+ context (dict): Key-value pairs storing additional synchronization context.
21
+ Can contain resume tokens, sync IDs, cursors, batch sizes, source versions,
22
+ retry counts, and other custom metadata specific to the job run.
23
+
24
+ Example:
25
+ >>> sync_info = MaxSync(
26
+ ... max_sync_date=datetime(2024, 1, 15, 10, 30, 0, tzinfo=UTC),
27
+ ... context={
28
+ ... "resume_token": "abc123xyz",
29
+ ... "sync_id": "sync_2024_001",
30
+ ... "batch_size": 1000
31
+ ... }
32
+ ... )
33
+ """
34
+
35
+ max_sync_date: datetime = datetime.now(UTC)
36
+ context: Optional[Dict] = None
37
+
38
+
39
+ class ConfigType(str, Enum):
40
+ template = "template"
41
+ process = "process"
42
+ generic = "generic"
43
+ fileconfig = "fileconfig"
44
+ probeconfig = "probeconfig"
45
+ DEPRECATED_crossroadsconfig = "crossroadsconfig"
46
+ DEPRECATED_connectorconfig = "connectorconfig"
47
+ scheduler = "scheduler"
48
+
49
+
50
+ class SchedulerJob(BaseModel):
51
+ enabled: bool = False
52
+ name: str
53
+ job_func: str = Field(description="The function from the scheduler module to run")
54
+ trigger: str = Field(description="The name of the APScheduler trigger to use for this job")
55
+ scheduler_kwargs: Optional[dict[str, Any]] = Field({},
56
+ description="To pass to the APScheduler job creation function")
57
+ job_kwargs: Optional[dict] = Field({}, description="To pass to the scheduled function")
58
+
59
+
60
+ class SchedulerConfig(BaseModel):
61
+ enabled: bool = False
62
+ jobs: list[SchedulerJob] = []
63
+
64
+
65
+ class Alert(BaseModel):
66
+ enabled: Optional[bool] = False
67
+ tolerance: Optional[int] = None
68
+ notification: Optional[bool] = False
69
+ distribution_list: Optional[list[str]] = None
70
+
71
+
72
+ class ConfigAction(str, Enum):
73
+ concat = "concat"
74
+ parse_date = "parse_date"
75
+ concat_date = "concat_date"
76
+ add = "add"
77
+ copy = "copy"
78
+ remove_leading_zeros = "remove_leading_zeros"
79
+ remove_trailing_zeros = "remove_trailing_zeros"
80
+ wesroc_volume_formula = "wesroc_volume_formula"
81
+ blank = ""
82
+
83
+
84
+ class FileConfigColumn(BaseModel):
85
+ column_name: str
86
+ file_columns: list[str]
87
+ action: ConfigAction | None = None # "None" is implicitly a copy action.
88
+ format: str | None = None
89
+
90
+
91
+ class FileConfig(BaseModel):
92
+ """Configuration information that details how a file should be processed."""
93
+ client_name: str = ""
94
+ file_name: str = ""
95
+ file_extension: str = 'csv'
96
+ separator: str = ''
97
+ cols: list[FileConfigColumn] = []
98
+ source_system: str = ""
99
+ inbound_directory: str = ""
100
+ archive_directory: str = ""
101
+ date_format: str = ""
102
+ config_id: Optional[str] = Field(default=None,
103
+ exclude=True) # Placeholder to stuff the parent config ID when needed.
104
+
105
+
106
+ class Config(BaseModel):
107
+ id: str = Field(..., alias="_id")
108
+ name: str
109
+ created_by: str
110
+ created_on: datetime = datetime.now(UTC)
111
+ updated_on: datetime = datetime.now(UTC)
112
+ updated_by: Optional[str] = None
113
+ type: ConfigType
114
+ owning_bucket_id: Optional[str] = None
115
+ password_fields: Optional[list[str]] = None
116
+ config: Union[Dict[str, Any], List[Any]]
117
+ alert: Optional[Alert] = Alert()
118
+ max_sync: Optional[MaxSync] = None
119
+
120
+ # I hate this. A config is not necessarily an integration. An Integration can have a config.
121
+ # We should instead model the Integration/Connection to contain max_sync or something like it.
122
+ # TODO: speak with Ben/Nick
123
+ model_config = ConfigDict(populate_by_name=True)
124
+
125
+
126
+ @model_validator(mode='before')
127
+ @classmethod
128
+ def ensure_alert_is_not_none(self, values):
129
+ if isinstance(values, dict):
130
+ if values.get("alert") is None:
131
+ values["alert"] = Alert()
132
+ return values
133
+
134
+ def validate_type(self) -> bool:
135
+ if self.type == "generic":
136
+ return True
137
+
138
+ if self.type == "fileconfig":
139
+ try:
140
+ FileConfig(**self.config)
141
+ return True
142
+ except ValidationError as e:
143
+ print(e)
144
+ return False
145
+
146
+ if self.type == "probeconfig":
147
+ try:
148
+ ProbeConfig(**self.config)
149
+ return True
150
+ except ValidationError as e:
151
+ print(e)
152
+ return False
153
+
154
+ if self.type == "scheduler":
155
+ try:
156
+ SchedulerConfig(**self.config)
157
+ return True
158
+ except ValidationError as e:
159
+ print(e)
160
+ return False
161
+ return True
162
+
163
+ def get_config_value(self):
164
+ if self.type == "generic":
165
+ return self.config
166
+
167
+ if self.type == "fileconfig":
168
+ return FileConfig(**self.config)
169
+
170
+
171
+ class GenericConfig(BaseModel):
172
+ config_id: str
173
+ config: Any
174
+
175
+ class Config:
176
+ arbitrary_types_allowed = True
177
+
178
+
179
+ if __name__ == "__main__":
180
+ doc = {
181
+ "name": "Test Config",
182
+ "created_by": "user123",
183
+ "type": "fileconfig",
184
+ "config": {}
185
+ }
186
+
187
+ config = Config(**doc)
188
+ print(config.alert)
@@ -0,0 +1,19 @@
1
+ parent_keys = [{'field': 'id', 'hide': True},
2
+ {'field': 'source_id', 'filter': True, 'editable': True, 'headerName': 'Source Id'},
3
+ {'field': 'gravitate_id', 'filter': True, 'editable': True, 'headerName': 'Gravitate Id'},
4
+ {'field': 'updated_by', 'filter': True, 'headerName': 'Updated By'},
5
+ {'field': 'updated_on', 'filter': True, 'headerName': 'Updated On', 'type': 'datetime'},
6
+ {'field': 'type', 'filter': True, 'headerName': 'Type'},
7
+ {'field': 'source_system', 'filter': True, 'headerName': 'Source System'},
8
+
9
+ ]
10
+ children_keys = [{'field': 'id', 'hide': True},
11
+ {'field': 'parent_id', 'hide': True},
12
+ {'field': 'source_id', 'filter': True, 'editable': True, 'headerName': 'Source Id'},
13
+ {'field': 'gravitate_id', 'filter': True, 'editable': True, 'headerName': 'Gravitate Id'},
14
+ {'field': 'parent_source_id', 'filter': True, 'headerName': 'Parent Source ID'},
15
+ {'field': 'parent_gravitate_id', 'filter': True, 'headerName': 'Parent Gravitate Id'},
16
+ {'field': 'parent_type', 'filter': True, 'headerName': 'Parent Type'},
17
+ {'field': 'updated_by', 'filter': True, 'headerName': 'Updated By'},
18
+ {'field': 'updated_on', 'filter': True, 'headerName': 'Updated On'},
19
+ ]
@@ -0,0 +1,293 @@
1
+ import uuid
2
+ from enum import Enum
3
+ from typing import Optional, Literal, Self, List
4
+
5
+ from pydantic import BaseModel, computed_field, model_validator, Field
6
+
7
+ from bb_integrations_lib.shared.model import AgGridBaseModel
8
+ from bb_integrations_lib.util.utils import is_valid_goid, is_uuid
9
+
10
+
11
+ class Point(BaseModel):
12
+ lat: float
13
+ lon: float
14
+
15
+
16
+ class CrossroadsEntityType(str, Enum):
17
+ """Describes the type of entity used for crossroads. These are independent of Supply & Dispatch's model. At present
18
+ it's a pretty close match but they will diverge more as more crossroads features are added."""
19
+ site = "site",
20
+ tank = "tank",
21
+ terminal = "terminal",
22
+ product = "product",
23
+ company = "company"
24
+
25
+
26
+ class BaseCrossroadsEntity(AgGridBaseModel):
27
+ """Base Crossroads Entity. All entities have an is_active switch for soft deletion and a GOID for unique identification.
28
+ All entities need a grid. They all need to extend AgGridBaseModel.
29
+ """
30
+ goid: str = Field(..., description="Gravitate Object ID of the record. This field is autogenerated, unique, and immutable.", frozen=True)
31
+ is_active: bool = Field(True)
32
+ record_owner: Optional[str] = Field(None, description="GOID of the company that owns this record. May be null.")
33
+
34
+
35
+ class CrossroadsCompany(BaseCrossroadsEntity):
36
+ """A Compnay is any business entity. It forms the backbone of the relationship network and the integration network and is a component of the entity network."""
37
+ name: str = Field(..., description="Name of the company")
38
+ description: Optional[str] = Field(None, description="Details")
39
+ is_gravitate_customer: bool = Field(False, description="Does this company use at least one Gravitate product?")
40
+ rita_tenant: Optional[str] = Field(None, description="The tenant this company is associated with, if any. Typically only exists for Gravitate customers.")
41
+ ein: Optional[str] = Field(None, description="The Federal Employer Identification Number of the company.")
42
+ is_retailer: bool = Field(False, description="True if the company is ever a retailer.")
43
+ is_wholesaler: bool = Field(False, description="True if the company is ever a wholesaler.")
44
+ is_carrier: bool = Field(False, description="True if the company is ever a carrier.")
45
+ is_supplier: bool = Field(False, description="True if the company is ever a supplier.")
46
+ is_dealer: bool = Field(False, description="True if the company is ever a dealer.")
47
+
48
+ def flatten(self) -> dict:
49
+ obj = self.model_dump(mode="json", exclude={"is_active"})
50
+ return obj
51
+
52
+ @classmethod
53
+ def unflatten(cls, flattened: dict) -> Self:
54
+ goid = flattened.get("goid") or str(uuid.uuid4())
55
+ return cls.model_validate(flattened | {"goid": goid})
56
+
57
+ @model_validator(mode="after")
58
+ def validate_goid(self):
59
+ if not is_valid_goid(self.goid, "company") and not is_uuid(self.goid):
60
+ raise ValueError("GOID must be in the form `company:<number>`")
61
+ return self
62
+
63
+
64
+ class ProductType(str, Enum):
65
+ """Top-level type for products"""
66
+ gas = "gas",
67
+ diesel = "diesel",
68
+ jet = "jet",
69
+ ethanol = "ethanol",
70
+ bio = "bio"
71
+
72
+
73
+ class ProductSubtype(str, Enum):
74
+ """Subtype for products. Some of these are only valid for certain product types. This is enforced by a validator on the Product entity."""
75
+ rbob = "RBOB",
76
+ cbob = "CBOB",
77
+ carb = "CARBOB",
78
+ ulsd = "ULSD"
79
+
80
+
81
+ gas_subtypes = [ProductSubtype.rbob, ProductSubtype.cbob, ProductSubtype.carb]
82
+ diesel_subtypes = [ProductSubtype.ulsd]
83
+
84
+
85
+ class CrossroadsProduct(BaseCrossroadsEntity):
86
+ """A fully-described product in the crossroads network."""
87
+ type: ProductType = Field(..., description="Type of product")
88
+ subtype: ProductSubtype = Field(..., description="Subtype of product")
89
+ name: str = Field(..., description="Name of the product")
90
+ gas_octane: Optional[int] = Field(None, description="Octane rating. type == gas only.")
91
+ gas_ethanol: Optional[int] = Field(None, description="Ethanol component as a percentage. type == gas only.")
92
+ gas_rvp: Optional[float] = Field(None, description="Reid Vapor Pressure. type == gas only.")
93
+ gas_formulation: Optional[Literal["Conv", "RFG"]] = Field(None, description="Formulation of the gas. type == gas only.")
94
+ diesel_no: Optional[int] = Field(None, description="Diesel formulation number. type == diesel only.")
95
+ diesel_bio: Optional[int] = Field(None, description="Diesel bio component as a percentage. type == diesel only.")
96
+ diesel_winter: Optional[bool] = Field(None, description="Whether this is a winter diesel blend. type == diesel only.")
97
+ diesel_dyed: Optional[bool] = Field(None, description="Whether this is a dyed diesel product. type == diesel only.")
98
+ diesel_additive: Optional[bool] = Field(None, description="Whether this product has additives. type == diesel only.")
99
+ # More properties planned for other product types
100
+
101
+ @computed_field
102
+ @property
103
+ def description(self) -> str:
104
+ if self.type == "gas":
105
+ return f"Gasoline {self.subtype.value} {self.gas_octane} E{self.gas_ethanol} {self.gas_rvp} RVP {self.gas_formulation}"
106
+ if self.type == "diesel":
107
+ return f"Diesel {self.subtype.value} #{self.diesel_no} B{self.diesel_bio} {"winter blend" if self.diesel_winter else "summer blend"} {"dyed" if self.diesel_dyed else "clear"} {"w/ additive" if self.diesel_additive else ""}"
108
+ return ""
109
+
110
+ def flatten(self) -> dict:
111
+ obj = self.model_dump(mode="json", exclude={"is_active"})
112
+ return obj
113
+
114
+ @classmethod
115
+ def unflatten(cls, flattened: dict) -> Self:
116
+ goid = flattened.get("goid") or str(uuid.uuid4())
117
+ return cls.model_validate(flattened | {"goid": goid})
118
+
119
+ @model_validator(mode="after")
120
+ def validate_fields(self):
121
+ if self.type == "gas":
122
+ if self.gas_octane is None or self.gas_ethanol is None or self.gas_rvp is None or self.gas_formulation is None:
123
+ raise ValueError("Gas product must have gas_octane, gas_ethanol, gas_rvp, and gas_formulation fields")
124
+ if self.diesel_no is not None or self.diesel_bio is not None or self.diesel_winter is not None or self.diesel_dyed is not None or self.diesel_additive is not None:
125
+ raise ValueError(
126
+ "Gas product cannot have diesel_no, diesel_bio, diesel_winter, diesel_dyed, or diesel_additive fields")
127
+ if self.subtype not in gas_subtypes:
128
+ raise ValueError(f"Gas product must have one of the following subtypes: {gas_subtypes}")
129
+ if self.type == "diesel":
130
+ if self.diesel_no is None or self.diesel_bio is None or self.diesel_winter is None or self.diesel_dyed is None or self.diesel_additive is None:
131
+ raise ValueError(
132
+ "Diesel product must have diesel_no, diesel_bio, diesel_winter, diesel_dyed, and diesel_additive fields")
133
+ if self.gas_octane is not None or self.gas_ethanol is not None or self.gas_rvp is not None or self.gas_formulation is not None:
134
+ raise ValueError("Diesel product cannot have gas_octane, gas_ethanol, gas_rvp, or gas_formulation fields")
135
+ if self.subtype not in diesel_subtypes:
136
+ raise ValueError(f"Diesel product must have one of the following subtypes: {diesel_subtypes}")
137
+ return self
138
+
139
+
140
+ class SiteStatus(str, Enum):
141
+ open = "open",
142
+ closed = "closed"
143
+
144
+
145
+ class CrossroadsSite(BaseCrossroadsEntity):
146
+ """Describes a site in the crossroads network."""
147
+ federal_site_id: Optional[str] = Field(None, description="Facility ID from the US EPA, if available.")
148
+ name: str = Field(..., description="Name of the site")
149
+ address: str = Field(..., description="Street address of the site")
150
+ city: str = Field(..., description="City")
151
+ state: str = Field(..., description="State (not abbreviated)")
152
+ country: str = Field(..., description="Country (not abbreviated)")
153
+ postal_code: str = Field(..., description="Postal code")
154
+ location: Point = Field(..., description="Geographic location of the site")
155
+ geofence: Optional[List[Point]] = Field(None, description="A list of points that form a polygon defining the geofence for this site. Points should be in order, with the first and last points being the same.", )
156
+ status: SiteStatus = Field("open", description="Whether the site is open or closed")
157
+
158
+ def flatten(self) -> dict:
159
+ obj = self.model_dump(mode="json", exclude={"is_active", "location", "geofence"})
160
+ obj["lat"] = self.location.lat
161
+ obj["lon"] = self.location.lon
162
+ return obj
163
+
164
+ @classmethod
165
+ def unflatten(cls, flattened: dict) -> Self:
166
+ location = {"lat": flattened.get("lat", 0), "lon": flattened.get("lon", 0)}
167
+ goid = flattened.get("goid") or str(uuid.uuid4())
168
+ return cls.model_validate(flattened | {"location": location, "goid": goid})
169
+
170
+ @model_validator(mode="after")
171
+ def validate_goid(self):
172
+ if not is_valid_goid(self.goid, "site") and not is_uuid(self.goid):
173
+ raise ValueError("goid must be in the form `site:<number>`")
174
+ if self.record_owner is not None and not is_valid_goid(self.record_owner, "company"):
175
+ raise ValueError("record_owner must be a GOID in the form `company:<number>`")
176
+ return self
177
+
178
+ @model_validator(mode="after")
179
+ def validate_geofence(self):
180
+ if self.geofence is None:
181
+ return self
182
+ if len(self.geofence) < 3:
183
+ raise ValueError("Geofence must contain at least 3 points")
184
+ if self.geofence[0] != self.geofence[-1]:
185
+ raise ValueError("First and last points of geofence must be the same")
186
+ return self
187
+
188
+
189
+ class TankStatus(str, Enum):
190
+ open = "open",
191
+ closed = "closed",
192
+ transitioning = "transitioning"
193
+
194
+
195
+ class CrossroadsTank(BaseCrossroadsEntity):
196
+ """Describes a tank in the crossroads network."""
197
+ tank_id: str = Field(..., description="ID of the tank. Must be unique within the site.")
198
+ federal_tank_id: Optional[str] = Field(None, description="Federal Tank ID, if available.")
199
+ inventory_manager: Optional[str] = Field(None, description="GOID of the company that manages the inventory for this tank. If it is null, the inventory manager is unknown.")
200
+ site: str = Field(..., description="The site that this tank belongs to.")
201
+ product: str = Field(..., description="ID of the product that this tank is for.")
202
+ brand: Literal["Branded", "Unbranded"] = Field(..., description="Whether this tank has a branded or unbranded product.")
203
+ tank_size: int = Field(..., description="Volume of the tank in gallons.")
204
+ status: TankStatus = Field("open", description="Status of the tank. If 'transitioning', see the transitioning fields for details.")
205
+ transitioning_to: Optional[str] = Field(None, description="GOID of the product that this tank is transitioning to. If null, this tank is not transitioning.")
206
+ transitioning_from: Optional[str] = Field(None, description="GOID of the product that this tank is transitioning from. If null, this tank is not transitioning.")
207
+ storage_max: Optional[int] = Field(..., description="Maximum storage capacity of the tank in gallons.")
208
+ fuel_bottom: Optional[int] = Field(..., description="Fuel bottom of the tank in gallons.")
209
+
210
+ @computed_field
211
+ @property
212
+ def name(self) -> str:
213
+ return f"Tank {self.tank_id} - {self.product}"
214
+
215
+ def flatten(self) -> dict:
216
+ obj = self.model_dump(mode="json", exclude={"is_active"})
217
+ return obj
218
+
219
+ @classmethod
220
+ def unflatten(cls, flattened: dict) -> Self:
221
+ goid = flattened.get("goid") or str(uuid.uuid4())
222
+ return cls.model_validate(flattened | {"goid": goid})
223
+
224
+ @model_validator(mode="after")
225
+ def validate(self):
226
+ if not is_valid_goid(self.goid, "tank") and not is_uuid(self.goid):
227
+ raise ValueError("goid must be a GOID in the form `tank:<number>`")
228
+ if self.record_owner is not None and not is_valid_goid(self.record_owner, "company"):
229
+ raise ValueError("record_owner must be a GOID in the form `company:<number>`")
230
+ if self.inventory_manager is not None and not is_valid_goid(self.inventory_manager, "company"):
231
+ raise ValueError("inventory_manager must be a GOID in the form `company:<number>`")
232
+ if not is_valid_goid(self.site, "site"):
233
+ raise ValueError("site must be a GOID in the form `site:<number>`")
234
+ if not is_valid_goid(self.product, "product"):
235
+ raise ValueError("product must be a GOID in the form `product:<number>`")
236
+ if self.status == "transitioning" and (self.transitioning_to is None and self.transitioning_from is None):
237
+ raise ValueError("If status is transitioning, transitioning_to_product_id and transitioning_from_product_id must be set")
238
+ if self.transitioning_to is not None and not is_valid_goid(self.transitioning_to, "product"):
239
+ raise ValueError("transitioning_to must be a GOID in the form `product:<number>`")
240
+ if self.transitioning_from is not None and not is_valid_goid(self.transitioning_from, "product"):
241
+ raise ValueError("transitioning_from must be a GOID in the form `product:<number>`")
242
+ return self
243
+
244
+
245
+ class CrossroadsTerminal(BaseCrossroadsEntity):
246
+ """Describes a terminal in the Crossroads Network"""
247
+ tcn: Optional[str] = Field(None, description="Federal Terminal ID. This should be set for most terminals.")
248
+ alternate_id: Optional[str] = Field(None, description="Alternate ID for this terminal. This must be set if tcn is not set.")
249
+ name: str = Field(..., description="Name of the terminal")
250
+ address: str = Field(..., description="Street address of the terminal")
251
+ city: str = Field(..., description="City")
252
+ state: str = Field(..., description="State (not abbreviated)")
253
+ country: str = Field(..., description="Country (not abbreviated)")
254
+ postal_code: str = Field(..., description="Postal code")
255
+ location: Point = Field(..., description="Geographic location of the terminal")
256
+ geofence: Optional[List[Point]] = Field(None, description="A list of points that form a polygon defining the geofence for this terminal. Points should be in order, with the first and last points being the same.", )
257
+ terminal_owner: Optional[str] = Field(None, description="GOID of the company that owns this terminal. If it is null, no link has been made between this record and a Company record.")
258
+ products: List[str] = Field([], description="List of product GOIDs that this terminal can supply.")
259
+ suppliers: List[str] = Field([], description="List of company GOIDs for suppliers that are available at this terminal.")
260
+
261
+ def flatten(self) -> dict:
262
+ obj = self.model_dump(mode="json", exclude={"is_active", "location", "geofence", "products", "suppliers"})
263
+ obj["lat"] = self.location.lat
264
+ obj["lon"] = self.location.lon
265
+ return obj
266
+
267
+ @classmethod
268
+ def unflatten(cls, flattened: dict) -> Self:
269
+ location = {"lat": flattened.get("lat", 0), "lon": flattened.get("lon", 0)}
270
+ goid = flattened.get("goid") or str(uuid.uuid4())
271
+ return cls.model_validate(flattened | {"location": location, "goid": goid})
272
+
273
+ @model_validator(mode="after")
274
+ def validate(self):
275
+ if (self.tcn is None and self.alternate_id is None) or (self.tcn is not None and self.alternate_id is not None):
276
+ raise ValueError("Terminal must have either a tcn or an alternate_id, but not both.")
277
+ if not is_valid_goid(self.goid, "terminal") and not is_uuid(self.goid):
278
+ raise ValueError("goid must be in the form `terminal:<number>`")
279
+ if self.record_owner is not None and not is_valid_goid(self.record_owner, "company"):
280
+ raise ValueError("reco:rd_owner must be a GOID in the form `company:<number>`")
281
+ if self.terminal_owner is not None and not is_valid_goid(self.terminal_owner, "company"):
282
+ raise ValueError("terminal_owner must be a GOID in the form `company:<number>`")
283
+ return self
284
+
285
+ @model_validator(mode="after")
286
+ def validate_geofence(self):
287
+ if self.geofence is None:
288
+ return self
289
+ if len(self.geofence) < 3:
290
+ raise ValueError("Geofence must contain at least 3 points")
291
+ if self.geofence[0] != self.geofence[-1]:
292
+ raise ValueError("First and last points of geofence must be the same")
293
+ return self