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