Skip to content

Commit b897d8b

Browse files
authored
Merge pull request #6 from fedspendingtransparency/mod/dev-12155-funding-opportunity-loaders
[DEV-12155] Funding Opportunity Extractor and Loader
2 parents 995185d + 86fec4b commit b897d8b

21 files changed

Lines changed: 781 additions & 120 deletions

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto eol=lf

brus_backend_common/config.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@
2020
Pydantic's dotenv will automagically populate it based on it
2121
"""
2222

23+
import logging
2324
import os
2425
import pathlib
26+
27+
import boto3
28+
29+
from dotenv import dotenv_values
30+
from io import StringIO
2531
from pydantic import BaseSettings, SecretStr
2632

2733
from brus_backend_common.helpers.uri import get_jdbc_url_from_pg_uri
@@ -34,6 +40,8 @@
3440

3541
ENV_FILE_PATH = os.path.join(_PROJECT_ROOT_DIR, ".env")
3642

43+
logger = logging.getLogger(__name__)
44+
3745

3846
class DefaultConfig(BaseSettings):
3947
"""Top-level config that defines all configuration variables, and their default, overridable values
@@ -130,6 +138,10 @@ def AWS_S3_ENDPOINT(self):
130138
def AWS_STS_ENDPOINT(self):
131139
return f"sts.{self.AWS_REGION}.amazonaws.com" if not self.IS_LOCAL else f"{self.MINIO_HOST}:{self.MINIO_PORT}"
132140

141+
@property
142+
def AWS_SSM_ENDPOINT(self):
143+
return f"ssm.{self.AWS_REGION}.amazonaws.com" if not self.IS_LOCAL else f"{self.MINIO_HOST}:{self.MINIO_PORT}"
144+
133145
# Buckets
134146
DATA_ARCHIVE_BUCKET: str = ""
135147
DATA_EXTRACTS_BUCKET: str = ""
@@ -198,10 +210,51 @@ def JDBC_METASTORE_URL(self):
198210
MINIO_DATA_DIR: str = ""
199211

200212

213+
# TODO: Update CONFIG to use BaseModel with nested environment variables which can help generate this list automatically
214+
# (ex. BUCKETS__DATA_ARCHIVE -> CONFIG.BUCKETS.DATA_ARCHIVE)
215+
CONFIG_BUCKETS = [attr for attr, value in DefaultConfig().__dict__.items() if attr.endswith("_BUCKET")]
216+
217+
218+
def pull_ssm_config() -> dict:
219+
"""This function lives in the liminal space between CONFIG and helpers.aws, having a hand in both.
220+
While this is essentially more helpers.aws based, that file imports and uses CONFIG values from this file,
221+
which'd result in a circular dependency.
222+
223+
Likewise, we could re-use the helper function below but that'd also run into a circular dependency.
224+
ssm_client = _get_boto3('client', 'ssm')
225+
"""
226+
env_group = "prod" if CONFIG.ENV_CODE == "prod" else "nonprod"
227+
228+
# TODO: Post-FAPC Cleanup
229+
non_fapc_path = f"/{env_group}/brus-backend-common/{CONFIG.ENV_CODE}/.env"
230+
fapc_path = "/kc-dtas/brus/broker/secrets"
231+
secrets_param_name = fapc_path if CONFIG.FAPC else non_fapc_path
232+
233+
ssm_client = boto3.client("ssm", region_name=CONFIG.AWS_REGION)
234+
secrets_yaml_param = ssm_client.get_parameter(Name=secrets_param_name, WithDecryption=True)
235+
return dotenv_values(stream=StringIO(secrets_yaml_param["Parameter"]["Value"]))
236+
237+
201238
CONFIG = DefaultConfig()
202239

203240

204-
def set_brus_config(config):
241+
def set_brus_config(config: dict) -> None:
205242
"""Takes in a config dict of the attributes to override"""
206243
for attr, value in config.items():
207244
setattr(CONFIG, attr, value)
245+
246+
247+
# Overwrite any values with ones pulled from SSM if not local
248+
# Note: DefaultConfig() can take the argument, but we need the initial default values to look up the right
249+
# Parameter values, so we're updating them after the initial pull.
250+
if not CONFIG.IS_LOCAL:
251+
ssm_config = pull_ssm_config()
252+
CONFIG = DefaultConfig(**ssm_config)
253+
254+
# Overwrite any values if running in a test to prevent conflicting with your local environment
255+
# https://docs.pytest.org/en/stable/example/simple.html#detect-if-running-from-within-a-pytest-run
256+
if os.environ.get("PYTEST_VERSION") is not None:
257+
# Rename the buckets in the test environment to not overwrite your local buckets
258+
CONFIG = DefaultConfig(
259+
**{bucket_config: f"test-{getattr(CONFIG, bucket_config)}" for bucket_config in CONFIG_BUCKETS}
260+
)

brus_backend_common/helpers/aws.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ def get_storage_options() -> dict:
4444
"aws_sts_endpoint": (
4545
f"http://{CONFIG.AWS_STS_ENDPOINT}" if CONFIG.IS_LOCAL else f"https://{CONFIG.AWS_STS_ENDPOINT}"
4646
),
47+
"aws_ssm_endpoint": (
48+
f"http://{CONFIG.AWS_SSM_ENDPOINT}" if CONFIG.IS_LOCAL else f"https://{CONFIG.AWS_SSM_ENDPOINT}"
49+
),
4750
"region": CONFIG.AWS_REGION,
4851
"allow_http": "true",
4952
"aws_conditional_put": "etag",
@@ -134,6 +137,9 @@ def _get_boto3(method_name: str, *args, region_name=CONFIG.AWS_REGION, **kwargs)
134137
"""
135138
attr = getattr(boto3, method_name)
136139
kwargs.update({"region_name": region_name})
140+
endpoint = None
141+
if len(args) > 0 and args[0].upper() in ("S3", "SSM", "STS"):
142+
endpoint = getattr(CONFIG, f"AWS_{args[0].upper()}_ENDPOINT")
137143

138144
if callable(attr):
139145
if CONFIG.IS_LOCAL:
@@ -143,9 +149,8 @@ def _get_boto3(method_name: str, *args, region_name=CONFIG.AWS_REGION, **kwargs)
143149
aws_secret_access_key=CONFIG.AWS_SECRET_KEY.get_secret_value(),
144150
)
145151
attr = getattr(session, method_name)
146-
kwargs.update({"endpoint_url": f"http://{CONFIG.AWS_S3_ENDPOINT}"})
147-
else:
148-
kwargs.update({"endpoint_url": f"https://{CONFIG.AWS_S3_ENDPOINT}"})
152+
if endpoint:
153+
kwargs.update({"endpoint_url": f"http{'s' if not CONFIG.IS_LOCAL else ''}://{endpoint}"})
149154
return attr(*args, **kwargs)
150155
return attr
151156

brus_backend_common/models/__init__.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
from brus_backend_common.models.lakehouse_model import LakeHouseCurrentMigration, ExternalDataLoadDate
22

3-
from brus_backend_common.models.reference import DEFCBronze, DEFCGold, DEFCGroup
4-
from brus_backend_common.config import CONFIG
5-
6-
LAKEHOUSE_BUCKETS = {
7-
"broker": CONFIG.LAKEHOUSE_BROKER_BUCKET,
8-
"reference": CONFIG.LAKEHOUSE_REFERENCE_BUCKET,
9-
"usas": CONFIG.LAKEHOUSE_USAS_BUCKET,
10-
}
11-
LAKEHOUSE_BUCKET_NAMES = list(LAKEHOUSE_BUCKETS.values())
3+
from brus_backend_common.models.reference import DEFCBronze, DEFCGold, DEFCGroup, FONBronze, FONGold
124

135
LAKEHOUSE_MODEL_CLASSES = [
146
DEFCBronze,
157
DEFCGroup,
168
DEFCGold,
9+
FONBronze,
10+
FONGold,
1711
LakeHouseCurrentMigration,
1812
ExternalDataLoadDate,
1913
]

0 commit comments

Comments
 (0)