diff --git a/.github/workflows/pipeline-run-check.yaml b/.github/workflows/pipeline-run-check.yaml index 2b88e543..e9f3a40b 100644 --- a/.github/workflows/pipeline-run-check.yaml +++ b/.github/workflows/pipeline-run-check.yaml @@ -73,7 +73,7 @@ jobs: for disease in COVID-19 Influenza RSV; do for location in US CA MT DC; do echo "Fitting EpiAutoGP weekly NHSN model for $disease, $location" - uv run bash pipelines/tests/test_epiautogp_fit.sh ${{ env.BASE_DIR }} \ + bash pipelines/tests/test_epiautogp_fit.sh ${{ env.BASE_DIR }} \ "$disease" "$location" nhsn epiweekly done done @@ -116,7 +116,7 @@ jobs: for disease in COVID-19 Influenza RSV; do for location in US CA MT DC; do echo "Fitting EpiAutoGP weekly NSSP percentage model for $disease, $location" - uv run bash pipelines/tests/test_epiautogp_fit.sh ${{ env.BASE_DIR }} \ + bash pipelines/tests/test_epiautogp_fit.sh ${{ env.BASE_DIR }} \ "$disease" "$location" nssp epiweekly pct done done @@ -159,7 +159,7 @@ jobs: for disease in COVID-19 Influenza RSV; do for location in US CA MT DC; do echo "Fitting EpiAutoGP daily NSSP count model for $disease, $location" - uv run bash pipelines/tests/test_epiautogp_fit.sh ${{ env.BASE_DIR }} \ + bash pipelines/tests/test_epiautogp_fit.sh ${{ env.BASE_DIR }} \ "$disease" "$location" nssp daily observed done done @@ -202,7 +202,7 @@ jobs: for disease in COVID-19 Influenza RSV; do for location in US CA MT DC; do echo "Fitting EpiAutoGP daily NSSP other ED visits model for $disease, $location" - uv run bash pipelines/tests/test_epiautogp_fit.sh ${{ env.BASE_DIR }} \ + bash pipelines/tests/test_epiautogp_fit.sh ${{ env.BASE_DIR }} \ "$disease" "$location" nssp daily other done done diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c4875082..ad9caf3c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -133,3 +133,7 @@ jobs: - name: Run EpiAutoGP parquet interop test run: | uv run pytest pipelines/tests/test_epiautogp_parquet_interop.py -q + + - name: Run direct NowcastAutoGP juliacall parquet test + run: | + uv run --no-dev --group test --group epiautogp-julia pytest pipelines/tests/test_epiautogp_juliacall_runner_parquet.py -q diff --git a/pipelines/epiautogp/forecast_epiautogp.py b/pipelines/epiautogp/forecast_epiautogp.py index 31f04adf..dc07e096 100644 --- a/pipelines/epiautogp/forecast_epiautogp.py +++ b/pipelines/epiautogp/forecast_epiautogp.py @@ -6,11 +6,10 @@ convert_to_epiautogp_json, setup_forecast_pipeline, ) +from pipelines.epiautogp.juliacall_runner import run_nowcastautogp_forecast from pipelines.utils.cli_utils import add_common_forecast_arguments from pipelines.utils.common_utils import ( parse_exclude_date_ranges, - run_julia_code, - run_julia_script, ) @@ -21,7 +20,7 @@ def run_epiautogp_forecast( execution_settings: dict, ) -> None: """ - Run EpiAutoGP forecasting model using Julia. + Run EpiAutoGP forecasting via NowcastAutoGP using juliacall. Parameters ---------- @@ -40,7 +39,6 @@ def run_epiautogp_forecast( - smc_data_proportion: Proportion of data used in each SMC step execution_settings : dict Execution settings for the Julia environment. Expected keys: - - project: Julia project name - threads: Number of threads to use or "auto" Returns @@ -50,42 +48,22 @@ def run_epiautogp_forecast( Raises ------ RuntimeError - If Julia environment setup or script execution fails + If Julia setup or model execution fails Notes ----- - This function sets up the EpiAutoGP Julia environment and runs the - forecasting script. The output is saved to model_dir. + This function keeps the existing Python pipeline interface while delegating + the model fit and forecast to NowcastAutoGP through the internal juliacall + runner. The output is saved to model_dir. """ # Ensure output directory exists model_dir.mkdir(parents=True, exist_ok=True) - # Instantiate julia environment for EpiAutoGP - run_julia_code( - """ - using Pkg - Pkg.activate("EpiAutoGP") - Pkg.instantiate() - """, - function_name="setup_epiautogp_environment", - ) - - # Add path arguments to pass to EpiAutoGP - params["json-input"] = str(json_input_path) - params["output-dir"] = str(model_dir) - - # Convert Python dict keys (with underscores) to Julia CLI args (with hyphens) - args_to_epiautogp = [ - f"--{key.replace('_', '-')}={value}" for key, value in params.items() - ] - executor_flags = [f"--{key}={value}" for key, value in execution_settings.items()] - - # Run Julia script - run_julia_script( - "EpiAutoGP/run.jl", - args_to_epiautogp, - executor_flags=executor_flags, - function_name="run_epiautogp_forecast", + run_nowcastautogp_forecast( + json_input_path=json_input_path, + model_dir=model_dir, + params=params, + execution_settings=execution_settings, ) return None diff --git a/pipelines/epiautogp/juliacall_runner.py b/pipelines/epiautogp/juliacall_runner.py new file mode 100644 index 00000000..581f3a34 --- /dev/null +++ b/pipelines/epiautogp/juliacall_runner.py @@ -0,0 +1,465 @@ +"""Run EpiAutoGP forecasts by calling NowcastAutoGP through juliacall.""" + +import datetime as dt +import json +import math +import os +import sys +from dataclasses import dataclass +from functools import cache +from pathlib import Path +from typing import Any + +import polars as pl + +VALID_TARGETS = frozenset({"nhsn", "nssp"}) +VALID_FREQUENCIES = frozenset({"daily", "epiweekly"}) +VALID_ED_VISIT_TYPES = frozenset({"observed", "other", "pct"}) + +NSSP_VARIABLE_BY_ED_VISIT_TYPE = { + "observed": "observed_ed_visits", + "other": "other_ed_visits", + "pct": "prop_disease_ed_visits", +} + +JULIA_HELPER_CODE = """ +using Dates +using NowcastAutoGP + +function run_direct_nowcastautogp( + date_strings, + reports, + frequency, + forecast_date_string, + nowcast_date_strings, + nowcast_reports, + n_ahead, + n_forecasts, + transformation_name, + n_particles, + smc_data_proportion, + n_mcmc, + n_hmc + ) + dates = Date.(collect(date_strings)) + report_values = Float64.(collect(reports)) + nowcast_dates = Date.(collect(nowcast_date_strings)) + nowcast_report_values = [ + Float64.(collect(nowcast_report)) for nowcast_report in collect(nowcast_reports) + ] + + transformation, inv_transformation = get_transformations( + String(transformation_name), + report_values + ) + + stable_data_indices = findall(date -> !(date in nowcast_dates), dates) + stable_data_dates = dates[stable_data_indices] + stable_data_values = report_values[stable_data_indices] + + nowcast_data = isempty(nowcast_dates) ? nothing : create_nowcast_data( + nowcast_report_values, + nowcast_dates; + transformation = transformation + ) + + n_ahead_int = Int(n_ahead) + n_forecasts_int = Int(n_forecasts) + forecast_date = Date(String(forecast_date_string)) + time_step = String(frequency) == "epiweekly" ? Week(1) : Day(1) + forecast_dates = [forecast_date + i * time_step for i in 0:n_ahead_int] + + n_forecasts_per_nowcast = isnothing(nowcast_data) ? + n_forecasts_int : + max(1, n_forecasts_int รท length(nowcast_data)) + + transformed_data = create_transformed_data( + stable_data_dates, + stable_data_values; + transformation = transformation + ) + base_model = make_and_fit_model( + transformed_data; + n_particles = Int(n_particles), + smc_data_proportion = Float64(smc_data_proportion), + n_mcmc = Int(n_mcmc), + n_hmc = Int(n_hmc) + ) + + forecasts = if isnothing(nowcast_data) + forecast( + base_model, + forecast_dates, + n_forecasts_per_nowcast; + inv_transformation = inv_transformation + ) + else + forecast_with_nowcasts( + base_model, + nowcast_data, + forecast_dates, + n_forecasts_per_nowcast; + inv_transformation = inv_transformation + ) + end + + return ( + forecast_dates = string.(forecast_dates), + forecasts_by_draw = [Float64.(forecasts[:, draw]) for draw in axes(forecasts, 2)] + ) +end +""" + + +@dataclass(frozen=True) +class EpiAutoGPInput: + dates: list[dt.date] + reports: list[float] + pathogen: str + location: str + target: str + frequency: str + ed_visit_type: str + forecast_date: dt.date + nowcast_dates: list[dt.date] + nowcast_reports: list[list[float]] + + @property + def date_strings(self) -> list[str]: + return [date.isoformat() for date in self.dates] + + @property + def forecast_date_string(self) -> str: + return self.forecast_date.isoformat() + + @property + def nowcast_date_strings(self) -> list[str]: + return [date.isoformat() for date in self.nowcast_dates] + + +def run_nowcastautogp_forecast( + json_input_path: Path, + model_dir: Path, + params: dict[str, Any], + execution_settings: dict[str, Any], +) -> None: + """Run NowcastAutoGP through juliacall and write pipeline samples output.""" + input_data = read_epiautogp_input(json_input_path) + model_dir.mkdir(parents=True, exist_ok=True) + + _configure_juliacall(execution_settings) + julia_module = _get_julia_module() + + try: + result = julia_module.run_direct_nowcastautogp( + input_data.date_strings, + input_data.reports, + input_data.frequency, + input_data.forecast_date_string, + input_data.nowcast_date_strings, + input_data.nowcast_reports, + params["n_ahead"], + params["n_forecast_draws"], + params["transformation"], + params["n_particles"], + params["smc_data_proportion"], + params["n_mcmc"], + params["n_hmc"], + ) + except Exception as exc: + raise RuntimeError("NowcastAutoGP Julia forecast failed") from exc + + forecast_dates = _parse_forecast_dates(result.forecast_dates) + forecasts_by_draw = _convert_forecasts_by_draw(result.forecasts_by_draw) + _write_pipeline_samples(input_data, forecast_dates, forecasts_by_draw, model_dir) + + +def read_epiautogp_input(json_input_path: Path) -> EpiAutoGPInput: + """Read and validate an EpiAutoGP input JSON file.""" + if not json_input_path.exists(): + raise FileNotFoundError(f"EpiAutoGP input JSON not found: {json_input_path}") + + raw_data = json.loads(json_input_path.read_text(encoding="utf-8")) + if not isinstance(raw_data, dict): + raise ValueError("EpiAutoGP input JSON must contain an object.") + + required_fields = [ + "dates", + "reports", + "pathogen", + "location", + "target", + "frequency", + "ed_visit_type", + "forecast_date", + "nowcast_dates", + "nowcast_reports", + ] + missing_fields = [field for field in required_fields if field not in raw_data] + if missing_fields: + raise ValueError( + "EpiAutoGP input JSON is missing required field(s): " + f"{', '.join(missing_fields)}" + ) + + input_data = EpiAutoGPInput( + dates=_parse_date_list(raw_data["dates"], "dates"), + reports=_parse_number_list(raw_data["reports"], "reports"), + pathogen=_parse_non_empty_string(raw_data["pathogen"], "pathogen"), + location=_parse_non_empty_string(raw_data["location"], "location"), + target=_parse_non_empty_string(raw_data["target"], "target"), + frequency=_parse_non_empty_string(raw_data["frequency"], "frequency"), + ed_visit_type=_parse_non_empty_string( + raw_data["ed_visit_type"], "ed_visit_type" + ), + forecast_date=_parse_date(raw_data["forecast_date"], "forecast_date"), + nowcast_dates=_parse_date_list(raw_data["nowcast_dates"], "nowcast_dates"), + nowcast_reports=_parse_number_matrix( + raw_data["nowcast_reports"], "nowcast_reports" + ), + ) + validate_epiautogp_input(input_data) + return input_data + + +def validate_epiautogp_input(input_data: EpiAutoGPInput) -> None: + """Validate an EpiAutoGP input object before passing it to Julia.""" + if input_data.target not in VALID_TARGETS: + raise ValueError( + f"target must be one of {sorted(VALID_TARGETS)}, got {input_data.target!r}" + ) + + if input_data.frequency not in VALID_FREQUENCIES: + raise ValueError( + "frequency must be one of " + f"{sorted(VALID_FREQUENCIES)}, got {input_data.frequency!r}" + ) + + if input_data.ed_visit_type not in VALID_ED_VISIT_TYPES: + raise ValueError( + "ed_visit_type must be one of " + f"{sorted(VALID_ED_VISIT_TYPES)}, got {input_data.ed_visit_type!r}" + ) + + if input_data.target == "nhsn" and input_data.frequency == "daily": + raise ValueError("NHSN data is only available in epiweekly frequency.") + + if input_data.target == "nhsn" and input_data.ed_visit_type != "observed": + raise ValueError("For NHSN, ed_visit_type must be 'observed'.") + + if len(input_data.dates) != len(input_data.reports): + raise ValueError( + "Length mismatch: dates " + f"({len(input_data.dates)}) and reports ({len(input_data.reports)}) " + "must have the same length." + ) + + if not input_data.dates: + raise ValueError("Empty data: dates and reports cannot be empty.") + + if input_data.dates != sorted(input_data.dates): + raise ValueError("Date ordering: dates must be sorted chronologically.") + + if input_data.nowcast_dates != sorted(input_data.nowcast_dates): + raise ValueError( + "Nowcast date ordering: nowcast_dates must be sorted chronologically." + ) + + if not input_data.nowcast_dates and input_data.nowcast_reports: + raise ValueError( + "Nowcast consistency error: no nowcast_dates provided but " + "nowcast_reports is not empty." + ) + + if input_data.nowcast_dates and not input_data.nowcast_reports: + raise ValueError( + "Nowcast consistency error: nowcast_dates provided but " + "nowcast_reports is empty." + ) + + for index, report_vector in enumerate(input_data.nowcast_reports, start=1): + if len(report_vector) != len(input_data.nowcast_dates): + raise ValueError( + "Nowcast vector length mismatch at index " + f"{index}: got {len(report_vector)} value(s), expected " + f"{len(input_data.nowcast_dates)}." + ) + + _validate_forecast_date_range(input_data) + + +def _configure_juliacall(execution_settings: dict[str, Any]) -> None: + if "juliacall" in sys.modules: + return + + threads = execution_settings.get("threads", "auto") + os.environ.setdefault("PYTHON_JULIACALL_THREADS", str(threads)) + os.environ.setdefault("PYTHON_JULIACALL_STARTUP_FILE", "no") + + +@cache +def _get_julia_module(): + try: + import juliacall + except ModuleNotFoundError as exc: + raise RuntimeError( + "juliacall is required to run EpiAutoGP through NowcastAutoGP. " + "Install the project dependencies before running this pipeline." + ) from exc + + julia_module = juliacall.newmodule("PyRenewHEWNowcastAutoGP") + julia_module.seval(JULIA_HELPER_CODE) + return julia_module + + +def _write_pipeline_samples( + input_data: EpiAutoGPInput, + forecast_dates: list[dt.date], + forecasts_by_draw: list[list[float]], + model_dir: Path, +) -> None: + variable_name = _variable_name(input_data) + dates: list[dt.date] = [] + values: list[float] = [] + draws: list[int] = [] + + for draw_index, sampled_values in enumerate(forecasts_by_draw, start=1): + if len(sampled_values) != len(forecast_dates): + raise ValueError( + "Forecast shape mismatch: draw " + f"{draw_index} has {len(sampled_values)} value(s), expected " + f"{len(forecast_dates)}." + ) + + for forecast_date, sampled_value in zip(forecast_dates, sampled_values): + dates.append(forecast_date) + values.append(_output_value(input_data, sampled_value)) + draws.append(draw_index) + + forecast_df = pl.DataFrame( + { + "date": dates, + ".value": values, + ".draw": draws, + ".variable": [variable_name] * len(dates), + "resolution": [input_data.frequency] * len(dates), + "geo_value": [input_data.location] * len(dates), + "disease": [input_data.pathogen] * len(dates), + }, + schema={ + "date": pl.Date, + ".value": pl.Float64, + ".draw": pl.Int32, + ".variable": pl.String, + "resolution": pl.String, + "geo_value": pl.String, + "disease": pl.String, + }, + ) + + samples_path = model_dir / "samples.parquet" + samples_path.parent.mkdir(parents=True, exist_ok=True) + forecast_df.write_parquet(samples_path) + + +def _variable_name(input_data: EpiAutoGPInput) -> str: + if input_data.target == "nhsn": + return "observed_hospital_admissions" + + return NSSP_VARIABLE_BY_ED_VISIT_TYPE[input_data.ed_visit_type] + + +def _output_value(input_data: EpiAutoGPInput, value: float) -> float: + if input_data.target == "nssp" and input_data.ed_visit_type == "pct": + return value / 100.0 + return value + + +def _parse_forecast_dates(date_values: Any) -> list[dt.date]: + return [ + _parse_date(str(date_value), "forecast_dates") for date_value in date_values + ] + + +def _convert_forecasts_by_draw(forecasts_by_draw: Any) -> list[list[float]]: + return [ + [float(value) for value in draw_values] for draw_values in forecasts_by_draw + ] + + +def _parse_date_list(value: Any, field_name: str) -> list[dt.date]: + if not isinstance(value, list): + raise ValueError(f"{field_name} must be a list.") + return [_parse_date(item, field_name) for item in value] + + +def _parse_number_matrix(value: Any, field_name: str) -> list[list[float]]: + if not isinstance(value, list): + raise ValueError(f"{field_name} must be a list.") + return [_parse_number_list(item, field_name) for item in value] + + +def _parse_number_list(value: Any, field_name: str) -> list[float]: + if not isinstance(value, list): + raise ValueError(f"{field_name} must be a list.") + + numbers = [] + for item in value: + if isinstance(item, bool) or not isinstance(item, int | float): + raise ValueError(f"{field_name} must contain only numeric values.") + + number = float(item) + if not math.isfinite(number) or number < 0: + raise ValueError( + f"{field_name} must contain only non-negative finite values." + ) + numbers.append(number) + + return numbers + + +def _parse_non_empty_string(value: Any, field_name: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{field_name} must be a non-empty string.") + return value + + +def _parse_date(value: Any, field_name: str) -> dt.date: + if not _is_iso_date(value): + raise ValueError(f"{field_name} must contain dates in YYYY-MM-DD format.") + return dt.date.fromisoformat(value) + + +def _is_iso_date(value: Any) -> bool: + return ( + isinstance(value, str) + and len(value) == 10 + and value[4] == "-" + and value[7] == "-" + and value[:4].isdigit() + and value[5:7].isdigit() + and value[8:10].isdigit() + ) + + +def _validate_forecast_date_range(input_data: EpiAutoGPInput) -> None: + date_range_days = (max(input_data.dates) - min(input_data.dates)).days + days_buffer = max(30, math.ceil(date_range_days / 10)) + + if input_data.forecast_date < min(input_data.dates) - dt.timedelta( + days=days_buffer + ): + raise ValueError( + "Forecast date " + f"({input_data.forecast_date}) is too far before the data range " + f"({min(input_data.dates)} to {max(input_data.dates)})." + ) + + if input_data.forecast_date > max(input_data.dates) + dt.timedelta( + days=days_buffer + ): + raise ValueError( + "Forecast date " + f"({input_data.forecast_date}) is too far after the data range " + f"({min(input_data.dates)} to {max(input_data.dates)})." + ) diff --git a/pipelines/juliapkg.json b/pipelines/juliapkg.json new file mode 100644 index 00000000..48ebc7a8 --- /dev/null +++ b/pipelines/juliapkg.json @@ -0,0 +1,10 @@ +{ + "julia": "1.11", + "packages": { + "NowcastAutoGP": { + "uuid": "7e9f7f4b-f590-4c14-8324-de4fcbed18f7", + "url": "https://github.com/CDCgov/NowcastAutoGP.git", + "rev": "v0.3.0" + } + } +} diff --git a/pipelines/tests/test_epiautogp_fit.sh b/pipelines/tests/test_epiautogp_fit.sh index 36f239d7..7ed84a12 100644 --- a/pipelines/tests/test_epiautogp_fit.sh +++ b/pipelines/tests/test_epiautogp_fit.sh @@ -44,7 +44,7 @@ if [ "$ed_visit_type" != "observed" ]; then cmd_args+=(--ed-visit-type "$ed_visit_type") fi -uv run python pipelines/epiautogp/forecast_epiautogp.py "${cmd_args[@]}" +uv run --no-dev --group epiautogp-julia python pipelines/epiautogp/forecast_epiautogp.py "${cmd_args[@]}" if [ "$?" -ne 0 ]; then echo "TEST-MODE FAIL: EpiAutoGP forecast pipeline failed" diff --git a/pipelines/tests/test_epiautogp_juliacall_runner_parquet.py b/pipelines/tests/test_epiautogp_juliacall_runner_parquet.py new file mode 100644 index 00000000..a26aaf5b --- /dev/null +++ b/pipelines/tests/test_epiautogp_juliacall_runner_parquet.py @@ -0,0 +1,81 @@ +"""Parquet checks for the direct juliacall/NowcastAutoGP EpiAutoGP runner.""" + +import datetime as dt +import importlib.util +import json +from pathlib import Path + +import polars as pl +import pytest + +from pipelines.epiautogp.juliacall_runner import run_nowcastautogp_forecast + + +def _write_pct_input(path: Path) -> None: + dates = [dt.date(2024, 1, 1) + dt.timedelta(days=index) for index in range(20)] + reports = [20.0 + index for index in range(20)] + payload = { + "dates": [date.isoformat() for date in dates], + "reports": reports, + "pathogen": "COVID-19", + "location": "US", + "target": "nssp", + "frequency": "daily", + "ed_visit_type": "pct", + "forecast_date": dates[-1].isoformat(), + "nowcast_dates": [], + "nowcast_reports": [], + } + path.write_text(json.dumps(payload), encoding="utf-8") + + +def test_juliacall_nowcastautogp_runner_writes_pipeline_samples_parquet( + tmp_path, +): + if importlib.util.find_spec("juliacall") is None: + pytest.skip("juliacall is not installed") + + input_path = tmp_path / "epiautogp_input.json" + model_dir = tmp_path / "model" + _write_pct_input(input_path) + + run_nowcastautogp_forecast( + json_input_path=input_path, + model_dir=model_dir, + params={ + "n_ahead": 2, + "n_particles": 1, + "n_mcmc": 3, + "n_hmc": 3, + "n_forecast_draws": 4, + "transformation": "percentage", + "smc_data_proportion": 0.5, + }, + execution_settings={"threads": "1"}, + ) + + samples_path = model_dir / "samples.parquet" + assert samples_path.is_file() + + samples = pl.read_parquet(samples_path) + expected_dates = [ + dt.date(2024, 1, 20), + dt.date(2024, 1, 21), + dt.date(2024, 1, 22), + ] * 4 + expected_draws = [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4] + + assert samples.schema["date"] == pl.Date + assert samples.schema[".draw"] == pl.Int32 + assert samples["date"].to_list() == expected_dates + assert samples[".draw"].to_list() == expected_draws + assert samples[".variable"].unique().to_list() == ["prop_disease_ed_visits"] + assert samples["resolution"].unique().to_list() == ["daily"] + assert samples["geo_value"].unique().to_list() == ["US"] + assert samples["disease"].unique().to_list() == ["COVID-19"] + assert samples[".value"].is_between(0, 1).all() + + lazy_schema = ( + pl.scan_parquet(samples_path).select(pl.col("date").min()).collect().schema + ) + assert lazy_schema["date"] == pl.Date diff --git a/pyproject.toml b/pyproject.toml index e39eb5d9..a78d8311 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,10 +28,16 @@ dependencies = [ [tool.hatch.build.targets.wheel] packages = ["pipelines", "cfa"] -include = ["cfa/stf/forecasttools/test_data/*.nc"] +include = [ + "cfa/stf/forecasttools/test_data/*.nc", + "pipelines/juliapkg.json", +] [tool.hatch.build.targets.sdist] -include = ["cfa/stf/forecasttools/test_data/*.nc"] +include = [ + "cfa/stf/forecasttools/test_data/*.nc", + "pipelines/juliapkg.json", +] [build-system] requires = ["hatchling"] @@ -53,6 +59,17 @@ test = [ "pytest-cov>=5.0.0", "pytest-mpl>=0.17.0" ] +epiautogp-julia = [ + "juliacall>=0.9.31", +] + +[tool.uv] +conflicts = [ + [ + {group = "dev"}, + {group = "epiautogp-julia"}, + ], +] [tool.ruff] diff --git a/uv.lock b/uv.lock index cfe1e4f9..acb75c87 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = "==3.13.*" +conflicts = [[ + { package = "cfa-stf-routine-forecasting", group = "dev" }, + { package = "cfa-stf-routine-forecasting", group = "epiautogp-julia" }, +]] [[package]] name = "adal" @@ -852,7 +856,7 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "cfa-dagster", extra = ["dev"] }, + { name = "cfa-dagster", extra = ["dev"], marker = "extra == 'group-27-cfa-stf-routine-forecasting-dev'" }, { name = "ipykernel" }, { name = "ipywidgets" }, { name = "jupyter" }, @@ -860,6 +864,9 @@ dev = [ { name = "pytest-cov" }, { name = "pytest-mpl" }, ] +epiautogp-julia = [ + { name = "juliacall" }, +] test = [ { name = "pytest" }, { name = "pytest-cov" }, @@ -892,6 +899,7 @@ dev = [ { name = "pytest-cov", specifier = ">=5.0.0" }, { name = "pytest-mpl", specifier = ">=0.17.0" }, ] +epiautogp-julia = [{ name = "juliacall", specifier = ">=0.9.31" }] test = [ { name = "pytest", specifier = ">=8.3.2" }, { name = "pytest-cov", specifier = ">=5.0.0" }, @@ -903,7 +911,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -951,7 +959,7 @@ name = "click" version = "8.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } wheels = [ @@ -1100,7 +1108,7 @@ name = "cryptography" version = "48.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } wheels = [ @@ -1160,10 +1168,10 @@ dependencies = [ { name = "grpcio-health-checking" }, { name = "jinja2" }, { name = "protobuf" }, - { name = "psutil", marker = "sys_platform == 'win32'" }, + { name = "psutil", marker = "sys_platform == 'win32' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, { name = "python-dotenv" }, { name = "pytz" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, { name = "requests" }, { name = "rich" }, { name = "six" }, @@ -1246,7 +1254,7 @@ dependencies = [ { name = "click-aliases" }, { name = "dagster-cloud-cli" }, { name = "dagster-shared" }, - { name = "gql", extra = ["requests"] }, + { name = "gql", extra = ["requests"], marker = "extra == 'group-27-cfa-stf-routine-forecasting-dev'" }, { name = "jinja2" }, { name = "jsonschema" }, { name = "markdown" }, @@ -1255,7 +1263,7 @@ dependencies = [ { name = "pyyaml" }, { name = "rich" }, { name = "setuptools" }, - { name = "tomlkit" }, + { name = "tomlkit", version = "0.13.2", source = { registry = "https://pypi.org/simple" } }, { name = "typer" }, { name = "typing-extensions" }, { name = "watchdog" }, @@ -1341,7 +1349,8 @@ dependencies = [ { name = "platformdirs" }, { name = "pydantic" }, { name = "pyyaml" }, - { name = "tomlkit" }, + { name = "tomlkit", version = "0.13.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-27-cfa-stf-routine-forecasting-dev'" }, + { name = "tomlkit", version = "0.14.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia' or extra != 'group-27-cfa-stf-routine-forecasting-dev'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/eb/61a5841e5cced34fe247988f5276cd92ce87f80d4370c87eb046e29378e8/dagster_shared-1.13.4.tar.gz", hash = "sha256:4116acb5a78aac852027e43ebad914471e98304fead7992f04c3baec39fa0d2d", size = 95734, upload-time = "2026-05-07T19:50:17.348Z" } @@ -1358,7 +1367,7 @@ dependencies = [ { name = "dagster" }, { name = "dagster-graphql" }, { name = "starlette" }, - { name = "uvicorn", extra = ["standard"] }, + { name = "uvicorn", extra = ["standard"], marker = "extra == 'group-27-cfa-stf-routine-forecasting-dev'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/b2/9d7f93c3f9a712d35910c0c31da1071b8d2098adb97e89c547430374d077/dagster_webserver-1.13.4.tar.gz", hash = "sha256:29da6bf1ce8893d3f53eda2be0edfe8eb0e6ec5cc44139f2df78f0cb3e825331", size = 11750572, upload-time = "2026-05-07T19:45:30.096Z" } wheels = [ @@ -1401,7 +1410,7 @@ name = "docker" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, { name = "requests" }, { name = "urllib3" }, ] @@ -1469,7 +1478,7 @@ name = "faker" version = "40.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "tzdata", marker = "sys_platform == 'win32'" }, + { name = "tzdata", marker = "sys_platform == 'win32' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fa/e5/b16bf568a2f20fe7423282db4a4059dbcadef70e9029c1c106836f8edd84/faker-40.11.1.tar.gz", hash = "sha256:61965046e79e8cfde4337d243eac04c0d31481a7c010033141103b43f603100c", size = 1957415, upload-time = "2026-03-23T14:05:50.233Z" } wheels = [ @@ -1598,7 +1607,7 @@ name = "github3-py" version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyjwt", extra = ["crypto"] }, + { name = "pyjwt", extra = ["crypto"], marker = "extra == 'group-27-cfa-stf-routine-forecasting-dev'" }, { name = "python-dateutil" }, { name = "requests" }, { name = "uritemplate" }, @@ -1674,7 +1683,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/15/a643b4ecd09969e30b8a150d5919960caae0abe4f5af75ab040b1ab85e78/greenlet-3.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", size = 623234, upload-time = "2026-04-27T13:02:40.611Z" }, { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, + { url = "https://files.pythonhosted.org/packages/77/18/3b13d5ef1275b0ffaf933b05efa21408ac4ca95823c7411d79682e4fdcff/greenlet-3.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", size = 425243, upload-time = "2026-04-27T13:05:15.689Z" }, { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, @@ -1818,7 +1829,7 @@ name = "humanfriendly" version = "10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyreadline3", marker = "sys_platform == 'win32'" }, + { name = "pyreadline3", marker = "sys_platform == 'win32' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } wheels = [ @@ -1869,7 +1880,7 @@ name = "ipykernel" version = "7.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "appnope", marker = "sys_platform == 'darwin' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, { name = "comm" }, { name = "debugpy" }, { name = "ipython" }, @@ -1893,12 +1904,12 @@ name = "ipython" version = "9.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, { name = "decorator" }, { name = "ipython-pygments-lexers" }, { name = "jedi" }, { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "pexpect", marker = "(sys_platform != 'emscripten' and sys_platform != 'win32') or (sys_platform == 'emscripten' and extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia') or (sys_platform == 'win32' and extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, { name = "prompt-toolkit" }, { name = "psutil" }, { name = "pygments" }, @@ -2085,6 +2096,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "juliacall" +version = "0.9.31" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "juliapkg" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/4a/39d774d53281ada1ccaac599646637e779192e688808228e8943a2b0ca77/juliacall-0.9.31.tar.gz", hash = "sha256:2b5c6abdde0b34d14ef2874e3cb82547700a142fdd8376d96f310b44efb412c2", size = 495355, upload-time = "2025-12-17T13:17:45.66Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1d/d6d77bd84c5261a5cbcb17e5000527d0f895e512545bd442caf28a7e3336/juliacall-0.9.31-py3-none-any.whl", hash = "sha256:171dd97489a855336558c49fea2eb26dcbeb1ddb1344acaddf99fff2a5a1b9ad", size = 12260, upload-time = "2025-12-17T13:17:42.677Z" }, +] + +[[package]] +name = "juliapkg" +version = "0.1.23" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "semver" }, + { name = "tomli" }, + { name = "tomlkit", version = "0.14.0", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/c9/827f4575c3bed01255ed646bb4dc4cade483a6710d7869d2fadb2f549c59/juliapkg-0.1.23.tar.gz", hash = "sha256:b61cc55a9d6c99643b22915920c614cfd28e70ca1dadf72f7ee2327d36e66477", size = 23434, upload-time = "2026-02-16T11:34:35.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/9d/1f4495bf047e61e904a69242941aca04ad3c7453639d9019c5d8facb3862/juliapkg-0.1.23-py3-none-any.whl", hash = "sha256:195eb0986d83c7e4df7781290d1158e5dd8809dada634ff2e8196ea16608f1a0", size = 21890, upload-time = "2026-02-16T11:34:34.481Z" }, +] + [[package]] name = "jupyter" version = "1.1.1" @@ -2197,7 +2235,7 @@ dependencies = [ { name = "nbformat" }, { name = "packaging" }, { name = "prometheus-client" }, - { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, { name = "pyzmq" }, { name = "send2trash" }, { name = "terminado" }, @@ -2215,7 +2253,7 @@ name = "jupyter-server-terminals" version = "0.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, { name = "terminado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } @@ -3535,7 +3573,7 @@ name = "pynacl" version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } wheels = [ @@ -3609,7 +3647,7 @@ name = "pytest" version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, @@ -3734,7 +3772,7 @@ name = "pyzmq" version = "27.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "implementation_name == 'pypy'" }, + { name = "cffi", marker = "implementation_name == 'pypy' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } wheels = [ @@ -3981,6 +4019,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, ] +[[package]] +name = "semver" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, +] + [[package]] name = "send2trash" version = "2.1.0" @@ -4031,7 +4078,7 @@ name = "sqlalchemy" version = "2.0.49" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } @@ -4131,8 +4178,8 @@ name = "terminado" version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "os_name != 'nt'" }, - { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "ptyprocess", marker = "os_name != 'nt' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, + { name = "pywinpty", marker = "os_name == 'nt' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, { name = "tornado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } @@ -4197,6 +4244,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955, upload-time = "2024-08-14T08:19:40.05Z" }, ] +[[package]] +name = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + [[package]] name = "toposort" version = "1.10" @@ -4228,7 +4284,7 @@ name = "tqdm" version = "4.67.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-27-cfa-stf-routine-forecasting-dev' and extra == 'group-27-cfa-stf-routine-forecasting-epiautogp-julia')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } wheels = [