Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/385.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`fit()` and `predict()` no longer print a progress spinner to stdout, `init()` no longer announces that it is reusing a cached token, and routine messages such as "Test set already exists, skipping upload." are logged at DEBUG instead of WARNING. `TABPFN_CLIENT_CI_MODE`, which only disabled the spinner, is removed.
10 changes: 4 additions & 6 deletions src/tabpfn_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import re
import struct
import time
import traceback
import warnings
from pydantic import BaseModel, ValidationError
from typing import Any, cast, Mapping, NoReturn
Expand Down Expand Up @@ -361,7 +360,7 @@ def fit(
)

if isinstance(prepare_resp, DuplicateTrainSetErrorResponse):
logger.warning("Train set already exists, skipping upload.")
logger.debug("Train set already exists, skipping upload.")
else:
with ThreadPoolExecutor(max_workers=2) as pool:
futures = [
Expand Down Expand Up @@ -708,7 +707,7 @@ def predict(
raise FittedModelNotFoundError(message)

if isinstance(prepare_resp, DuplicateTestSetErrorResponse):
logger.warning("Test set already exists, skipping upload.")
logger.debug("Test set already exists, skipping upload.")
else:
cls._upload_to_gcs(
"x_test",
Expand Down Expand Up @@ -989,9 +988,8 @@ def try_connection(cls) -> bool:
if response.status_code == 200:
found_valid_connection = True

except httpx.ConnectError as e:
logger.error(f"Failed to connect to the server with error: {e}")
traceback.print_exc()
except httpx.ConnectError:
logger.debug("Failed to connect to the server", exc_info=True)
found_valid_connection = False

return found_valid_connection
Expand Down
22 changes: 10 additions & 12 deletions src/tabpfn_client/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,16 @@ def init(use_server=True):
except ConnectError:
raise CONNECTION_ERROR

if is_valid_token:
PromptAgent.prompt_reusing_existing_token()
elif unverified_token is not None:
# The token is well-formed but the account's email is unverified,
# which no token can work around.
raise RuntimeError(
"Your TabPFN account's email address is not verified.\n"
"Check your inbox for the verification email, or sign in at\n"
f" {ServiceClient.server_config.gui_url}\n"
"to request a new one, then run your script again."
)
else:
if not is_valid_token:
if unverified_token is not None:
# The token is well-formed but the account's email is unverified,
# which no token can work around.
raise RuntimeError(
"Your TabPFN account's email address is not verified.\n"
"Check your inbox for the verification email, or sign in at\n"
f" {ServiceClient.server_config.gui_url}\n"
"to request a new one, then run your script again."
)
if not UserAuthenticationClient.is_accessible_connection():
raise CONNECTION_ERROR
# Never prompt from the default path: this is a library, so a
Expand Down
121 changes: 38 additions & 83 deletions src/tabpfn_client/estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@
from __future__ import annotations

import logging
import sys
import time
import warnings
from uuid import uuid4
from concurrent.futures import ThreadPoolExecutor
from typing import Any, Callable, Literal, cast, overload
from typing import Any, Literal, cast, overload
from typing_extensions import Self
from uuid import UUID

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -45,7 +42,6 @@
)
from tabpfn_client.models import ApiMode, TabPFNConfig, FitModeLiteral
from tabpfn_client.persistence import ModelPersistenceMixin
from tabpfn_client.options import get_opts

try:
from torch import Tensor # type: ignore
Expand Down Expand Up @@ -329,19 +325,16 @@ def fit(

self._last_trace_id = self.client_options.headers["sentry-trace"]

def fit_task() -> UUID:
return InferenceClient.fit(
X_clean,
y,
task_config=task_config,
tabpfn_systems=tabpfn_systems,
thinking_config=thinking_config,
api_mode=self.api_mode,
client_options=self.client_options,
description=description,
)

self.model_id_ = cast(UUID, run_task(fit_task, "Fitting"))
self.model_id_ = InferenceClient.fit(
X_clean,
y,
task_config=task_config,
tabpfn_systems=tabpfn_systems,
thinking_config=thinking_config,
api_mode=self.api_mode,
client_options=self.client_options,
description=description,
)
# NOTE: Previously classes were assigned in-place before a fit succeeded,
# consider this failure mode:
# 1. first fit() -> succeeds, model_id_ and classes_ are assigned
Expand Down Expand Up @@ -414,19 +407,16 @@ def _predict(
):
self.client_options.headers["sentry-trace"] = self._last_trace_id

def predict_task() -> PredictionResult:
return InferenceClient.predict(
X_clean,
fitted_train_set_id=self.model_id_,
task_config=task_config,
client_options=self.client_options,
)

result = run_task(predict_task, "Predicting")
result = InferenceClient.predict(
X_clean,
fitted_train_set_id=self.model_id_,
task_config=task_config,
client_options=self.client_options,
)
# Unpack and store metadata
self._last_meta = result.metadata

return result.y_pred
return cast("np.ndarray", result.y_pred)

def _get_tabpfn_config(self) -> ClassifierTabPFNConfig:
init_params = self.get_params()
Expand Down Expand Up @@ -674,19 +664,16 @@ def fit(

self._last_trace_id = self.client_options.headers["sentry-trace"]

def fit_task() -> UUID:
return InferenceClient.fit(
X_clean,
y,
task_config=task_config,
tabpfn_systems=tabpfn_systems,
thinking_config=thinking_config,
api_mode=self.api_mode,
client_options=self.client_options,
description=description,
)

self.model_id_ = cast(UUID, run_task(fit_task, "Fitting"))
self.model_id_ = InferenceClient.fit(
X_clean,
y,
task_config=task_config,
tabpfn_systems=tabpfn_systems,
thinking_config=thinking_config,
api_mode=self.api_mode,
client_options=self.client_options,
description=description,
)
self._n_train_rows = X.shape[0]
self._fit_count += 1
else:
Expand Down Expand Up @@ -775,17 +762,12 @@ def predict(
self.client_options.headers["sentry-trace"] = self._last_trace_id

def predict_rows(X_rows: Any) -> PredictionResult:
X_clean = _clean_text_features(X_rows)

def predict_task() -> PredictionResult:
return InferenceClient.predict(
X_clean,
fitted_train_set_id=self.model_id_,
task_config=task_config,
client_options=self.client_options,
)

return run_task(predict_task, "Predicting")
return InferenceClient.predict(
_clean_text_features(X_rows),
fitted_train_set_id=self.model_id_,
task_config=task_config,
client_options=self.client_options,
)

if chunked:
rows_per_call = cast(int, rows_per_call)
Expand Down Expand Up @@ -821,9 +803,10 @@ def predict_task() -> PredictionResult:
borders=torch.tensor(full["borders"])
)
except ImportError:
logger.warning(
warnings.warn(
"Optional dependencies 'tabpfn' and 'torch' are required to "
"construct the criterion when output_type='full'. Skipping criterion."
"construct the criterion when output_type='full'. Skipping criterion.",
stacklevel=2,
)
return full

Expand Down Expand Up @@ -1050,34 +1033,6 @@ def _clean_text_features(X):
return df


def run_task(task: Callable, message: str, with_spinner: bool = True) -> Any:
if not with_spinner or get_opts().TABPFN_CLIENT_CI_MODE:
result = task()
else:
start = time.time()
spinner = ["-", "\\", "|", "/"]
i = 0
minutes = 0
seconds = 0
with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(task)
while not future.done():
elapsed = int(time.time() - start)
minutes = elapsed // 60
seconds = elapsed % 60
sys.stdout.write(
f"\r{minutes:02d}:{seconds:02d} {message}... {spinner[i % len(spinner)]}"
)
sys.stdout.flush()
time.sleep(0.2)
i += 1
result = future.result()
# Remove spinner, but keep elapsed time
sys.stdout.write(f"\r{minutes:02d}:{seconds:02d} {message}... Done!\n")
sys.stdout.flush()
return result


def _build_fit_task_config(tabpfn_config: TabPFNConfig) -> FitTaskConfig:
match tabpfn_config:
case ClassifierTabPFNConfig():
Expand Down
1 change: 0 additions & 1 deletion src/tabpfn_client/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ class Options(BaseSettings):
TABPFN_CLIENT_MAX_THREAD_PER_UPLOAD: int = 8
TABPFN_CLIENT_TIMEOUT: float = 900.0
TABPFN_CLIENT_UPLOAD_TIMEOUT: float = 7200.0 # 2 hours
TABPFN_CLIENT_CI_MODE: bool = False
TABPFN_CLIENT_FORCE_REUPLOAD: bool = False
TABPFN_CLIENT_DEDUP_DATASETS: bool = True

Expand Down
4 changes: 0 additions & 4 deletions src/tabpfn_client/prompt_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,10 +354,6 @@ def _verify_user_email(cls, access_token: str | None) -> bool:
" [cyan]Try again, type 'resend' for a new code, or 'quit' to exit.[/cyan]"
)

@classmethod
def prompt_reusing_existing_token(cls):
notify("Found existing access token, reusing it for authentication.")

@classmethod
def prompt_retrieved_greeting_messages(cls, greeting_messages: list[str]):
for message in greeting_messages:
Expand Down
44 changes: 8 additions & 36 deletions src/tabpfn_client/service_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,50 +211,27 @@ class UserDataClient(ServiceClientWrapper, Singleton):

@classmethod
def get_data_summary(cls) -> dict:
try:
summary = ServiceClient.get_data_summary()
except RuntimeError as e:
logging.error(f"Failed to get data summary: {e}")
raise e

return summary
return ServiceClient.get_data_summary()

@classmethod
def download_all_data(cls, save_dir: Path = Path(".")) -> Path:
try:
saved_path = ServiceClient.download_all_data(save_dir)
except RuntimeError as e:
logging.error(f"Failed to download data: {e}")
raise e

saved_path = ServiceClient.download_all_data(save_dir)
if saved_path is None:
raise RuntimeError("Failed to download data.")

logging.info(f"Data saved to {saved_path}")
logger.info(f"Data saved to {saved_path}")
return saved_path

@classmethod
def delete_dataset(cls, dataset_uid: str) -> list[str]:
try:
deleted_datasets = ServiceClient.delete_dataset(dataset_uid)
except RuntimeError as e:
logging.error(f"Failed to delete dataset: {e}")
raise e

logging.info(f"Deleted datasets: {deleted_datasets}")

deleted_datasets = ServiceClient.delete_dataset(dataset_uid)
logger.info(f"Deleted datasets: {deleted_datasets}")
return deleted_datasets

@classmethod
def delete_all_datasets(cls) -> list[str]:
try:
deleted_datasets = ServiceClient.delete_all_datasets()
except RuntimeError as e:
logging.error(f"Failed to delete all datasets: {e}")
raise e

logging.info(f"Deleted datasets: {deleted_datasets}")

deleted_datasets = ServiceClient.delete_all_datasets()
logger.info(f"Deleted datasets: {deleted_datasets}")
return deleted_datasets

@classmethod
Expand All @@ -266,12 +243,7 @@ def delete_user_account(cls):
logger.info("Account deletion cancelled — confirmation phrase not entered.")
return

try:
ServiceClient.delete_user_account()
except RuntimeError as e:
logging.error(f"Failed to delete user account: {e}")
raise e

ServiceClient.delete_user_account()
PromptAgent.prompt_account_deleted()


Expand Down
Loading
Loading