diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b6d34c8..e5a316a8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,16 +26,6 @@ jobs: strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] - pydantic-version: ["v2"] - include: - - python-version: "3.9" - pydantic-version: "v1" - - python-version: "3.10" - pydantic-version: "v1" - - python-version: "3.11" - pydantic-version: "v1" - - python-version: "3.12" - pydantic-version: "v1" fail-fast: false runs-on: ubuntu-latest env: @@ -63,9 +53,6 @@ jobs: limit-access-to-actor: true - name: Install Dependencies run: uv sync --locked --all-extras --dev - - name: Install Pydantic v1 - if: matrix.pydantic-version == 'v1' - run: uv pip install "pydantic<2.0.0" - name: Lint run: uv run --no-sync scripts/lint.sh - run: mkdir coverage @@ -77,7 +64,7 @@ jobs: - name: Store coverage files uses: actions/upload-artifact@v4 with: - name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} + name: coverage-${{ matrix.python-version }} path: coverage include-hidden-files: true diff --git a/pyproject.toml b/pyproject.toml index 066b85e0..252980c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "rignore >= 0.5.1", "httpx >= 0.27.0", "rich-toolkit >= 0.14.5", - "pydantic[email] >= 1.6.1", + "pydantic[email] >= 2.0", "sentry-sdk >= 2.20.0", "fastar >= 0.8.0", ] diff --git a/src/fastapi_cloud_cli/commands/deploy.py b/src/fastapi_cloud_cli/commands/deploy.py index b67e18b6..18146784 100644 --- a/src/fastapi_cloud_cli/commands/deploy.py +++ b/src/fastapi_cloud_cli/commands/deploy.py @@ -13,7 +13,7 @@ import rignore import typer from httpx import Client -from pydantic import BaseModel, EmailStr, ValidationError +from pydantic import BaseModel, EmailStr, TypeAdapter, ValidationError from rich.text import Text from rich_toolkit import RichToolkit from rich_toolkit.menu import Option @@ -23,11 +23,6 @@ from fastapi_cloud_cli.utils.apps import AppConfig, get_app_config, write_app_config from fastapi_cloud_cli.utils.auth import is_logged_in from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors -from fastapi_cloud_cli.utils.pydantic_compat import ( - TypeAdapter, - model_dump, - model_validate, -) logger = logging.getLogger(__name__) @@ -108,7 +103,7 @@ def _get_teams() -> list[Team]: data = response.json()["data"] - return [model_validate(Team, team) for team in data] + return [Team.model_validate(team) for team in data] class AppResponse(BaseModel): @@ -125,7 +120,7 @@ def _create_app(team_id: str, app_name: str) -> AppResponse: response.raise_for_status() - return model_validate(AppResponse, response.json()) + return AppResponse.model_validate(response.json()) class DeploymentStatus(str, Enum): @@ -178,7 +173,7 @@ def _create_deployment(app_id: str) -> CreateDeploymentResponse: response = client.post(f"/apps/{app_id}/deployments/") response.raise_for_status() - return model_validate(CreateDeploymentResponse, response.json()) + return CreateDeploymentResponse.model_validate(response.json()) class RequestUploadResponse(BaseModel): @@ -203,7 +198,7 @@ def _upload_deployment(deployment_id: str, archive_path: Path) -> None: response = fastapi_client.post(f"/deployments/{deployment_id}/upload") response.raise_for_status() - upload_data = model_validate(RequestUploadResponse, response.json()) + upload_data = RequestUploadResponse.model_validate(response.json()) logger.debug("Received upload URL: %s", upload_data.url) logger.debug("Starting file upload to S3") @@ -238,7 +233,7 @@ def _get_app(app_slug: str) -> Optional[AppResponse]: data = response.json() - return model_validate(AppResponse, data) + return AppResponse.model_validate(data) def _get_apps(team_id: str) -> list[AppResponse]: @@ -248,7 +243,7 @@ def _get_apps(team_id: str) -> list[AppResponse]: data = response.json()["data"] - return [model_validate(AppResponse, app) for app in data] + return [AppResponse.model_validate(app) for app in data] WAITING_MESSAGES = [ @@ -434,7 +429,7 @@ def _send_waitlist_form( with toolkit.progress("Sending your request...") as progress: with APIClient() as client: with handle_http_errors(progress): - response = client.post("/users/waiting-list", json=model_dump(result)) + response = client.post("/users/waiting-list", json=result.model_dump()) response.raise_for_status() @@ -459,7 +454,7 @@ def _waitlist_form(toolkit: RichToolkit) -> None: toolkit.print_line() - result = model_validate(SignupToWaitingList, {"email": email}) + result = SignupToWaitingList.model_validate({"email": email}) if toolkit.confirm( "Do you want to get access faster by giving us more information?", @@ -483,8 +478,7 @@ def _waitlist_form(toolkit: RichToolkit) -> None: result = form.run() # type: ignore try: - result = model_validate( - SignupToWaitingList, + result = SignupToWaitingList.model_validate( { "email": email, **result, # type: ignore diff --git a/src/fastapi_cloud_cli/commands/env.py b/src/fastapi_cloud_cli/commands/env.py index 1f81e131..351b96a8 100644 --- a/src/fastapi_cloud_cli/commands/env.py +++ b/src/fastapi_cloud_cli/commands/env.py @@ -10,7 +10,6 @@ from fastapi_cloud_cli.utils.auth import is_logged_in from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors from fastapi_cloud_cli.utils.env import validate_environment_variable_name -from fastapi_cloud_cli.utils.pydantic_compat import model_validate logger = logging.getLogger(__name__) @@ -29,7 +28,7 @@ def _get_environment_variables(app_id: str) -> EnvironmentVariableResponse: response = client.get(f"/apps/{app_id}/environment-variables/") response.raise_for_status() - return model_validate(EnvironmentVariableResponse, response.json()) + return EnvironmentVariableResponse.model_validate(response.json()) def _delete_environment_variable(app_id: str, name: str) -> bool: diff --git a/src/fastapi_cloud_cli/commands/login.py b/src/fastapi_cloud_cli/commands/login.py index 75dfcfc7..dd980d8c 100644 --- a/src/fastapi_cloud_cli/commands/login.py +++ b/src/fastapi_cloud_cli/commands/login.py @@ -16,7 +16,6 @@ write_auth_config, ) from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors -from fastapi_cloud_cli.utils.pydantic_compat import model_validate_json logger = logging.getLogger(__name__) @@ -44,7 +43,7 @@ def _start_device_authorization( response.raise_for_status() - return model_validate_json(AuthorizationData, response.text) + return AuthorizationData.model_validate_json(response.text) def _fetch_access_token(client: httpx.Client, device_code: str, interval: int) -> str: @@ -74,7 +73,7 @@ def _fetch_access_token(client: httpx.Client, device_code: str, interval: int) - time.sleep(interval) - response_data = model_validate_json(TokenResponse, response.text) + response_data = TokenResponse.model_validate_json(response.text) return response_data.access_token diff --git a/src/fastapi_cloud_cli/utils/api.py b/src/fastapi_cloud_cli/utils/api.py index 5e64c60a..1e84207d 100644 --- a/src/fastapi_cloud_cli/utils/api.py +++ b/src/fastapi_cloud_cli/utils/api.py @@ -15,13 +15,12 @@ ) import httpx -from pydantic import BaseModel, Field, ValidationError +from pydantic import BaseModel, Field, TypeAdapter, ValidationError from typing_extensions import ParamSpec from fastapi_cloud_cli import __version__ from fastapi_cloud_cli.config import Settings from fastapi_cloud_cli.utils.auth import get_auth_token -from fastapi_cloud_cli.utils.pydantic_compat import TypeAdapter logger = logging.getLogger(__name__) @@ -49,8 +48,8 @@ class BuildLogLineMessage(BaseModel): BuildLogLine = Union[BuildLogLineMessage, BuildLogLineGeneric] -BuildLogAdapter = TypeAdapter[BuildLogLine]( - Annotated[BuildLogLine, Field(discriminator="type")] # type: ignore +BuildLogAdapter: TypeAdapter[BuildLogLine] = TypeAdapter( + Annotated[BuildLogLine, Field(discriminator="type")] ) diff --git a/src/fastapi_cloud_cli/utils/apps.py b/src/fastapi_cloud_cli/utils/apps.py index 221bb217..5e68c4c4 100644 --- a/src/fastapi_cloud_cli/utils/apps.py +++ b/src/fastapi_cloud_cli/utils/apps.py @@ -4,8 +4,6 @@ from pydantic import BaseModel -from fastapi_cloud_cli.utils.pydantic_compat import model_dump_json, model_validate_json - logger = logging.getLogger("fastapi_cli") @@ -23,7 +21,7 @@ def get_app_config(path_to_deploy: Path) -> Optional[AppConfig]: return None logger.debug("App config loaded successfully") - return model_validate_json(AppConfig, config_path.read_text(encoding="utf-8")) + return AppConfig.model_validate_json(config_path.read_text(encoding="utf-8")) README = """ @@ -52,7 +50,7 @@ def write_app_config(path_to_deploy: Path, app_config: AppConfig) -> None: config_path.parent.mkdir(parents=True, exist_ok=True) config_path.write_text( - model_dump_json(app_config), + app_config.model_dump_json(), encoding="utf-8", ) readme_path.write_text(README, encoding="utf-8") diff --git a/src/fastapi_cloud_cli/utils/auth.py b/src/fastapi_cloud_cli/utils/auth.py index 487531e6..5a72d493 100644 --- a/src/fastapi_cloud_cli/utils/auth.py +++ b/src/fastapi_cloud_cli/utils/auth.py @@ -7,8 +7,6 @@ from pydantic import BaseModel -from fastapi_cloud_cli.utils.pydantic_compat import model_dump_json, model_validate_json - from .config import get_auth_path logger = logging.getLogger("fastapi_cli") @@ -22,7 +20,7 @@ def write_auth_config(auth_data: AuthConfig) -> None: auth_path = get_auth_path() logger.debug("Writing auth config to: %s", auth_path) - auth_path.write_text(model_dump_json(auth_data), encoding="utf-8") + auth_path.write_text(auth_data.model_dump_json(), encoding="utf-8") logger.debug("Auth config written successfully") @@ -46,7 +44,7 @@ def read_auth_config() -> Optional[AuthConfig]: return None logger.debug("Auth config loaded successfully") - return model_validate_json(AuthConfig, auth_path.read_text(encoding="utf-8")) + return AuthConfig.model_validate_json(auth_path.read_text(encoding="utf-8")) def get_auth_token() -> Optional[str]: diff --git a/src/fastapi_cloud_cli/utils/pydantic_compat.py b/src/fastapi_cloud_cli/utils/pydantic_compat.py deleted file mode 100644 index 424fbfec..00000000 --- a/src/fastapi_cloud_cli/utils/pydantic_compat.py +++ /dev/null @@ -1,72 +0,0 @@ -from typing import Any, Generic, TypeVar - -from pydantic import BaseModel -from pydantic.version import VERSION as PYDANTIC_VERSION - -PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) -PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 - - -T = TypeVar("T") -Model = TypeVar("Model", bound=BaseModel) - - -def model_validate(model_class: type[Model], data: dict[Any, Any]) -> Model: - if PYDANTIC_V2: - return model_class.model_validate(data) # type: ignore[no-any-return, unused-ignore, attr-defined] - else: - return model_class.parse_obj(data) # type: ignore[no-any-return, unused-ignore, attr-defined] - - -def model_validate_json(model_class: type[Model], data: str) -> Model: - if PYDANTIC_V2: - return model_class.model_validate_json(data) # type: ignore[no-any-return, unused-ignore, attr-defined] - else: - return model_class.parse_raw(data) # type: ignore[no-any-return, unused-ignore, attr-defined] - - -def model_dump(obj: BaseModel, **kwargs: Any) -> dict[Any, Any]: - if PYDANTIC_V2: - return obj.model_dump(**kwargs) # type: ignore[no-any-return, unused-ignore, attr-defined] - else: - return obj.dict(**kwargs) # type: ignore[no-any-return, unused-ignore, attr-defined] - - -def model_dump_json(obj: BaseModel) -> str: - if PYDANTIC_V2: - return obj.model_dump_json() # type: ignore[no-any-return, unused-ignore, attr-defined] - else: - # Use compact separators to match Pydantic v2's output format - return obj.json(separators=(",", ":")) # type: ignore[no-any-return, unused-ignore, attr-defined] - - -class TypeAdapter(Generic[T]): - def __init__(self, type_: type[T]) -> None: - self.type_ = type_ - - if PYDANTIC_V2: - from pydantic import ( # type: ignore[attr-defined, unused-ignore] - TypeAdapter as PydanticTypeAdapter, - ) - - self._adapter = PydanticTypeAdapter(type_) - else: - self._adapter = None # type: ignore[assignment, unused-ignore] - - def validate_python(self, value: Any) -> T: - """Validate a Python object against the type.""" - if PYDANTIC_V2: - return self._adapter.validate_python(value) # type: ignore[no-any-return, union-attr, unused-ignore] - else: - from pydantic import parse_obj_as - - return parse_obj_as(self.type_, value) # type: ignore[no-any-return, unused-ignore] - - def validate_json(self, value: str) -> T: - """Validate a JSON string against the type.""" - if PYDANTIC_V2: - return self._adapter.validate_json(value) # type: ignore[no-any-return, union-attr, unused-ignore] - else: - from pydantic import parse_raw_as - - return parse_raw_as(self.type_, value) # type: ignore[no-any-return, unused-ignore, operator] diff --git a/uv.lock b/uv.lock index 2b309a43..06236020 100644 --- a/uv.lock +++ b/uv.lock @@ -423,7 +423,7 @@ dev = [ requires-dist = [ { name = "fastar", specifier = ">=0.8.0" }, { name = "httpx", specifier = ">=0.27.0" }, - { name = "pydantic", extras = ["email"], specifier = ">=1.6.1" }, + { name = "pydantic", extras = ["email"], specifier = ">=2.0" }, { name = "rich-toolkit", specifier = ">=0.14.5" }, { name = "rignore", specifier = ">=0.5.1" }, { name = "sentry-sdk", specifier = ">=2.20.0" },