diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a1efd3f..72779b0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ { "name": "hydroshift", "build": { - "dockerfile": "Dockerfile", + "dockerfile": "../Dockerfile", "context": ".." } - } +} diff --git a/docker-compose.yml b/docker-compose.yml index e27a8ed..53dafff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,15 +5,31 @@ services: image: ${ECR_AWS_ACCOUNT_ID}.dkr.ecr.${ECR_AWS_REGION}.amazonaws.com/${ECR_CLIENT_IMAGE_TAG} networks: - hydroshift + env_file: + - .env restart: unless-stopped mem_limit: 4GB mem_reservation: 2GB stdin_open: true + + nginx: + container_name: nginx + init: true + image: nginx:stable-alpine + networks: + - hydroshift + ports: + - "8501:80" + volumes: + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf + restart: unless-stopped + mem_limit: 0.5GB + mem_reservation: 0.25GB labels: - 'traefik.enable=true' - - 'traefik.http.routers.client.rule=Host(`${PROD_APP_HOST}`)' - - 'traefik.http.routers.client.tls=true' - - 'traefik.http.routers.client.tls.certresolver=letsencrypt' + - 'traefik.http.routers.nginx.rule=Host(`${PROD_APP_HOST}`)' + - 'traefik.http.routers.nginx.tls=true' + - 'traefik.http.routers.nginx.tls.certresolver=letsencrypt' - 'traefik.docker.network=hydroshift_hydroshift' - 'traefik.http.routers.static.middlewares=secHeaders@file' networks: diff --git a/hydroshift/_pages/changepoint.py b/hydroshift/_pages/changepoint.py index 5911097..69787af 100644 --- a/hydroshift/_pages/changepoint.py +++ b/hydroshift/_pages/changepoint.py @@ -377,7 +377,7 @@ def get_changepoints(data: pd.DataFrame, arl0: int, burn_in: int) -> dict: @st.cache_data(max_entries=MAX_CACHE_ENTRIES) -def ffa_analysis(data: pd.DataFrame, regimes: list): +def ffa_analysis(data: pd.DataFrame, regimes: list, est_method: str, skew_mode: str): """Run multiple flood frequency analyses for different regimes.""" ffas = [] for r in regimes: @@ -388,8 +388,8 @@ def ffa_analysis(data: pd.DataFrame, regimes: list): lp3 = LP3Analysis( st.session_state.gage_id, peaks, - use_map_skew=False, - est_method="MOM", + skew_mode=skew_mode, + est_method=est_method, label=label, ) ffas.append(lp3) @@ -427,9 +427,32 @@ def make_body(): st.header("Modified flood frequency analysis") if len(st.session_state[st.session_state.data_editor_key]["added_rows"]) > 0: + # Options + opt_col_1, opt_col_2 = st.columns(2) + with opt_col_1: + est_method = st.selectbox( + "Estimation Method", + ["L-moments", "Method of moments", "Maximum Likelihood"], + index=1, + ) + est_method = { + "L-moments": "LMOM", + "Method of moments": "MOM", + "Maximum Likelihood": "MLE", + }[est_method] + with opt_col_2: + with st.container(): + skew_mode = st.radio("Skew Source", options=["Station Skew", "Regional Skew", "Weighted Skew"], horizontal=True, disabled=not cpa.gage.has_regional_skew) + + # Analysis and display + if cpa.gage.missing_dates_ams is not None and len(cpa.gage.missing_dates_ams) > 0: + st.warning(f"Missing {len(cpa.gage.missing_dates_ams)} LP3 records") + ffa_plot, ffa_df = ffa_analysis( cpa.gage.ams, st.session_state[st.session_state.data_editor_key]["added_rows"], + est_method, + skew_mode ) if ffa_plot is not None and ffa_df is not None: st.markdown(cpa.ffa_text) diff --git a/hydroshift/utils/data_retrieval.py b/hydroshift/utils/data_retrieval.py index 681e13f..3c479ba 100644 --- a/hydroshift/utils/data_retrieval.py +++ b/hydroshift/utils/data_retrieval.py @@ -16,19 +16,26 @@ logger = logging.getLogger(__name__) +def _call_nwis(func_name: str, *args, **kwargs): + """Log NWIS request metadata and execute the NWIS function.""" + logger.debug("NWIS request: %s | args=%s | kwargs=%s", func_name, args, kwargs) + return getattr(nwis, func_name)(*args, **kwargs) + + class Gage: """A USGS Gage.""" def __init__(self, gage_id: str): """Construct class.""" self.gage_id = self.validate_id(gage_id) - self.site_data = load_site_data(gage_id) - self.data_catalog = get_site_catalog(gage_id) + self.site_data = load_site_data(self.gage_id) + self.data_catalog = get_site_catalog(self.gage_id) @staticmethod def validate_id(idx: str): if idx is None: raise GageNotFoundException() + idx = idx.strip() if not idx.isnumeric(): raise GageNotFoundException() return idx @@ -251,7 +258,7 @@ def get_ams(gage_id): if gage_id == "testing": df = fake_ams() else: - df = nwis.get_record(service="peaks", sites=[gage_id], ssl_check=True) + df = _call_nwis("get_record", service="peaks", sites=[gage_id], ssl_check=True) except NoSitesError: logger.debug(f"Peaks could not be found for gage id: {gage_id}") return pd.DataFrame() @@ -260,6 +267,7 @@ def get_ams(gage_id): {1: "Winter", 2: "Spring", 3: "Summer", 4: "Fall"} ) # TODO: should add labels like Winter(JFM), Spring(AMJ), etc + df = df.replace([np.inf, -np.inf, 0], np.nan) df = df.dropna(subset="peak_va") return df @@ -268,7 +276,7 @@ def get_ams(gage_id): def get_flow_stats(gage_id): """Fetches flow statistics for a given gage.""" try: - df = nwis.get_stats(sites=gage_id, parameterCd="00060", ssl_check=True)[0] + df = _call_nwis("get_stats", sites=gage_id, parameterCd="00060", ssl_check=True)[0] except IndexError: logger.debug(f"Flow stats could not be found for gage_id: {gage_id}") return None @@ -280,7 +288,7 @@ def get_flow_stats(gage_id): def load_site_data(gage_number: str) -> dict: """Query NWIS for site information""" try: - resp = nwis.get_record(sites=gage_number, service="site", ssl_check=True) + resp = _call_nwis("get_record", sites=gage_number, service="site", ssl_check=True) except ValueError: raise GageNotFoundException() @@ -300,7 +308,7 @@ def load_site_data(gage_number: str) -> dict: def get_site_catalog(gage_number: str) -> dict: """Query NWIS for site information""" try: - df = nwis.what_sites(sites=gage_number, seriesCatalogOutput="true", ssl_check=True)[0] + df = _call_nwis("what_sites", sites=gage_number, seriesCatalogOutput="true", ssl_check=True)[0] except Exception as e: logger.error("Error querying site: %s", e, exc_info=True) return df @@ -310,7 +318,7 @@ def get_site_catalog(gage_number: str) -> dict: def get_daily_values(gage_id, start_date, end_date): """Fetches mean daily flow values for a given gage.""" try: - dv = nwis.get_dv(gage_id, start_date, end_date, ssl_check=True, parameterCd="00060")[0] + dv = _call_nwis("get_dv", gage_id, start_date, end_date, ssl_check=True, parameterCd="00060")[0] except Exception: logger.debug(f"Daily Values could not be found for gage_id: {gage_id}") return None @@ -322,7 +330,7 @@ def get_daily_values(gage_id, start_date, end_date): def get_monthly_values(gage_id): """Fetches mean monthly flow values for a given gage and assigns a datetime column based on the year and month.""" try: - mv = nwis.get_stats(gage_id, statReportType="monthly", ssl_check=True, parameterCd="00060")[0] + mv = _call_nwis("get_stats", gage_id, statReportType="monthly", ssl_check=True, parameterCd="00060")[0] except Exception: logger.debug(f"Monthly Values could not be found for gage_id: {gage_id}") return None diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..ff6fc7e --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,25 @@ +server { + listen 80; + server_name hydroshift.dewberryanalytics.com www.hydroshift.dewberryanalytics.com; + index index.php index.html index.htm; + location / { + proxy_pass http://client:80/; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header Content-Security-Policy "default-src * data: blob: 'unsafe-eval' 'unsafe-inline'" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Permissions-Policy "autoplay=(), geolocation=(), camera=(), fullscreen=()"; + } + error_page 404 /404.html; + error_page 500 502 503 504 /50x.html; +location = /50x.html { + root /usr/share/nginx/html; + } +} \ No newline at end of file