diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index fcee693d0..a84687f1e 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -21,11 +21,12 @@ env: WEAVIATE_127: 1.27.27 WEAVIATE_128: 1.28.16 WEAVIATE_129: 1.29.11 - WEAVIATE_130: 1.30.21 - WEAVIATE_131: 1.31.19 - WEAVIATE_132: 1.32.16 - WEAVIATE_133: 1.33.4 - WEAVIATE_134: 1.34.0 + WEAVIATE_130: 1.30.22 + WEAVIATE_131: 1.31.20 + WEAVIATE_132: 1.32.23 + WEAVIATE_133: 1.33.10 + WEAVIATE_134: 1.34.5 + WEAVIATE_135: 1.35.0 jobs: lint-and-format: @@ -59,7 +60,7 @@ jobs: strategy: fail-fast: false matrix: - version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + version: ["3.10", "3.11", "3.12", "3.13", "3.14"] folder: ["weaviate", "integration", "integration_embedded"] steps: - uses: actions/checkout@v4 @@ -79,7 +80,7 @@ jobs: strategy: fail-fast: false matrix: - version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + version: ["3.10", "3.11", "3.12", "3.13", "3.14"] folder: ["test", "mock_tests"] steps: - uses: actions/checkout@v4 @@ -122,7 +123,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + version: ["3.10", "3.11", "3.12", "3.13", "3.14"] optional_dependencies: [false] steps: - uses: actions/checkout@v4 @@ -153,11 +154,11 @@ jobs: fail-fast: false matrix: versions: [ - { py: "3.9", weaviate: $WEAVIATE_132, grpc: "1.59.0"}, - { py: "3.10", weaviate: $WEAVIATE_132, grpc: "1.66.0"}, - { py: "3.11", weaviate: $WEAVIATE_132, grpc: "1.70.0"}, - { py: "3.12", weaviate: $WEAVIATE_132, grpc: "1.72.1"}, - { py: "3.13", weaviate: $WEAVIATE_132, grpc: "1.74.0"} + { py: "3.10", weaviate: $WEAVIATE_132, grpc: "1.59.0"}, + { py: "3.11", weaviate: $WEAVIATE_132, grpc: "1.66.0"}, + { py: "3.12", weaviate: $WEAVIATE_132, grpc: "1.70.0"}, + { py: "3.13", weaviate: $WEAVIATE_132, grpc: "1.72.1"}, + { py: "3.14", weaviate: $WEAVIATE_132, grpc: "1.76.0"} ] optional_dependencies: [false] steps: @@ -208,11 +209,11 @@ jobs: fail-fast: false matrix: versions: [ - { py: "3.9", weaviate: $WEAVIATE_132}, { py: "3.10", weaviate: $WEAVIATE_132}, { py: "3.11", weaviate: $WEAVIATE_132}, { py: "3.12", weaviate: $WEAVIATE_132}, - { py: "3.13", weaviate: $WEAVIATE_132} + { py: "3.13", weaviate: $WEAVIATE_132}, + { py: "3.14", weaviate: $WEAVIATE_132} ] optional_dependencies: [false] steps: @@ -305,6 +306,7 @@ jobs: $WEAVIATE_132, $WEAVIATE_133, $WEAVIATE_134 + $WEAVIATE_135 ] steps: - name: Checkout diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4b897b920..fb73a7012 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.7 + rev: v0.14.7 hooks: # Run the linter. - id: ruff @@ -19,24 +19,24 @@ repos: args: [ weaviate, integration, test, mock_tests, journey_tests ] - repo: https://github.com/PyCQA/flake8 - rev: 7.1.0 + rev: 7.3.0 hooks: - id: flake8 name: linting additional_dependencies: [ - 'flake8-bugbear==22.10.27', - 'flake8-comprehensions==3.10.1', - 'flake8-builtins==2.0.1' + 'flake8-bugbear==24.12.12', + 'flake8-comprehensions==3.17.0', + 'flake8-builtins==3.0.0' ] - id: flake8 name: docstrings additional_dependencies: [ 'flake8-docstrings==1.7.0', - 'pydoclint==0.6.5', + 'pydoclint==0.7.3', ] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: - id: no-commit-to-branch - id: trailing-whitespace diff --git a/integration/conftest.py b/integration/conftest.py index bad9cef32..256517ea6 100644 --- a/integration/conftest.py +++ b/integration/conftest.py @@ -1,4 +1,5 @@ import os +import time from typing import ( Any, AsyncGenerator, @@ -11,16 +12,16 @@ Type, Union, ) +from typing import Callable, TypeVar import pytest import pytest_asyncio from _pytest.fixtures import SubRequest -import time -from typing import Callable, TypeVar import weaviate from weaviate.collections import Collection, CollectionAsync from weaviate.collections.classes.config import ( + _ObjectTTLConfigCreate, Configure, DataType, Property, @@ -37,7 +38,6 @@ from weaviate.collections.classes.config_named_vectors import _NamedVectorConfigCreate from weaviate.collections.classes.types import Properties from weaviate.config import AdditionalConfig - from weaviate.exceptions import UnexpectedStatusCodeError @@ -66,6 +66,7 @@ def __call__( vector_config: Optional[ Optional[Union[_VectorConfigCreate, List[_VectorConfigCreate]]] ] = None, + object_ttl: Optional[_ObjectTTLConfigCreate] = None, ) -> Collection[Any, Any]: """Typing for fixture.""" ... @@ -140,6 +141,7 @@ def _factory( vector_config: Optional[ Optional[Union[_VectorConfigCreate, List[_VectorConfigCreate]]] ] = None, + object_ttl: Optional[_ObjectTTLConfigCreate] = None, ) -> Collection[Any, Any]: try: nonlocal client_fixture, name_fixtures, call_counter # noqa: F824 @@ -172,6 +174,7 @@ def _factory( vector_index_config=vector_index_config, reranker_config=reranker_config, vector_config=vector_config, + object_ttl_config=object_ttl, ) return collection except Exception as e: diff --git a/integration/test_collection_config.py b/integration/test_collection_config.py index 8b0c8466f..1ed1df103 100644 --- a/integration/test_collection_config.py +++ b/integration/test_collection_config.py @@ -1,3 +1,4 @@ +import datetime from typing import Generator, List, Optional, Union import pytest as pytest @@ -1833,3 +1834,119 @@ def test_uncompressed_quantitizer(collection_factory: CollectionFactory) -> None assert config.vector_index_config is not None assert isinstance(config.vector_index_config, _VectorIndexConfigHNSW) assert config.vector_index_config.quantizer is None + + +def test_object_ttl_creation(collection_factory: CollectionFactory) -> None: + dummy = collection_factory("dummy") + if dummy._connection._weaviate_version.is_lower_than(1, 35, 0): + pytest.skip("object ttl is not supported in Weaviate versions lower than 1.35.0") + + collection = collection_factory( + object_ttl=Configure.ObjectTTL.delete_by_creation_time( + time_to_live=datetime.timedelta(days=30), + filter_expired_objects=True, + ), + inverted_index_config=Configure.inverted_index(index_timestamps=True), + ) + + config = collection.config.get() + assert config.object_ttl_config is not None + assert config.object_ttl_config.delete_on == "creationTime" + assert config.object_ttl_config.time_to_live == datetime.timedelta(days=30) + + +def test_object_ttl_update_time(collection_factory: CollectionFactory) -> None: + dummy = collection_factory("dummy") + if dummy._connection._weaviate_version.is_lower_than(1, 35, 0): + pytest.skip("object ttl is not supported in Weaviate versions lower than 1.35.0") + + collection = collection_factory( + object_ttl=Configure.ObjectTTL.delete_by_update_time( + time_to_live=datetime.timedelta(days=30), + filter_expired_objects=True, + ), + inverted_index_config=Configure.inverted_index(index_timestamps=True), + ) + + config = collection.config.get() + assert config.object_ttl_config is not None + assert config.object_ttl_config.delete_on == "updateTime" + assert config.object_ttl_config.filter_expired_objects + assert config.object_ttl_config.time_to_live == datetime.timedelta(days=30) + + +def test_object_ttl_custom(collection_factory: CollectionFactory) -> None: + dummy = collection_factory("dummy") + if dummy._connection._weaviate_version.is_lower_than(1, 35, 0): + pytest.skip("object ttl is not supported in Weaviate versions lower than 1.35.0") + + collection = collection_factory( + properties=[wvc.config.Property(name="customDate", data_type=DataType.DATE)], + object_ttl=Configure.ObjectTTL.delete_by_date_property( + property_name="customDate", filter_expired_objects=False, ttl_offset=-1 + ), + inverted_index_config=Configure.inverted_index(index_timestamps=True), + ) + + config = collection.config.get() + assert config.object_ttl_config is not None + assert config.object_ttl_config.delete_on == "customDate" + assert config.object_ttl_config.time_to_live == datetime.timedelta(seconds=-1) + assert not config.object_ttl_config.filter_expired_objects + + +def test_object_ttl_update(collection_factory: CollectionFactory) -> None: + dummy = collection_factory("dummy") + if dummy._connection._weaviate_version.is_lower_than(1, 35, 0): + pytest.skip("object ttl is not supported in Weaviate versions lower than 1.35.0") + + collection = collection_factory( + properties=[ + wvc.config.Property(name="customDate", data_type=DataType.DATE), + wvc.config.Property(name="customDate2", data_type=DataType.DATE), + ], + inverted_index_config=Configure.inverted_index(index_timestamps=True), + ) + + conf = collection.config.get() + assert conf.object_ttl_config is None + + collection.config.update( + object_ttl_config=Reconfigure.ObjectTTL.delete_by_date_property( + property_name="customDate", filter_expired_objects=True, ttl_offset=3600 + ), + ) + + conf = collection.config.get() + assert conf.object_ttl_config is not None + assert conf.object_ttl_config.delete_on == "customDate" + assert conf.object_ttl_config.time_to_live == datetime.timedelta(seconds=3600) + assert conf.object_ttl_config.filter_expired_objects + + collection.config.update( + object_ttl_config=Reconfigure.ObjectTTL.delete_by_update_time(filter_expired_objects=False), + ) + + conf = collection.config.get() + assert conf.object_ttl_config is not None + assert conf.object_ttl_config.delete_on == "updateTime" + assert conf.object_ttl_config.time_to_live == datetime.timedelta(seconds=3600) + assert not conf.object_ttl_config.filter_expired_objects + + collection.config.update( + object_ttl_config=Reconfigure.ObjectTTL.delete_by_creation_time( + time_to_live=datetime.timedelta(seconds=600), + ), + ) + + conf = collection.config.get() + assert conf.object_ttl_config is not None + assert conf.object_ttl_config.delete_on == "creationTime" + assert conf.object_ttl_config.time_to_live == datetime.timedelta(seconds=600) + assert not conf.object_ttl_config.filter_expired_objects + + collection.config.update( + object_ttl_config=Reconfigure.ObjectTTL.disable(), + ) + conf = collection.config.get() + assert conf.object_ttl_config is None diff --git a/mock_tests/test_collection.py b/mock_tests/test_collection.py index 38cd0b088..17b9dacc2 100644 --- a/mock_tests/test_collection.py +++ b/mock_tests/test_collection.py @@ -106,6 +106,7 @@ def test_missing_multi_tenancy_config( reranker_config=None, vectorizer_config=None, vector_config=None, + object_ttl_config=None, inverted_index_config=InvertedIndexConfig( bm25=BM25Config(b=0, k1=0), cleanup_interval_seconds=0, diff --git a/requirements-devel.txt b/requirements-devel.txt index 9c9771707..c4d6a7192 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,10 +1,10 @@ httpx==0.26.0 validators==0.34.0 authlib==1.6.5 -grpcio==1.66.2 -grpcio-tools==1.66.2 -grpcio-health-checking==1.66.2 -pydantic==2.8.0 +grpcio==1.75.1 +grpcio-tools==1.75.1 +grpcio-health-checking==1.75.1 +pydantic==2.12.0 deprecation==2.1.0 build diff --git a/setup.cfg b/setup.cfg index 882700b4f..6493bef49 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,12 +36,12 @@ include_package_data = True install_requires = httpx>=0.26.0,<0.29.0 validators>=0.34.0,<1.0.0 - authlib>=1.2.1,<2.0.0 - pydantic>=2.8.0,<3.0.0 + authlib>=1.6.5,<2.0.0 + pydantic>=2.12.0,<3.0.0 grpcio>=1.59.5,<1.80.0 protobuf>=4.21.6,<7.0.0 deprecation>=2.1.0,<3.0.0 -python_requires = >=3.9 +python_requires = >=3.10 [options.extras_require] agents = diff --git a/weaviate/collections/classes/config.py b/weaviate/collections/classes/config.py index ae6db911a..df79252b6 100644 --- a/weaviate/collections/classes/config.py +++ b/weaviate/collections/classes/config.py @@ -1,3 +1,4 @@ +import datetime from dataclasses import dataclass from typing import ( Any, @@ -14,7 +15,7 @@ ) from deprecation import deprecated as docstring_deprecated -from pydantic import AnyHttpUrl, AnyUrl, Field, ValidationInfo, field_validator +from pydantic import AnyHttpUrl, Field, TypeAdapter, ValidationInfo, field_validator from typing_extensions import TypeAlias from typing_extensions import deprecated as typing_deprecated @@ -31,6 +32,12 @@ _NamedVectors, _NamedVectorsUpdate, ) +from weaviate.collections.classes.config_object_ttl import ( + _ObjectTTL, + _ObjectTTLConfigCreate, + _ObjectTTLConfigUpdate, + _ObjectTTLUpdate, +) from weaviate.collections.classes.config_vector_index import ( PQEncoderDistribution, PQEncoderType, @@ -1278,7 +1285,10 @@ def cohere( base_url: The base URL to send the reranker requests to. Defaults to `None`, which uses the server-defined default. """ return _RerankerCohereConfig( - model=model, baseURL=AnyUrl(base_url) if base_url is not None else None + model=model, + baseURL=TypeAdapter(AnyHttpUrl).validate_python(base_url) + if base_url is not None + else None, ) @staticmethod @@ -1344,70 +1354,15 @@ def contextualai( return _RerankerContextualAIConfig(model=model, instruction=instruction, topN=top_n) -class _CollectionConfigCreateBase(_ConfigCreateModel): - description: Optional[str] = Field(default=None) - invertedIndexConfig: Optional[_InvertedIndexConfigCreate] = Field( - default=None, alias="inverted_index_config" - ) - multiTenancyConfig: Optional[_MultiTenancyConfigCreate] = Field( - default=None, alias="multi_tenancy_config" - ) - replicationConfig: Optional[_ReplicationConfigCreate] = Field( - default=None, alias="replication_config" - ) - shardingConfig: Optional[_ShardingConfigCreate] = Field(default=None, alias="sharding_config") - vectorIndexConfig: Optional[_VectorIndexConfigCreate] = Field( - default=None, alias="vector_index_config" - ) - moduleConfig: _VectorizerConfigCreate = Field( - default=_Vectorizer.none(), alias="vectorizer_config" - ) - generativeSearch: Optional[_GenerativeProvider] = Field(default=None, alias="generative_config") - rerankerConfig: Optional[_RerankerProvider] = Field(default=None, alias="reranker_config") - - def _to_dict(self) -> Dict[str, Any]: - ret_dict: Dict[str, Any] = {} - - for cls_field in type(self).model_fields: - val = getattr(self, cls_field) - if cls_field in ["name", "model", "properties", "references"] or val is None: - continue - elif isinstance(val, (bool, float, str, int)): - ret_dict[cls_field] = str(val) - elif isinstance(val, _GenerativeProvider): - self.__add_to_module_config(ret_dict, val.generative.value, val._to_dict()) - elif isinstance(val, _RerankerProvider): - self.__add_to_module_config(ret_dict, val.reranker.value, val._to_dict()) - elif isinstance(val, _VectorizerConfigCreate): - ret_dict["vectorizer"] = val.vectorizer.value - if val.vectorizer != Vectorizers.NONE: - self.__add_to_module_config(ret_dict, val.vectorizer.value, val._to_dict()) - elif isinstance(val, _VectorIndexConfigCreate): - ret_dict["vectorIndexType"] = val.vector_index_type() - ret_dict[cls_field] = val._to_dict() - else: - assert isinstance(val, _ConfigCreateModel) - ret_dict[cls_field] = val._to_dict() - if self.vectorIndexConfig is None: - ret_dict["vectorIndexType"] = VectorIndexType.HNSW.value - return ret_dict - - @staticmethod - def __add_to_module_config( - return_dict: Dict[str, Any], addition_key: str, addition_val: Dict[str, Any] - ) -> None: - if "moduleConfig" not in return_dict: - return_dict["moduleConfig"] = {addition_key: addition_val} - else: - return_dict["moduleConfig"][addition_key] = addition_val - - class _CollectionConfigUpdate(_ConfigUpdateModel): description: Optional[str] = Field(default=None) property_descriptions: Optional[Dict[str, str]] = Field(default=None) invertedIndexConfig: Optional[_InvertedIndexConfigUpdate] = Field( default=None, alias="inverted_index_config" ) + objectTTLConfig: Optional[_ObjectTTLConfigUpdate] = Field( + default=None, alias="object_ttl_config" + ) replicationConfig: Optional[_ReplicationConfigUpdate] = Field( default=None, alias="replication_config" ) @@ -1515,6 +1470,10 @@ def merge_with_existing(self, schema: Dict[str, Any]) -> Dict[str, Any]: schema["multiTenancyConfig"] = self.multiTenancyConfig.merge_with_existing( schema["multiTenancyConfig"] ) + if self.objectTTLConfig is not None: + schema["objectTTLConfig"] = self.objectTTLConfig.merge_with_existing( + schema.get("objectTTLConfig", {}) + ) if self.vectorIndexConfig is not None: self.__check_quantizers(self.vectorIndexConfig.quantizer, schema["vectorIndexConfig"]) schema["vectorIndexConfig"] = self.vectorIndexConfig.merge_with_existing( @@ -1972,6 +1931,17 @@ def to_dict(self) -> Dict: NamedVectorConfig = _NamedVectorConfig +@dataclass +class _ObjectTTLConfig(_ConfigBase): + enabled: bool + time_to_live: Optional[datetime.timedelta] + filter_expired_objects: bool + delete_on: Union[str, Literal["updateTime"], Literal["creationTime"]] + + +ObjectTTLConfig = _ObjectTTLConfig + + @dataclass class _CollectionConfig(_ConfigBase): name: str @@ -1979,6 +1949,7 @@ class _CollectionConfig(_ConfigBase): generative_config: Optional[GenerativeConfig] inverted_index_config: InvertedIndexConfig multi_tenancy_config: MultiTenancyConfig + object_ttl_config: Optional[ObjectTTLConfig] properties: List[PropertyConfig] references: List[ReferencePropertyConfig] replication_config: ReplicationConfig @@ -2049,6 +2020,7 @@ class _CollectionConfigSimple(_ConfigBase): vectorizer_config: Optional[VectorizerConfig] vectorizer: Optional[Union[Vectorizers, str]] vector_config: Optional[Dict[str, _NamedVectorConfig]] + object_ttl_config: Optional[ObjectTTLConfig] CollectionConfigSimple = _CollectionConfigSimple @@ -2200,6 +2172,9 @@ class _CollectionConfigCreate(_ConfigCreateModel): multiTenancyConfig: Optional[_MultiTenancyConfigCreate] = Field( default=None, alias="multi_tenancy_config" ) + objectTtlConfig: Optional[_ObjectTTLConfigCreate] = Field( + default=None, alias="object_ttl_config" + ) replicationConfig: Optional[_ReplicationConfigCreate] = Field( default=None, alias="replication_config" ) @@ -2225,9 +2200,17 @@ def model_post_init(self, __context: Any) -> None: @classmethod def validate_vector_names( cls, - v: Union[_VectorizerConfigCreate, _NamedVectorConfigCreate, List[_NamedVectorConfigCreate]], + v: Union[ + _VectorizerConfigCreate, + _NamedVectorConfigCreate, + List[_NamedVectorConfigCreate], + ], info: ValidationInfo, - ) -> Union[_VectorizerConfigCreate, _NamedVectorConfigCreate, List[_NamedVectorConfigCreate]]: + ) -> Union[ + _VectorizerConfigCreate, + _NamedVectorConfigCreate, + List[_NamedVectorConfigCreate], + ]: if isinstance(v, list): names = [vc.name for vc in v] if len(names) != len(set(names)): @@ -2248,7 +2231,8 @@ def inject_vector_config_none( and info.data["vectorIndexConfig"] is None ): return _VectorConfigCreate( - name="default", vectorizer=_VectorizerConfigCreate(vectorizer=Vectorizers.NONE) + name="default", + vectorizer=_VectorizerConfigCreate(vectorizer=Vectorizers.NONE), ) return v @@ -2363,6 +2347,7 @@ class Configure: NamedVectors = _NamedVectors Vectors = _Vectors MultiVectors = _MultiVectors + ObjectTTL = _ObjectTTL @staticmethod def inverted_index( @@ -2640,6 +2625,7 @@ class Reconfigure: VectorIndex = _VectorIndexUpdate Generative = _Generative # config is the same for create and update Reranker = _Reranker # config is the same for create and update + ObjectTTL = _ObjectTTLUpdate @staticmethod def inverted_index( diff --git a/weaviate/collections/classes/config_methods.py b/weaviate/collections/classes/config_methods.py index 6b815ba24..8d4f0c4ae 100644 --- a/weaviate/collections/classes/config_methods.py +++ b/weaviate/collections/classes/config_methods.py @@ -1,3 +1,4 @@ +import datetime from typing import Any, Dict, List, Optional, Union, cast from weaviate.collections.classes.config import ( @@ -25,6 +26,7 @@ _NamedVectorConfig, _NamedVectorizerConfig, _NestedProperty, + _ObjectTTLConfig, _PQConfig, _PQEncoderConfig, _Property, @@ -293,6 +295,7 @@ def _collection_config_simple_from_json(schema: Dict[str, Any]) -> _CollectionCo name=schema["class"], description=schema.get("description"), generative_config=__get_generative_config(schema), + object_ttl_config=_get_object_ttl_config(schema), properties=( _properties_from_config(schema) if schema.get("properties") is not None else [] ), @@ -340,6 +343,7 @@ def _collection_config_from_json(schema: Dict[str, Any]) -> _CollectionConfig: "autoTenantActivation", False ), ), + object_ttl_config=_get_object_ttl_config(schema), properties=( _properties_from_config(schema) if schema.get("properties") is not None else [] ), @@ -378,6 +382,27 @@ def _collection_config_from_json(schema: Dict[str, Any]) -> _CollectionConfig: ) +def _get_object_ttl_config(schema: Dict[str, Any]) -> Optional[_ObjectTTLConfig]: + if "objectTtlConfig" in schema and schema["objectTtlConfig"].get("enabled", False): + time_to_live = schema["objectTtlConfig"].get("defaultTtl") + if time_to_live is not None and isinstance(time_to_live, int): + time_to_live = datetime.timedelta(seconds=time_to_live) + delete_on = schema["objectTtlConfig"]["deleteOn"] + if delete_on == "_lastUpdateTimeUnix": + delete_on = "updateTime" + elif delete_on == "_creationTimeUnix": + delete_on = "creationTime" + + return _ObjectTTLConfig( + enabled=True, + delete_on=delete_on, + filter_expired_objects=schema["objectTtlConfig"]["filterExpiredObjects"], + time_to_live=time_to_live, + ) + else: + return None + + def _collection_configs_from_json(schema: Dict[str, Any]) -> Dict[str, _CollectionConfig]: configs = { schema["class"]: _collection_config_from_json(schema) for schema in schema["classes"] diff --git a/weaviate/collections/classes/config_object_ttl.py b/weaviate/collections/classes/config_object_ttl.py new file mode 100644 index 000000000..ec15a55b8 --- /dev/null +++ b/weaviate/collections/classes/config_object_ttl.py @@ -0,0 +1,158 @@ +import datetime +from typing import Optional + +from weaviate.collections.classes.config_base import _ConfigCreateModel, _ConfigUpdateModel + + +class _ObjectTTLConfigCreate(_ConfigCreateModel): + enabled: bool = True + filterExpiredObjects: Optional[bool] + deleteOn: Optional[str] + defaultTtl: Optional[int] + + +class _ObjectTTLConfigUpdate(_ConfigUpdateModel): + enabled: bool + filterExpiredObjects: Optional[bool] = None + deleteOn: Optional[str] = None + defaultTtl: Optional[int] = None + + +class _ObjectTTL: + """Configuration class for Weaviate's object time-to-live (TTL) feature.""" + + @staticmethod + def delete_by_update_time( + time_to_live: int | datetime.timedelta, + filter_expired_objects: Optional[bool] = None, + ) -> _ObjectTTLConfigCreate: + """Create an `ObjectTimeToLiveConfig` object to be used when defining the object time-to-live configuration of Weaviate. + + Args: + time_to_live: The time-to-live for objects in relation to their last update time (seconds). Must be positive. + filter_expired_objects: If enabled, exclude expired but not deleted objects from search results. + """ + if isinstance(time_to_live, datetime.timedelta): + time_to_live = int(time_to_live.total_seconds()) + return _ObjectTTLConfigCreate( + deleteOn="_lastUpdateTimeUnix", + filterExpiredObjects=filter_expired_objects, + defaultTtl=time_to_live, + ) + + @staticmethod + def delete_by_creation_time( + time_to_live: int | datetime.timedelta, + filter_expired_objects: Optional[bool] = None, + ) -> _ObjectTTLConfigCreate: + """Create an `ObjectTimeToLiveConfig` object to be used when defining the object time-to-live configuration of Weaviate. + + Args: + time_to_live: The time-to-live for objects in relation to their creation time (seconds). Must be positive. + filter_expired_objects: If enabled, exclude expired but not deleted objects from search results. + """ + if isinstance(time_to_live, datetime.timedelta): + time_to_live = int(time_to_live.total_seconds()) + return _ObjectTTLConfigCreate( + deleteOn="_creationTimeUnix", + filterExpiredObjects=filter_expired_objects, + defaultTtl=time_to_live, + ) + + @staticmethod + def delete_by_date_property( + property_name: str, + ttl_offset: Optional[int | datetime.timedelta] = None, + filter_expired_objects: Optional[bool] = None, + ) -> _ObjectTTLConfigCreate: + """Create an Object ttl config for a custom date property. + + Args: + property_name: The name of the date property to use for object expiration. + ttl_offset: The time-to-live for objects relative to the date (seconds if integer). Can be negative for indicating that objects should expire before the date property value. + filter_expired_objects: If enabled, exclude expired but not deleted objects from search results. + """ + if isinstance(ttl_offset, datetime.timedelta): + ttl_offset = int(ttl_offset.total_seconds()) + if ttl_offset is None: + ttl_offset = 0 + return _ObjectTTLConfigCreate( + deleteOn=property_name, + filterExpiredObjects=filter_expired_objects, + defaultTtl=ttl_offset, + ) + + +class _ObjectTTLUpdate: + """Configuration class for Weaviate's object time-to-live (TTL) feature.""" + + @staticmethod + def disable() -> _ObjectTTLConfigUpdate: + """Create an `ObjectTimeToLiveConfig` object to disable the object time-to-live configuration of Weaviate.""" + return _ObjectTTLConfigUpdate( + enabled=False, + ) + + @staticmethod + def delete_by_update_time( + time_to_live: Optional[int | datetime.timedelta] = None, + filter_expired_objects: Optional[bool] = None, + ) -> _ObjectTTLConfigUpdate: + """Create an `ObjectTimeToLiveConfig` object to be used when defining the object time-to-live configuration of Weaviate. + + Args: + time_to_live: The time-to-live for objects in relation to their last update time (seconds). Must be positive. + filter_expired_objects: If enabled, exclude expired but not deleted objects from search results. + """ + if isinstance(time_to_live, datetime.timedelta): + time_to_live = int(time_to_live.total_seconds()) + return _ObjectTTLConfigUpdate( + enabled=True, + deleteOn="_lastUpdateTimeUnix", + filterExpiredObjects=filter_expired_objects, + defaultTtl=time_to_live, + ) + + @staticmethod + def delete_by_creation_time( + time_to_live: Optional[int | datetime.timedelta] = None, + filter_expired_objects: Optional[bool] = None, + ) -> _ObjectTTLConfigUpdate: + """Create an `ObjectTimeToLiveConfig` object to be used when defining the object time-to-live configuration of Weaviate. + + Args: + time_to_live: The time-to-live for objects in relation to their creation time (seconds). Must be positive. + filter_expired_objects: If enabled, exclude expired but not deleted objects from search results. + """ + if isinstance(time_to_live, datetime.timedelta): + time_to_live = int(time_to_live.total_seconds()) + return _ObjectTTLConfigUpdate( + enabled=True, + deleteOn="_creationTimeUnix", + filterExpiredObjects=filter_expired_objects, + defaultTtl=time_to_live, + ) + + @staticmethod + def delete_by_date_property( + property_name: Optional[str] = None, + ttl_offset: Optional[int | datetime.timedelta] = None, + filter_expired_objects: Optional[bool] = None, + ) -> _ObjectTTLConfigUpdate: + """Create an Object ttl config for a custom date property. + + Args: + property_name: The name of the date property to use for object expiration. + ttl_offset: The time-to-live for objects relative to the date (seconds if integer). Can be negative for indicating that objects should expire before the date property value. + filter_expired_objects: If enabled, exclude expired but not deleted objects from search results. + """ + if isinstance(ttl_offset, datetime.timedelta): + ttl_offset = int(ttl_offset.total_seconds()) + if ttl_offset is None: + ttl_offset = 0 + return _ObjectTTLConfigUpdate( + enabled=True, + deleteOn=property_name, + filterExpiredObjects=filter_expired_objects, + defaultTtl=ttl_offset, + ) diff --git a/weaviate/collections/classes/generative.py b/weaviate/collections/classes/generative.py index 77a389794..ad6b55079 100644 --- a/weaviate/collections/classes/generative.py +++ b/weaviate/collections/classes/generative.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import List, Optional, Union -from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field +from pydantic import AnyHttpUrl, BaseModel, Field, TypeAdapter from typing_extensions import deprecated as typing_deprecated from weaviate.collections.classes.config import ( @@ -509,7 +509,9 @@ def anthropic( top_p: The top P to use. Defaults to `None`, which uses the server-defined default """ return _GenerativeAnthropic( - base_url=AnyUrl(base_url) if base_url is not None else None, + base_url=TypeAdapter(AnyHttpUrl).validate_python(base_url) + if base_url is not None + else None, model=model, max_tokens=max_tokens, stop_sequences=stop_sequences, @@ -533,7 +535,9 @@ def anyscale( temperature: The temperature to use. Defaults to `None`, which uses the server-defined default """ return _GenerativeAnyscale( - base_url=AnyUrl(base_url) if base_url is not None else None, + base_url=TypeAdapter(AnyHttpUrl).validate_python(base_url) + if base_url is not None + else None, model=model, temperature=temperature, ) @@ -573,7 +577,9 @@ def aws( max_tokens=max_tokens, region=region, service=service, - endpoint=AnyUrl(endpoint) if endpoint is not None else None, + endpoint=TypeAdapter(AnyHttpUrl).validate_python(endpoint) + if endpoint is not None + else None, target_model=target_model, target_variant=target_variant, temperature=temperature, @@ -614,7 +620,9 @@ def aws_bedrock( max_tokens=max_tokens, region=region, service="bedrock", - endpoint=AnyUrl(endpoint) if endpoint is not None else None, + endpoint=TypeAdapter(AnyHttpUrl).validate_python(endpoint) + if endpoint is not None + else None, target_model=None, target_variant=None, temperature=temperature, @@ -657,7 +665,9 @@ def aws_sagemaker( max_tokens=max_tokens, region=region, service="sagemaker", - endpoint=AnyUrl(endpoint) if endpoint is not None else None, + endpoint=TypeAdapter(AnyHttpUrl).validate_python(endpoint) + if endpoint is not None + else None, target_model=target_model, target_variant=target_variant, temperature=temperature, @@ -694,7 +704,9 @@ def cohere( temperature: The temperature to use. Defaults to `None`, which uses the server-defined default """ return _GenerativeCohere( - base_url=AnyUrl(base_url) if base_url is not None else None, + base_url=TypeAdapter(AnyHttpUrl).validate_python(base_url) + if base_url is not None + else None, k=k, max_tokens=max_tokens, model=model, @@ -767,7 +779,7 @@ def databricks( top_p: The top P value to use. Defaults to `None`, which uses the server-defined default """ return _GenerativeDatabricks( - endpoint=AnyUrl(endpoint), + endpoint=TypeAdapter(AnyHttpUrl).validate_python(endpoint), frequency_penalty=frequency_penalty, log_probs=log_probs, max_tokens=max_tokens, @@ -806,7 +818,9 @@ def friendliai( top_p: The top P value to use. Defaults to `None`, which uses the server-defined default """ return _GenerativeFriendliai( - base_url=AnyUrl(base_url) if base_url is not None else None, + base_url=TypeAdapter(AnyHttpUrl).validate_python(base_url) + if base_url is not None + else None, max_tokens=max_tokens, model=model, n=n, @@ -853,7 +867,9 @@ def google( top_p: The top P to use. Defaults to `None`, which uses the server-defined default """ return _GenerativeGoogle( - api_endpoint=AnyUrl(api_endpoint) if api_endpoint is not None else None, + api_endpoint=TypeAdapter(AnyHttpUrl).validate_python(api_endpoint) + if api_endpoint is not None + else None, endpoint_id=endpoint_id, frequency_penalty=frequency_penalty, max_tokens=max_tokens, @@ -903,7 +919,9 @@ def google_vertex( top_p: The top P to use. Defaults to `None`, which uses the server-defined default """ return _GenerativeGoogle( - api_endpoint=AnyUrl(api_endpoint) if api_endpoint is not None else None, + api_endpoint=TypeAdapter(AnyHttpUrl).validate_python(api_endpoint) + if api_endpoint is not None + else None, endpoint_id=endpoint_id, frequency_penalty=frequency_penalty, max_tokens=max_tokens, @@ -945,7 +963,9 @@ def google_gemini( top_p: The top P to use. Defaults to `None`, which uses the server-defined default """ return _GenerativeGoogle( - api_endpoint=AnyUrl("generativelanguage.googleapis.com"), + api_endpoint=TypeAdapter(AnyHttpUrl).validate_python( + "https://generativelanguage.googleapis.com" + ), endpoint_id=None, frequency_penalty=frequency_penalty, max_tokens=max_tokens, @@ -978,7 +998,9 @@ def mistral( top_p: The top P value to use. Defaults to `None`, which uses the server-defined default """ return _GenerativeMistral( - base_url=AnyUrl(base_url) if base_url is not None else None, + base_url=TypeAdapter(AnyHttpUrl).validate_python(base_url) + if base_url is not None + else None, max_tokens=max_tokens, model=model, temperature=temperature, @@ -1004,7 +1026,9 @@ def nvidia( top_p: The top P value to use. Defaults to `None`, which uses the server-defined default """ return _GenerativeNvidia( - base_url=AnyUrl(base_url) if base_url is not None else None, + base_url=TypeAdapter(AnyHttpUrl).validate_python(base_url) + if base_url is not None + else None, max_tokens=max_tokens, model=model, temperature=temperature, @@ -1031,7 +1055,9 @@ def ollama( The number of images passed to the prompt will match the value of `limit` in the search query. """ return _GenerativeOllama( - api_endpoint=AnyUrl(api_endpoint) if api_endpoint is not None else None, + api_endpoint=TypeAdapter(AnyHttpUrl).validate_python(api_endpoint) + if api_endpoint is not None + else None, model=model, temperature=temperature, ) @@ -1075,7 +1101,9 @@ def openai( """ return _GenerativeOpenAI( api_version=api_version, - base_url=AnyUrl(base_url) if base_url is not None else None, + base_url=TypeAdapter(AnyHttpUrl).validate_python(base_url) + if base_url is not None + else None, deployment_id=deployment_id, frequency_penalty=frequency_penalty, max_tokens=max_tokens, @@ -1125,7 +1153,9 @@ def azure_openai( """ return _GenerativeOpenAI( api_version=api_version, - base_url=AnyUrl(base_url) if base_url is not None else None, + base_url=TypeAdapter(AnyHttpUrl).validate_python(base_url) + if base_url is not None + else None, deployment_id=deployment_id, frequency_penalty=frequency_penalty, max_tokens=max_tokens, @@ -1162,7 +1192,9 @@ def xai( top_p: The top P to use. Defaults to `None`, which uses the server-defined default """ return _GenerativeXAI( - base_url=AnyUrl(base_url) if base_url is not None else None, + base_url=TypeAdapter(AnyHttpUrl).validate_python(base_url) + if base_url is not None + else None, max_tokens=max_tokens, model=model, temperature=temperature, diff --git a/weaviate/collections/collections/async_.pyi b/weaviate/collections/collections/async_.pyi index af66b4469..4aeb7b491 100644 --- a/weaviate/collections/collections/async_.pyi +++ b/weaviate/collections/collections/async_.pyi @@ -18,6 +18,7 @@ from weaviate.collections.classes.config import ( _VectorIndexConfigCreate, _VectorizerConfigCreate, ) +from weaviate.collections.classes.config_object_ttl import _ObjectTTLConfigCreate from weaviate.collections.classes.internal import References from weaviate.collections.classes.types import ( Properties, @@ -36,6 +37,7 @@ class _CollectionsAsync(_CollectionsBase[ConnectionAsync]): generative_config: Optional[_GenerativeProvider] = None, inverted_index_config: Optional[_InvertedIndexConfigCreate] = None, multi_tenancy_config: Optional[_MultiTenancyConfigCreate] = None, + object_ttl_config: Optional[_ObjectTTLConfigCreate] = None, properties: Optional[Sequence[Property]] = None, references: Optional[List[_ReferencePropertyBase]] = None, replication_config: Optional[_ReplicationConfigCreate] = None, @@ -62,6 +64,7 @@ class _CollectionsAsync(_CollectionsBase[ConnectionAsync]): generative_config: Optional[_GenerativeProvider] = None, inverted_index_config: Optional[_InvertedIndexConfigCreate] = None, multi_tenancy_config: Optional[_MultiTenancyConfigCreate] = None, + object_ttl_config: Optional[_ObjectTTLConfigCreate] = None, properties: Optional[Sequence[Property]] = None, references: Optional[List[_ReferencePropertyBase]] = None, replication_config: Optional[_ReplicationConfigCreate] = None, @@ -88,6 +91,7 @@ class _CollectionsAsync(_CollectionsBase[ConnectionAsync]): generative_config: Optional[_GenerativeProvider] = None, inverted_index_config: Optional[_InvertedIndexConfigCreate] = None, multi_tenancy_config: Optional[_MultiTenancyConfigCreate] = None, + object_ttl_config: Optional[_ObjectTTLConfigCreate] = None, properties: Optional[Sequence[Property]] = None, references: Optional[List[_ReferencePropertyBase]] = None, replication_config: Optional[_ReplicationConfigCreate] = None, diff --git a/weaviate/collections/collections/executor.py b/weaviate/collections/collections/executor.py index b0088fd75..69b2baa64 100644 --- a/weaviate/collections/collections/executor.py +++ b/weaviate/collections/collections/executor.py @@ -23,6 +23,7 @@ _InvertedIndexConfigCreate, _MultiTenancyConfigCreate, _NamedVectorConfigCreate, + _ObjectTTLConfigCreate, _ReferencePropertyBase, _ReplicationConfigCreate, _RerankerProvider, @@ -152,6 +153,7 @@ def create( generative_config: Optional[_GenerativeProvider] = None, inverted_index_config: Optional[_InvertedIndexConfigCreate] = None, multi_tenancy_config: Optional[_MultiTenancyConfigCreate] = None, + object_ttl_config: Optional[_ObjectTTLConfigCreate] = None, properties: Optional[Sequence[Property]] = None, references: Optional[List[_ReferencePropertyBase]] = None, replication_config: Optional[_ReplicationConfigCreate] = None, @@ -189,6 +191,7 @@ def create( generative_config: The configuration for Weaviate's generative capabilities. inverted_index_config: The configuration for Weaviate's inverted index. multi_tenancy_config: The configuration for Weaviate's multi-tenancy capabilities. + object_ttl_config: The configuration for Weaviate's object time-to-live (TTL) feature. properties: The properties of the objects in the collection. references: The references of the objects in the collection. replication_config: The configuration for Weaviate's replication strategy. @@ -219,6 +222,7 @@ def create( name=name, properties=properties, references=references, + object_ttl_config=object_ttl_config, replication_config=replication_config, reranker_config=reranker_config, sharding_config=sharding_config, diff --git a/weaviate/collections/collections/sync.pyi b/weaviate/collections/collections/sync.pyi index 1ab4c3cc1..22f3356e6 100644 --- a/weaviate/collections/collections/sync.pyi +++ b/weaviate/collections/collections/sync.pyi @@ -18,6 +18,7 @@ from weaviate.collections.classes.config import ( _VectorIndexConfigCreate, _VectorizerConfigCreate, ) +from weaviate.collections.classes.config_object_ttl import _ObjectTTLConfigCreate from weaviate.collections.classes.internal import References from weaviate.collections.classes.types import ( Properties, @@ -36,6 +37,7 @@ class _Collections(_CollectionsBase[ConnectionSync]): generative_config: Optional[_GenerativeProvider] = None, inverted_index_config: Optional[_InvertedIndexConfigCreate] = None, multi_tenancy_config: Optional[_MultiTenancyConfigCreate] = None, + object_ttl_config: Optional[_ObjectTTLConfigCreate] = None, properties: Optional[Sequence[Property]] = None, references: Optional[List[_ReferencePropertyBase]] = None, replication_config: Optional[_ReplicationConfigCreate] = None, @@ -62,6 +64,7 @@ class _Collections(_CollectionsBase[ConnectionSync]): generative_config: Optional[_GenerativeProvider] = None, inverted_index_config: Optional[_InvertedIndexConfigCreate] = None, multi_tenancy_config: Optional[_MultiTenancyConfigCreate] = None, + object_ttl_config: Optional[_ObjectTTLConfigCreate] = None, properties: Optional[Sequence[Property]] = None, references: Optional[List[_ReferencePropertyBase]] = None, replication_config: Optional[_ReplicationConfigCreate] = None, @@ -88,6 +91,7 @@ class _Collections(_CollectionsBase[ConnectionSync]): generative_config: Optional[_GenerativeProvider] = None, inverted_index_config: Optional[_InvertedIndexConfigCreate] = None, multi_tenancy_config: Optional[_MultiTenancyConfigCreate] = None, + object_ttl_config: Optional[_ObjectTTLConfigCreate] = None, properties: Optional[Sequence[Property]] = None, references: Optional[List[_ReferencePropertyBase]] = None, replication_config: Optional[_ReplicationConfigCreate] = None, diff --git a/weaviate/collections/config/async_.pyi b/weaviate/collections/config/async_.pyi index 9fcfefdb3..3b07f55c6 100644 --- a/weaviate/collections/config/async_.pyi +++ b/weaviate/collections/config/async_.pyi @@ -22,6 +22,7 @@ from weaviate.collections.classes.config import ( _VectorIndexConfigFlatUpdate, _VectorIndexConfigHNSWUpdate, ) +from weaviate.collections.classes.config_object_ttl import _ObjectTTLConfigUpdate from weaviate.collections.classes.config_vector_index import _VectorIndexConfigDynamicUpdate from weaviate.connect.v4 import ConnectionAsync @@ -43,6 +44,7 @@ class _ConfigCollectionAsync(_ConfigCollectionExecutor[ConnectionAsync]): property_descriptions: Optional[Dict[str, str]] = None, inverted_index_config: Optional[_InvertedIndexConfigUpdate] = None, multi_tenancy_config: Optional[_MultiTenancyConfigUpdate] = None, + object_ttl_config: Optional[_ObjectTTLConfigUpdate] = None, replication_config: Optional[_ReplicationConfigUpdate] = None, vector_index_config: Optional[ Union[_VectorIndexConfigHNSWUpdate, _VectorIndexConfigFlatUpdate] diff --git a/weaviate/collections/config/executor.py b/weaviate/collections/config/executor.py index e5772b76a..bb1f33859 100644 --- a/weaviate/collections/config/executor.py +++ b/weaviate/collections/config/executor.py @@ -43,6 +43,7 @@ _collection_config_from_json, _collection_config_simple_from_json, ) +from weaviate.collections.classes.config_object_ttl import _ObjectTTLConfigUpdate from weaviate.collections.classes.config_vector_index import ( _VectorIndexConfigDynamicUpdate, ) @@ -129,6 +130,7 @@ def update( property_descriptions: Optional[Dict[str, str]] = None, inverted_index_config: Optional[_InvertedIndexConfigUpdate] = None, multi_tenancy_config: Optional[_MultiTenancyConfigUpdate] = None, + object_ttl_config: Optional[_ObjectTTLConfigUpdate] = None, replication_config: Optional[_ReplicationConfigUpdate] = None, vector_index_config: Optional[ Union[ @@ -155,6 +157,9 @@ def update( Args: description: A description of the collection. inverted_index_config: Configuration for the inverted index. Use `Reconfigure.inverted_index` to generate one. + multi_tenancy_config: Configuration for multi-tenancy settings. Use `Reconfigure.multi_tenancy` to generate one. + Only `auto_tenant_creation` is supported. + object_ttl_config: Configuration for object TTL settings. Use `Reconfigure.object_ttl` to generate one. replication_config: Configuration for the replication. Use `Reconfigure.replication` to generate one. reranker_config: Configuration for the reranker. Use `Reconfigure.replication` to generate one. vector_index_config (DEPRECATED use `vector_config`): Configuration for the vector index of the default single vector. Use `Reconfigure.vector_index` to generate one. @@ -163,8 +168,6 @@ def update( Using this argument with a list of `Reconfigure.NamedVectors` is **DEPRECATED**. Use the `vector_config` argument instead in such a case. vector_config: Configuration for the vector index (or indices) of your collection. Use `Reconfigure.Vectors` for both single and multiple vectorizers. Supply a list to update many vectorizers at once. - multi_tenancy_config: Configuration for multi-tenancy settings. Use `Reconfigure.multi_tenancy` to generate one. - Only `auto_tenant_creation` is supported. Raises: weaviate.exceptions.WeaviateInvalidInputError: If the input parameters are invalid. @@ -195,6 +198,7 @@ def update( replication_config=replication_config, vector_index_config=vector_index_config, vectorizer_config=vectorizer_config, + object_ttl_config=object_ttl_config, multi_tenancy_config=multi_tenancy_config, generative_config=generative_config, reranker_config=reranker_config, diff --git a/weaviate/collections/config/sync.pyi b/weaviate/collections/config/sync.pyi index 89f37615e..21fd705ac 100644 --- a/weaviate/collections/config/sync.pyi +++ b/weaviate/collections/config/sync.pyi @@ -22,6 +22,7 @@ from weaviate.collections.classes.config import ( _VectorIndexConfigFlatUpdate, _VectorIndexConfigHNSWUpdate, ) +from weaviate.collections.classes.config_object_ttl import _ObjectTTLConfigUpdate from weaviate.collections.classes.config_vector_index import _VectorIndexConfigDynamicUpdate from weaviate.connect.v4 import ConnectionSync @@ -41,6 +42,7 @@ class _ConfigCollection(_ConfigCollectionExecutor[ConnectionSync]): property_descriptions: Optional[Dict[str, str]] = None, inverted_index_config: Optional[_InvertedIndexConfigUpdate] = None, multi_tenancy_config: Optional[_MultiTenancyConfigUpdate] = None, + object_ttl_config: Optional[_ObjectTTLConfigUpdate] = None, replication_config: Optional[_ReplicationConfigUpdate] = None, vector_index_config: Optional[ Union[_VectorIndexConfigHNSWUpdate, _VectorIndexConfigFlatUpdate] diff --git a/weaviate/util.py b/weaviate/util.py index 77840f3ba..f9b8e9385 100644 --- a/weaviate/util.py +++ b/weaviate/util.py @@ -731,8 +731,9 @@ def _datetime_from_weaviate_str(string: str) -> datetime.datetime: return datetime.datetime.strptime(string, date_format) except ValueError as e: # note that the year 9999 is valid and does not need to be handled. for 5 digit years only the first - # 4 digits are considered and it wrapps around - if "year 0 is out of range" in str(e): + # 4 digits are considered and it wrapps around. The datetime library changed the error message in python 3.14 + # to include "year must be in ...", so we check for both messages here. + if "year 0 is out of range" in str(e) or "year must be in" in str(e): _Warnings.datetime_year_zero(string) return datetime.datetime.min raise e