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,192 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import Iterable, Self
|
|
3
|
+
|
|
4
|
+
import pytz
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from bb_integrations_lib.gravitate.rita_api import GravitateRitaAPI
|
|
8
|
+
from bb_integrations_lib.mappers.rita_mapper import RitaAPICachedMappingProvider
|
|
9
|
+
from bb_integrations_lib.pipelines.parsers.tank_reading_parser import TankReadingParser
|
|
10
|
+
from bb_integrations_lib.pipelines.steps.importing.load_imap_attachment_step import LoadIMAPAttachmentStep
|
|
11
|
+
from bb_integrations_lib.pipelines.steps.importing.sftp_file_config_step import SFTPFileConfigStep
|
|
12
|
+
from bb_integrations_lib.pipelines.steps.null_step import NullStep
|
|
13
|
+
from bb_integrations_lib.pipelines.steps.processing.archive_gcs_step import ArchiveGCSStep
|
|
14
|
+
from bb_integrations_lib.pipelines.steps.processing.archive_sftp_step import ArchiveSFTPStep
|
|
15
|
+
from bb_integrations_lib.pipelines.steps.processing.bbd_upload_tankreading_step import BBDUploadTankReadingStep
|
|
16
|
+
from bb_integrations_lib.pipelines.steps.processing.delete_sftp_step import DeleteSFTPStep
|
|
17
|
+
from bb_integrations_lib.pipelines.steps.processing.file_config_parser_step_v2 import FileConfigParserV2
|
|
18
|
+
from bb_integrations_lib.pipelines.wrappers.wrapper import PipelineWrapper
|
|
19
|
+
from bb_integrations_lib.protocols.pipelines import JobPipeline
|
|
20
|
+
from bb_integrations_lib.secrets import SecretProvider
|
|
21
|
+
from bb_integrations_lib.secrets.factory import APIFactory
|
|
22
|
+
from bb_integrations_lib.shared.model import ImportTankReadings, ATGConfig, ConfigMode
|
|
23
|
+
from bb_integrations_lib.util.config.manager import GlobalConfigManager
|
|
24
|
+
from bb_integrations_lib.util.config.model import GlobalConfig
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BBDImportTankReadingsPipeline(JobPipeline):
|
|
28
|
+
@classmethod
|
|
29
|
+
async def create(
|
|
30
|
+
cls,
|
|
31
|
+
config: ATGConfig,
|
|
32
|
+
secret_provider: SecretProvider,
|
|
33
|
+
rita_client: GravitateRitaAPI,
|
|
34
|
+
bucket_name: str,
|
|
35
|
+
config_name: str,
|
|
36
|
+
tenant_name: str,
|
|
37
|
+
send_reports: bool = True
|
|
38
|
+
):
|
|
39
|
+
api_factory = APIFactory(secret_provider)
|
|
40
|
+
|
|
41
|
+
steps = [{
|
|
42
|
+
"id": "start",
|
|
43
|
+
"parent_id": None,
|
|
44
|
+
"step": NullStep({})
|
|
45
|
+
}]
|
|
46
|
+
if config.minutes_back is not None and config.timezone is not None:
|
|
47
|
+
min_date = datetime.now(pytz.timezone(config.timezone)) - timedelta(minutes=config.minutes_back)
|
|
48
|
+
else:
|
|
49
|
+
min_date = None
|
|
50
|
+
|
|
51
|
+
ftp_client = None
|
|
52
|
+
if config.mode == "ftp":
|
|
53
|
+
ftp_client = await api_factory.ftp(config.ftp_credentials)
|
|
54
|
+
pull_step = SFTPFileConfigStep(
|
|
55
|
+
rita_client=rita_client,
|
|
56
|
+
ftp_client=ftp_client,
|
|
57
|
+
bucket_name=bucket_name,
|
|
58
|
+
config_name=config_name,
|
|
59
|
+
mode=ConfigMode.ByName,
|
|
60
|
+
match_mode=config.file_match_type,
|
|
61
|
+
min_date=min_date or datetime.min
|
|
62
|
+
)
|
|
63
|
+
elif config.mode == "email":
|
|
64
|
+
pull_step = LoadIMAPAttachmentStep(
|
|
65
|
+
rita_client=rita_client,
|
|
66
|
+
imap_client=await api_factory.imap(config.email_credentials),
|
|
67
|
+
from_email_address=config.from_email_address,
|
|
68
|
+
delivered_to_email_address=config.delivered_to_email_address,
|
|
69
|
+
to_email_address=config.to_email_address,
|
|
70
|
+
attachment_extension=config.attachment_extension or ".csv",
|
|
71
|
+
bucket_name=bucket_name,
|
|
72
|
+
config_names=[config_name],
|
|
73
|
+
email_subject=config.email_subject
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
raise ValueError(f"Invalid mode: {config.mode}")
|
|
77
|
+
|
|
78
|
+
mapping_provider = RitaAPICachedMappingProvider(rita_client)
|
|
79
|
+
|
|
80
|
+
steps += [
|
|
81
|
+
{
|
|
82
|
+
"id": "pull",
|
|
83
|
+
"parent_id": "start",
|
|
84
|
+
"step": pull_step
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": f"parse_readings",
|
|
88
|
+
"parent_id": "pull",
|
|
89
|
+
"step": FileConfigParserV2(
|
|
90
|
+
rita_client=rita_client,
|
|
91
|
+
mapping_type=config.mapping_type,
|
|
92
|
+
parser=TankReadingParser,
|
|
93
|
+
parser_kwargs={
|
|
94
|
+
"included_payload": {
|
|
95
|
+
"source": f"{tenant_name} flat file tank readings"
|
|
96
|
+
},
|
|
97
|
+
"mapping_provider": mapping_provider,
|
|
98
|
+
"sd_client": await api_factory.sd(config.sd_credentials),
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": f"bbd_upload",
|
|
104
|
+
"parent_id": f"parse_readings",
|
|
105
|
+
"step": BBDUploadTankReadingStep(
|
|
106
|
+
sd_client=await api_factory.sd(config.sd_credentials),
|
|
107
|
+
)
|
|
108
|
+
},
|
|
109
|
+
]
|
|
110
|
+
if config.archive_gcs_bucket_path is not None and config.gcs_credentials is not None:
|
|
111
|
+
steps.append({
|
|
112
|
+
"id": f"archive_gcs",
|
|
113
|
+
"parent_id": f"bbd_upload",
|
|
114
|
+
"alt_input": "pull",
|
|
115
|
+
"step": ArchiveGCSStep(
|
|
116
|
+
gcloud_storage=await api_factory.gcloud_storage(config.gcs_credentials),
|
|
117
|
+
bucket_path=config.archive_gcs_bucket_path,
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
if config.archive_files:
|
|
121
|
+
if ftp_client:
|
|
122
|
+
steps.append({
|
|
123
|
+
"id": f"archive_ftp",
|
|
124
|
+
"parent_id": f"bbd_upload",
|
|
125
|
+
"alt_input": "pull",
|
|
126
|
+
"step": ArchiveSFTPStep(
|
|
127
|
+
ftp_client=ftp_client,
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
else:
|
|
131
|
+
logger.warning("No FTP credentials provided, skipping FTP archive step")
|
|
132
|
+
if config.delete_files:
|
|
133
|
+
if ftp_client:
|
|
134
|
+
steps.append({
|
|
135
|
+
"id": "delete_ftp",
|
|
136
|
+
"parent_id": f"bbd_upload",
|
|
137
|
+
"alt_input": "pull",
|
|
138
|
+
"step": DeleteSFTPStep(
|
|
139
|
+
ftp_client=ftp_client
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
else:
|
|
143
|
+
logger.warning("No FTP credentials provided, skipping FTP delete step")
|
|
144
|
+
config = await rita_client.get_config_by_name(bucket_path="/" + bucket_name, config_name=config_name)
|
|
145
|
+
config_id = config[config_name].config_id
|
|
146
|
+
return cls(
|
|
147
|
+
steps,
|
|
148
|
+
rita_client=rita_client,
|
|
149
|
+
pipeline_name=f"Import tank readings - {config_name}",
|
|
150
|
+
pipeline_config_id=config_id,
|
|
151
|
+
secret_provider=secret_provider,
|
|
152
|
+
send_reports=send_reports
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class MultiImportTankReadingsPipeline:
|
|
157
|
+
def __init__(self, pipelines: Iterable[BBDImportTankReadingsPipeline]):
|
|
158
|
+
self.pipelines = pipelines
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
async def create(cls, config_id: str, tenant_name: str, rita_client: GravitateRitaAPI,
|
|
162
|
+
secret_provider: SecretProvider, send_reports: bool = True) -> Self:
|
|
163
|
+
main_config = await rita_client.get_config_by_id(config_id)
|
|
164
|
+
[bucket] = [b for b in await rita_client.get_all_buckets() if b["_id"] == main_config.owning_bucket_id]
|
|
165
|
+
|
|
166
|
+
import_config = ImportTankReadings.model_validate(main_config.config)
|
|
167
|
+
pipelines = []
|
|
168
|
+
for atg_config in import_config.configs:
|
|
169
|
+
for config_name in atg_config.config_names:
|
|
170
|
+
logger.info(f"Creating pipeline for config {config_name}")
|
|
171
|
+
pipelines.append(await BBDImportTankReadingsPipeline.create(
|
|
172
|
+
config=atg_config,
|
|
173
|
+
secret_provider=secret_provider,
|
|
174
|
+
rita_client=rita_client,
|
|
175
|
+
bucket_name=bucket["name"],
|
|
176
|
+
config_name=config_name,
|
|
177
|
+
tenant_name=tenant_name,
|
|
178
|
+
send_reports=send_reports
|
|
179
|
+
))
|
|
180
|
+
return cls(pipelines)
|
|
181
|
+
|
|
182
|
+
async def run(self) -> None:
|
|
183
|
+
if not self.pipelines:
|
|
184
|
+
raise Exception("No pipelines to run - check config and try again")
|
|
185
|
+
for pipeline in self.pipelines:
|
|
186
|
+
try:
|
|
187
|
+
logger.info(f"Starting pipeline '{pipeline.pipeline_name}'")
|
|
188
|
+
await pipeline.execute()
|
|
189
|
+
logger.info(f"Finished pipeline '{pipeline.pipeline_name}'")
|
|
190
|
+
logger.info("-" * 40)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.exception(f"Failed to run pipeline '{pipeline.pipeline_name}'")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from typing import Tuple, TypeVar, Generic, Type, Optional
|
|
2
|
+
from bb_integrations_lib.gravitate.rita_api import GravitateRitaAPI
|
|
3
|
+
from bb_integrations_lib.models.rita.config import GenericConfig
|
|
4
|
+
from bb_integrations_lib.shared.exceptions import ConfigNotFoundError, ConfigValidationError
|
|
5
|
+
from bb_integrations_lib.util.config.manager import GlobalConfigManager
|
|
6
|
+
from bb_integrations_lib.util.config.model import GlobalConfig, ClientConstructor
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PipelineWrapper(Generic[T]):
|
|
14
|
+
def __init__(self,
|
|
15
|
+
tenant_name: str,
|
|
16
|
+
bucket_name: str,
|
|
17
|
+
config_class: Type[T],
|
|
18
|
+
mode: Optional[str] = "production",
|
|
19
|
+
):
|
|
20
|
+
self.tenant_name = tenant_name
|
|
21
|
+
self.bucket_name = bucket_name
|
|
22
|
+
self.config_class = config_class
|
|
23
|
+
self.config_name: Optional[str] = None
|
|
24
|
+
self.config_manager = GlobalConfigManager()
|
|
25
|
+
self.mode = mode
|
|
26
|
+
|
|
27
|
+
async def load_config(self,
|
|
28
|
+
config_name: str,
|
|
29
|
+
rita_client: GravitateRitaAPI = None,
|
|
30
|
+
) -> Tuple[T, str, str]:
|
|
31
|
+
"""
|
|
32
|
+
Load and validate configuration from Rita API.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Tuple of (parsed_config, config_id, config_name)
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ConfigNotFoundError: If the configuration is not found
|
|
39
|
+
ConfigValidationError: If the configuration fails validation
|
|
40
|
+
"""
|
|
41
|
+
if not rita_client:
|
|
42
|
+
rita_client = GravitateRitaAPI(
|
|
43
|
+
client_id=self.secret_data.prod.rita.client_id,
|
|
44
|
+
client_secret=self.secret_data.prod.rita.client_secret,
|
|
45
|
+
tenant=self.tenant_name,
|
|
46
|
+
)
|
|
47
|
+
try:
|
|
48
|
+
configs = await rita_client.get_config_by_name(
|
|
49
|
+
bucket_path=self.bucket_name,
|
|
50
|
+
config_name=config_name
|
|
51
|
+
)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
msg = f"Failed to retrieve config '{config_name}' from bucket '{self.bucket_name}': {e}"
|
|
54
|
+
raise ConfigNotFoundError(
|
|
55
|
+
msg
|
|
56
|
+
) from e
|
|
57
|
+
|
|
58
|
+
job_config: GenericConfig = configs[config_name]
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
pipeline_config: T = self.config_class.model_validate(job_config.config)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
raise ConfigValidationError(
|
|
64
|
+
f"Failed to validate config '{config_name}' as {self.config_class.__name__}: {e}"
|
|
65
|
+
) from e
|
|
66
|
+
|
|
67
|
+
return pipeline_config, job_config.config_id, config_name
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def secret_data(self) -> GlobalConfig:
|
|
71
|
+
return self.config_manager.get_environment(self.tenant_name)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def api_clients(self) -> ClientConstructor:
|
|
75
|
+
return self.config_manager.environment_from_name(self.tenant_name, self.mode)
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
return (f"PipelineWrapper(tenant='{self.tenant_name}', "
|
|
79
|
+
f"bucket='{self.bucket_name}', "
|
|
80
|
+
f"type={self.config_class.__name__})")
|
|
81
|
+
|
|
File without changes
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Protocol, Iterable, Optional, List, Literal
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pydantic import BaseModel, Field, field_validator
|
|
5
|
+
from bb_integrations_lib.shared.model import RawData, File
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FileStorageClient(Protocol):
|
|
9
|
+
"""Protocol for classes that can upload and download files from some source, when credentials are provided."""
|
|
10
|
+
|
|
11
|
+
def list_files(self, directory: str, credential_file_name: str = None) -> list[str]:
|
|
12
|
+
"""Integration method to list files in a remote directory"""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def get_raw_data(self, min_date: datetime, credential_file_name: str = None, check_date: bool = True) -> \
|
|
16
|
+
Iterable[RawData]:
|
|
17
|
+
"""Integration method to download a file from a remote directory"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def rename_file(self, old_name: str, new_name: str, credential_file_name: str = None) -> None:
|
|
21
|
+
"""Integration method to rename a remote file"""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def delete_file(self, path: str, credential_file_name: str = None) -> None:
|
|
25
|
+
"""Integration method to delete a remote file"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def upload_file(self, file: File, path: str, credential_file_name: str = None) -> None:
|
|
29
|
+
"""Integration method to upload a file to a remote directory"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Integration(Protocol):
|
|
34
|
+
"""
|
|
35
|
+
An integration retrieves data from a source and returns an iterable of RawData objects.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def get_raw_data(self, min_date: datetime = datetime.min) -> Iterable[RawData]:
|
|
39
|
+
"""Method for fetching the raw data from the integration"""
|
|
40
|
+
|
|
41
|
+
def archive_data(self, raw_data: RawData):
|
|
42
|
+
"""Method that archives the data after it has been processed"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Translator(Protocol):
|
|
46
|
+
def translate_row(self, row: dict) -> dict:
|
|
47
|
+
"""Translate the data in the provided row. Returns a new dictionary that has been translated."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Parser(Protocol):
|
|
51
|
+
def parse_raw_data(self, raw_data: RawData) -> Iterable:
|
|
52
|
+
"""Method to process the raw data from the get_raw_data Integration method."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TankMonitorType(str, Enum):
|
|
56
|
+
bbd = "bbd"
|
|
57
|
+
aws = "aws"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TankReading(BaseModel):
|
|
61
|
+
date: str # ISO Datetime
|
|
62
|
+
payload: dict = {}
|
|
63
|
+
store: str
|
|
64
|
+
tank: str
|
|
65
|
+
timezone: str | None
|
|
66
|
+
volume: float
|
|
67
|
+
monitor_type: TankMonitorType
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SalesAdjustedDeliveryReading(BaseModel):
|
|
71
|
+
source: str
|
|
72
|
+
number: str
|
|
73
|
+
store_id: str
|
|
74
|
+
tank_id: str
|
|
75
|
+
product_id: str
|
|
76
|
+
volume: float
|
|
77
|
+
date: datetime
|
|
78
|
+
|
|
79
|
+
class TankSales(BaseModel):
|
|
80
|
+
store_number: str
|
|
81
|
+
tank_id: str
|
|
82
|
+
date: str # ISO Datetime
|
|
83
|
+
sales: float
|
|
84
|
+
|
|
85
|
+
class PELookup(BaseModel):
|
|
86
|
+
"""Lookup model for common source identifiers"""
|
|
87
|
+
SourceSystemId: Optional[int] = None
|
|
88
|
+
SourceId: Optional[int] = None
|
|
89
|
+
SourceId2: Optional[int] = None
|
|
90
|
+
SourceId3: Optional[int] = None
|
|
91
|
+
SourceIdString: Optional[str] = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class PriceInstrumentDTO(BaseModel):
|
|
95
|
+
"""Price Instrument Data Transfer Object"""
|
|
96
|
+
Name: str
|
|
97
|
+
Abbreviation: str
|
|
98
|
+
UnitOfMeasureLookup: Optional[PELookup] = None
|
|
99
|
+
CurrencyLookup: Optional[PELookup] = None
|
|
100
|
+
BookLookup: Optional[PELookup] = None
|
|
101
|
+
ProductLookup: Optional[PELookup] = None
|
|
102
|
+
LocationLookup: Optional[PELookup] = None
|
|
103
|
+
ToCurrencyLookup: Optional[PELookup] = None
|
|
104
|
+
CounterPartyLookup: Optional[PELookup] = None
|
|
105
|
+
IsActive: bool = True
|
|
106
|
+
ExternalReferenceNumber: Optional[str] = None
|
|
107
|
+
SourceId: Optional[int] = None
|
|
108
|
+
SourceIdString: Optional[str] = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class PriceTypeDTO(BaseModel):
|
|
112
|
+
"""Price Type Data Transfer Object"""
|
|
113
|
+
PriceTypeMeaning: str
|
|
114
|
+
ExtractPrices: bool = True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class BulkSyncIntegrationDTO(BaseModel):
|
|
118
|
+
"""Integration Data Transfer Object"""
|
|
119
|
+
Name: Optional[str] = None
|
|
120
|
+
Abbreviation: Optional[str] = None
|
|
121
|
+
PricePublisherTypeMeaning: Optional[str] = None
|
|
122
|
+
IsActive: bool = True
|
|
123
|
+
MatchByType: Literal["SourceId", "SourceIdString"] = None
|
|
124
|
+
PriceInstrumentDTOs: List[PriceInstrumentDTO] = Field(default_factory=list)
|
|
125
|
+
PriceTypeDTOs: List[PriceTypeDTO] = Field(default_factory=list)
|
|
126
|
+
SourceId: Optional[int] = None
|
|
127
|
+
SourceIdString: Optional[str] = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class PeBulkSyncIntegrationOptions(BaseModel):
|
|
132
|
+
"""Options configuration"""
|
|
133
|
+
IsPartialDataSet: bool = True
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class PeBulkSyncIntegration(BaseModel):
|
|
137
|
+
"""Root model for price integration data"""
|
|
138
|
+
Options: PeBulkSyncIntegrationOptions = PeBulkSyncIntegrationOptions()
|
|
139
|
+
IntegrationDtos: List[BulkSyncIntegrationDTO] = Field(default_factory=list)
|
|
140
|
+
SourceSystemId: int = None
|
|
141
|
+
|
|
142
|
+
class PriceMergeValue(BaseModel):
|
|
143
|
+
Value: float
|
|
144
|
+
PriceTypeMeaning: Literal["High", "Low", "Posting", "Average"] = "Posting"
|
|
145
|
+
|
|
146
|
+
class PriceMergeIntegrationDTO(BaseModel):
|
|
147
|
+
PriceInstrumentLookup: Optional[PELookup] = None
|
|
148
|
+
EstimateActual: Literal["A", "S"] = "A"
|
|
149
|
+
EffectiveFromDateTime: str = None
|
|
150
|
+
EffectiveToDateTime: Optional[str] = None
|
|
151
|
+
TradePeriodFromDateTime: Optional[str] = None
|
|
152
|
+
TradePeriodToDateTime: Optional[str] = None
|
|
153
|
+
UnitOfMeasureLookup: Optional[PELookup] = None
|
|
154
|
+
IsActive: Optional[bool] = True
|
|
155
|
+
PriceValues: List[PriceMergeValue]
|
|
156
|
+
|
|
157
|
+
class PePriceMergeIntegration(BaseModel):
|
|
158
|
+
IntegrationDtos: List
|
|
159
|
+
SourceSystemId: Optional[int] = None
|
|
160
|
+
|
|
161
|
+
class DriverCredential(BaseModel):
|
|
162
|
+
driver_id: dict
|
|
163
|
+
expiration_date: str | None # ISO Datetime
|
|
164
|
+
certification_date: str | None # ISO Datetime
|
|
165
|
+
credential_id: str | None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class PriceRow(BaseModel):
|
|
169
|
+
effective_from: str # ISO Datetime
|
|
170
|
+
effective_to: str # ISO Datetime
|
|
171
|
+
price: float
|
|
172
|
+
price_type: str
|
|
173
|
+
contract: Optional[str] = "" # This is to make sure we don't break contracts
|
|
174
|
+
timezone: Optional[str] = "UTC"
|
|
175
|
+
terminal_id: Optional[str] = None
|
|
176
|
+
terminal_source_id: Optional[str] = None
|
|
177
|
+
terminal_source_system_id: Optional[str] = None
|
|
178
|
+
terminal: Optional[str] = None
|
|
179
|
+
product_id: Optional[str] = None
|
|
180
|
+
product_source_id: Optional[str] = None
|
|
181
|
+
product_source_system_id: Optional[str] = None
|
|
182
|
+
product: Optional[str] = None
|
|
183
|
+
supplier_id: Optional[str] = None
|
|
184
|
+
supplier_source_id: Optional[str] = None
|
|
185
|
+
supplier_source_system_id: Optional[str] = None
|
|
186
|
+
counterparty_source_id: Optional[str] = None
|
|
187
|
+
counterparty_source_system_id: Optional[str] = None
|
|
188
|
+
supplier: Optional[str] = None
|
|
189
|
+
enabled: Optional[bool] = None
|
|
190
|
+
disabled_until: Optional[str] = None
|
|
191
|
+
expire: Optional[str] = None
|
|
192
|
+
min_quantity: Optional[str] = None
|
|
193
|
+
max_quantity: Optional[str] = None
|
|
194
|
+
curve_id: Optional[str] = None
|
|
195
|
+
row: Optional[str] = None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class TankSales(BaseModel):
|
|
199
|
+
store_number: str
|
|
200
|
+
tank_id: str
|
|
201
|
+
date: str # ISO Datetime
|
|
202
|
+
sales: float
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class UniqueTank(BaseModel):
|
|
206
|
+
store: str
|
|
207
|
+
tank_id: str
|
|
208
|
+
monitor_type: TankMonitorType
|
|
209
|
+
|
|
210
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from typing import Protocol, Optional, Dict, Any
|
|
2
|
+
from pymongo.database import Database
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class System(Protocol):
|
|
6
|
+
sd: str
|
|
7
|
+
pe: str
|
|
8
|
+
rita: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GravitateConfig(Protocol):
|
|
12
|
+
system_psk: Optional[str]
|
|
13
|
+
url: Optional[str]
|
|
14
|
+
conn_str: Optional[str]
|
|
15
|
+
dbs: Optional[Dict[str, str]]
|
|
16
|
+
system: Optional[System]
|
|
17
|
+
short_name: Optional[str]
|
|
18
|
+
admin_username: Optional[str]
|
|
19
|
+
admin_password: Optional[str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SecureWebAPI(Protocol):
|
|
23
|
+
system_name: str
|
|
24
|
+
username: Optional[str]
|
|
25
|
+
password: Optional[str]
|
|
26
|
+
client_id: Optional[str]
|
|
27
|
+
client_secret: Optional[str]
|
|
28
|
+
psk: str
|
|
29
|
+
base_url: str
|
|
30
|
+
token: Optional[str]
|
|
31
|
+
system: Any
|
|
32
|
+
extra_headers: Dict[str, str]
|
|
33
|
+
|
|
34
|
+
def post(self, url: str, params: Optional[Dict] = None, headers: Optional[Dict] = None,
|
|
35
|
+
data: Optional[Dict] = None, json: Optional[Dict] = None, files: Optional[Dict] = None,
|
|
36
|
+
timeout: int = 180, raise_error: bool = True) -> Any: ...
|
|
37
|
+
|
|
38
|
+
def get(self, url: str, params: Optional[Dict] = None, headers: Optional[Dict] = None,
|
|
39
|
+
timeout: int = 180, raise_error: bool = True) -> Any: ...
|
|
40
|
+
|
|
41
|
+
def token_post(self, url: str, params: Optional[Dict] = None, data: Optional[Dict] = None,
|
|
42
|
+
json: Optional[Dict] = None, files: Optional[Dict] = None, timeout: int = 180,
|
|
43
|
+
raise_error: bool = True, headers: Optional[Dict] = None) -> Any: ...
|
|
44
|
+
|
|
45
|
+
def token_get(self, url: str, params: Optional[Dict] = None, timeout: int = 180,
|
|
46
|
+
raise_error: bool = True) -> Any: ...
|
|
47
|
+
|
|
48
|
+
def psk_post(self, url: str, params: Optional[Dict] = None, data: Optional[Dict] = None,
|
|
49
|
+
json: Optional[Dict] = None, timeout: int = 180, psk_param: str = "system_psk",
|
|
50
|
+
raise_error: bool = True) -> Any: ...
|
|
51
|
+
|
|
52
|
+
def get_service_api(self, service_name: str) -> 'SecureWebAPI': ...
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DatabaseAPI(Protocol):
|
|
56
|
+
dbs: Dict[str, str]
|
|
57
|
+
conn_string: str
|
|
58
|
+
client: Any
|
|
59
|
+
backend: Optional[Database]
|
|
60
|
+
price: Optional[Database]
|
|
61
|
+
payroll: Optional[Database]
|
|
62
|
+
auth: Optional[Database]
|
|
63
|
+
forecast: Optional[Database]
|
|
64
|
+
ims: Optional[Database]
|
|
65
|
+
valuation: Optional[Database]
|
|
66
|
+
|
|
67
|
+
def get_db(self, db_name: str) -> Optional[Database]: ...
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class WebAPIWrapper(Protocol):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class GravitateAPIClient(Protocol):
|
|
75
|
+
system: System
|
|
76
|
+
web: SecureWebAPI
|
|
77
|
+
db: DatabaseAPI
|
|
78
|
+
client_id: Optional[str]
|
|
79
|
+
client_secret: Optional[str]
|
|
80
|
+
base_url: str
|
|
81
|
+
url: str
|
|
82
|
+
system_name: Optional[str]
|
|
83
|
+
short_name: Optional[str]
|
|
84
|
+
username: Optional[str]
|
|
85
|
+
password: Optional[str]
|
|
86
|
+
psk: Optional[str]
|
|
87
|
+
connection_string: Optional[str]
|
|
88
|
+
dbs: Optional[Dict[str, str]]
|
|
89
|
+
api_map: Dict[Any, WebAPIWrapper]
|
|
90
|
+
apis: WebAPIWrapper
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class SDWebAPIWrapper(WebAPIWrapper):
|
|
94
|
+
backend: GravitateAPIClient
|
|
95
|
+
forecast: GravitateAPIClient
|
|
96
|
+
valuation: GravitateAPIClient
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class PEWebAPIWrapper(WebAPIWrapper):
|
|
100
|
+
integration: GravitateAPIClient
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class RitaWebAPIWrapper(WebAPIWrapper):
|
|
104
|
+
backend: GravitateAPIClient
|