diff --git a/src/sources/metaculus.py b/src/sources/metaculus.py index c0eec80d..49da7fc3 100644 --- a/src/sources/metaculus.py +++ b/src/sources/metaculus.py @@ -157,7 +157,7 @@ def update( dfq.at[index, "forecast_horizons"] = "N/A" # Build resolution file - df_res = self._create_resolution_file(dfq, index, market) + df_res = self._build_resolution_file(dfq, index, market) if df_res is not None: resolution_files[str(row["id"])] = df_res dfq.at[index, "freeze_datetime_value"] = ( @@ -173,7 +173,7 @@ def update( filename = f"{self.name}/{question_id}.jsonl" if filename not in files_in_storage and question_id not in resolution_files: market = self._get_market(row["id"]) - df_res = self._create_resolution_file(dfq, index, market) + df_res = self._build_resolution_file(dfq, index, market) if df_res is not None: resolution_files[question_id] = df_res @@ -325,12 +325,12 @@ def _get_resolved_market_value(market: dict) -> float: return 0 return np.nan - def _create_resolution_file( + def _build_resolution_file( self, dfq: pd.DataFrame, index: int, market: dict, - ) -> pd.DataFrame | None: + ) -> DataFrame[ResolutionFrame] | None: """Build the resolution file for a market from its aggregation history. Overwrites the resolution file entirely on each run (Metaculus returns the diff --git a/src/tests/test_metaculus.py b/src/tests/test_metaculus.py index 26f4448b..038303d0 100644 --- a/src/tests/test_metaculus.py +++ b/src/tests/test_metaculus.py @@ -155,12 +155,12 @@ def test_only_keeps_id_date_value(self): # --------------------------------------------------------------------------- -# _create_resolution_file +# _build_resolution_file # --------------------------------------------------------------------------- class TestCreateResolutionFile: - """Tests for MetaculusSource._create_resolution_file.""" + """Tests for MetaculusSource._build_resolution_file.""" def _dfq_row(self, resolved=False, resolution_datetime="N/A"): return make_question_df( @@ -179,7 +179,7 @@ def test_empty_history_returns_none(self, metaculus_source): question={"aggregations": {"recency_weighted": {"history": []}}} ) dfq = self._dfq_row() - result = metaculus_source._create_resolution_file(dfq, 0, market) + result = metaculus_source._build_resolution_file(dfq, 0, market) assert result is None def test_single_day_entries_filtered(self, metaculus_source): @@ -201,7 +201,7 @@ def test_single_day_entries_filtered(self, metaculus_source): } ) dfq = self._dfq_row() - result = metaculus_source._create_resolution_file(dfq, 0, market) + result = metaculus_source._build_resolution_file(dfq, 0, market) assert result is None def test_single_day_last_millisecond_kept(self, metaculus_source): @@ -224,7 +224,7 @@ def test_single_day_last_millisecond_kept(self, metaculus_source): } ) dfq = self._dfq_row() - result = metaculus_source._create_resolution_file(dfq, 0, market) + result = metaculus_source._build_resolution_file(dfq, 0, market) assert result is not None assert len(result) >= 1 @@ -252,7 +252,7 @@ def test_backfill_gaps(self, metaculus_source): } ) dfq = self._dfq_row() - result = metaculus_source._create_resolution_file(dfq, 0, market) + result = metaculus_source._build_resolution_file(dfq, 0, market) assert result is not None # Should have continuous dates from start through last date dates_in_result = result["date"].tolist() @@ -282,7 +282,7 @@ def test_deduplication_keeps_last(self, metaculus_source): } ) dfq = self._dfq_row() - result = metaculus_source._create_resolution_file(dfq, 0, market) + result = metaculus_source._build_resolution_file(dfq, 0, market) assert result is not None # Both entries map to date 2025-01-01 (end_date - 1 day) # The second (0.9) should be kept @@ -304,7 +304,7 @@ def test_resolved_market_truncates_and_appends(self, metaculus_source): resolved=True, resolution_datetime="2025-01-03T00:00:00+00:00", ) - result = metaculus_source._create_resolution_file(dfq, 0, market) + result = metaculus_source._build_resolution_file(dfq, 0, market) assert result is not None # Last row should be the resolution value (yes -> 1) assert float(result.iloc[-1]["value"]) == 1.0 @@ -335,7 +335,7 @@ def test_null_end_time_uses_today(self, metaculus_source, freeze_today): } ) dfq = self._dfq_row() - result = metaculus_source._create_resolution_file(dfq, 0, market) + result = metaculus_source._build_resolution_file(dfq, 0, market) assert result is not None # Should have dates from Dec 31 through Jan 4 (today - 1 day) assert len(result) >= 2 @@ -364,7 +364,7 @@ def test_future_dates_dropped(self, metaculus_source, freeze_today): } ) dfq = self._dfq_row() - result = metaculus_source._create_resolution_file(dfq, 0, market) + result = metaculus_source._build_resolution_file(dfq, 0, market) assert result is not None # No date may exceed yesterday (2025-01-04); the future 2025-01-09 entry is dropped. assert max(result["date"].tolist()) <= "2025-01-04" @@ -389,7 +389,7 @@ def test_future_only_unresolved_market_returns_none(self, metaculus_source, free } ) dfq = self._dfq_row() - result = metaculus_source._create_resolution_file(dfq, 0, market) + result = metaculus_source._build_resolution_file(dfq, 0, market) assert result is None def test_date_assignment_subtracts_day(self, metaculus_source): @@ -412,7 +412,7 @@ def test_date_assignment_subtracts_day(self, metaculus_source): } ) dfq = self._dfq_row() - result = metaculus_source._create_resolution_file(dfq, 0, market) + result = metaculus_source._build_resolution_file(dfq, 0, market) assert result is not None # end_date 2025-01-02 minus 1 day = 2025-01-01 assert "2025-01-01" in result["date"].tolist() @@ -421,7 +421,7 @@ def test_id_is_string(self, metaculus_source): """Output id column contains string values.""" market = make_metaculus_market() dfq = self._dfq_row() - result = metaculus_source._create_resolution_file(dfq, 0, market) + result = metaculus_source._build_resolution_file(dfq, 0, market) assert result is not None assert all(isinstance(v, str) for v in result["id"].tolist()) @@ -429,7 +429,7 @@ def test_output_validated_as_resolution_frame(self, metaculus_source): """Output passes ResolutionFrame.validate().""" market = make_metaculus_market() dfq = self._dfq_row() - result = metaculus_source._create_resolution_file(dfq, 0, market) + result = metaculus_source._build_resolution_file(dfq, 0, market) assert result is not None ResolutionFrame.validate(result) @@ -682,7 +682,7 @@ def test_api_key_required(self): # --------------------------------------------------------------------------- -# update() (mock _get_market and _create_resolution_file) +# update() (mock _get_market and _build_resolution_file) # --------------------------------------------------------------------------- @@ -699,7 +699,7 @@ def _resolution_df(self, question_id="42472", value=0.6): } ) - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_new_question_appended(self, mock_market, mock_res, metaculus_source): """ID in dff not in dfq gets appended with defaults.""" @@ -715,7 +715,7 @@ def test_new_question_appended(self, mock_market, mock_res, metaculus_source): row = result.dfq[result.dfq["id"] == "200"].iloc[0] assert row["freeze_datetime_value_explanation"] == "The community prediction." - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_existing_question_not_duplicated(self, mock_market, mock_res, metaculus_source): """ID already in dfq does not add a new row.""" @@ -728,7 +728,7 @@ def test_existing_question_not_duplicated(self, mock_market, mock_res, metaculus result = metaculus_source.update(dfq, dff) assert len(result.dfq) == 1 - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_question_fields_updated(self, mock_market, mock_res, metaculus_source): """Unresolved question fields are updated from market data.""" @@ -753,7 +753,7 @@ def test_question_fields_updated(self, mock_market, mock_res, metaculus_source): assert row["market_info_resolution_criteria"] == "New criteria" assert "metaculus.com/questions/42472" in row["url"] - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_background_empty_string_kept(self, mock_market, mock_res, metaculus_source): """Empty description string is stored as-is, not converted to 'N/A'.""" @@ -767,7 +767,7 @@ def test_background_empty_string_kept(self, mock_market, mock_res, metaculus_sou result = metaculus_source.update(dfq, dff) assert result.dfq.iloc[0]["background"] == "" - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_background_missing_key_becomes_na(self, mock_market, mock_res, metaculus_source): """Missing description key becomes 'N/A'.""" @@ -782,7 +782,7 @@ def test_background_missing_key_becomes_na(self, mock_market, mock_res, metaculu result = metaculus_source.update(dfq, dff) assert result.dfq.iloc[0]["background"] == "N/A" - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_resolved_market_sets_resolution_datetime( self, mock_market, mock_res, metaculus_source @@ -808,10 +808,10 @@ def test_resolved_market_sets_resolution_datetime( # min(close=March 1, resolve=Feb 15) = Feb 15 assert "2026-02-15" in str(row["market_info_resolution_datetime"]) - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_resolution_file_stored(self, mock_market, mock_res, metaculus_source): - """Resolution file from _create_resolution_file is stored in result.""" + """Resolution file from _build_resolution_file is stored in result.""" mock_market.return_value = make_metaculus_market() res_df = self._resolution_df() mock_res.return_value = res_df @@ -822,7 +822,7 @@ def test_resolution_file_stored(self, mock_market, mock_res, metaculus_source): result = metaculus_source.update(dfq, dff) assert "42472" in result.resolution_files - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_freeze_value_from_last_resolution(self, mock_market, mock_res, metaculus_source): """freeze_datetime_value is the last value in the resolution file.""" @@ -836,10 +836,10 @@ def test_freeze_value_from_last_resolution(self, mock_market, mock_res, metaculu # QuestionFrame coerces freeze_datetime_value to str assert str(result.dfq.iloc[0]["freeze_datetime_value"]) == "0.75" - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_freeze_value_na_when_no_resolution(self, mock_market, mock_res, metaculus_source): - """freeze_datetime_value is 'N/A' when _create_resolution_file returns None.""" + """freeze_datetime_value is 'N/A' when _build_resolution_file returns None.""" mock_market.return_value = make_metaculus_market() mock_res.return_value = None @@ -849,7 +849,7 @@ def test_freeze_value_na_when_no_resolution(self, mock_market, mock_res, metacul result = metaculus_source.update(dfq, dff) assert result.dfq.iloc[0]["freeze_datetime_value"] == "N/A" - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_market_api_failure_propagates(self, mock_market, mock_res, metaculus_source): """A persistent _get_market failure propagates (fail loudly), not silently skipped.""" @@ -862,7 +862,7 @@ def test_market_api_failure_propagates(self, mock_market, mock_res, metaculus_so metaculus_source.update(dfq, dff) mock_res.assert_not_called() - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_resolved_missing_resolution_regenerated(self, mock_market, mock_res, metaculus_source): """Resolved question without existing resolution file triggers regeneration.""" @@ -895,7 +895,7 @@ def test_resolved_missing_resolution_regenerated(self, mock_market, mock_res, me mock_market.assert_called_once_with("42472") assert "42472" in result.resolution_files - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_resolved_with_existing_file_not_regenerated( self, mock_market, mock_res, metaculus_source @@ -917,7 +917,7 @@ def test_resolved_with_existing_file_not_regenerated( mock_market.assert_not_called() mock_res.assert_not_called() - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_cap_new_questions(self, mock_market, mock_res, metaculus_source): """New IDs exceeding _QUESTION_LIMIT - unresolved are capped.""" @@ -942,7 +942,7 @@ def test_api_key_required(self): with pytest.raises(RuntimeError, match="api_key must be set"): src.update(dfq, dff) - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_forecast_horizons_always_na(self, mock_market, mock_res, metaculus_source): """Every updated row has forecast_horizons = 'N/A'.""" @@ -955,7 +955,7 @@ def test_forecast_horizons_always_na(self, mock_market, mock_res, metaculus_sour result = metaculus_source.update(dfq, dff) assert result.dfq.iloc[0]["forecast_horizons"] == "N/A" - @patch.object(MetaculusSource, "_create_resolution_file") + @patch.object(MetaculusSource, "_build_resolution_file") @patch.object(MetaculusSource, "_get_market") def test_valid_question_frame_output(self, mock_market, mock_res, metaculus_source): """result.dfq passes QuestionFrame.validate()."""