diff --git a/.gitignore b/.gitignore index cee7dc4..5fe4b08 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +uv.toml /resources/secrets/ /resources/data/ /resources/logs/ @@ -11,3 +12,11 @@ /wrench-code-library.iml /WrenchCL.iml /site/* +/.hypothesis +/.kiro +/legacy_logging_reference +/benchmark_report.html +/benchmark_report_generator.py +/benchmark_results.json +logging_v2_demo.py +test_logging_visual.py \ No newline at end of file diff --git a/WrenchCL/Connect/AwsClientHub.py b/WrenchCL/Connect/AwsClientHub.py index 0d20b11..3258517 100644 --- a/WrenchCL/Connect/AwsClientHub.py +++ b/WrenchCL/Connect/AwsClientHub.py @@ -63,6 +63,7 @@ def reload_config(self, env_path: Optional[str] = None, **kwargs): def config(self) -> _ConfigurationManager: """Loaded configuration object.""" self._initialize() + assert self.__config is not None return self.__config @property @@ -126,9 +127,11 @@ def _init_rds_client(self): """Initialize the database client, applying PGHOST/PGPORT override or setting up an SSH tunnel if configured.""" try: if self.config and isinstance(self.config, _ConfigurationManager): + db_port = self.config.pgport_override or self.config.db_port + assert db_port is not None config = { "PGHOST": self.config.pghost_override or self.config.db_host, - "PGPORT": int(self.config.pgport_override or self.config.db_port), + "PGPORT": int(db_port), "PGDATABASE": self.config.db_name, "PGUSER": self.config.db_user, "PGPASSWORD": self.config.db_pass, @@ -138,7 +141,7 @@ def _init_rds_client(self): self.config.ssh_user, self.config.pem_path or self.config.ssh_password, ]): - config["SSH_TUNNEL"] = { + config["SSH_TUNNEL"] = { # type: ignore "SSH_SERVER": self.config.ssh_server, "SSH_PORT": self.config.ssh_port, "SSH_USER": self.config.ssh_user, @@ -181,7 +184,7 @@ def _rds_handle_configuration(self, config: dict) -> "psycopg2.extensions.connec password=config["PGPASSWORD"], ) - def get_secret(self, secret_id: str = None) -> Union[dict, str, None]: + def get_secret(self, secret_id: Optional[str] = None) -> Union[dict, str, None]: """ Retrieve a secret by ARN or default from config. diff --git a/WrenchCL/Connect/ProcessingTracker.py b/WrenchCL/Connect/ProcessingTracker.py index aa017e9..84542b3 100644 --- a/WrenchCL/Connect/ProcessingTracker.py +++ b/WrenchCL/Connect/ProcessingTracker.py @@ -7,14 +7,18 @@ import uuid from dataclasses import dataclass, field from datetime import datetime, timezone -from typing import Literal, Optional - -from Connect import AwsClientHub -from Decorators import SingletonClass -from typing_extensions import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal, Optional from .. import logger -from ..Types.TTLSet import TTLSet +from ..Connect import AwsClientHub +from ..Decorators import SingletonClass + +# from ..Types.TTLSet import TTLSet +class TTLSet(set): # type: ignore[override] + """Stub for TTLSet — accepts ttl kwarg, behaves as a plain set until TTLSet is implemented.""" + + def __init__(self, *args: object, ttl: int = 0, **kwargs: object) -> None: + super().__init__(*args, **kwargs) if TYPE_CHECKING: from mypy_boto3_rds import RDSClient @@ -160,8 +164,8 @@ class ProcessingTracker: _lock = threading.RLock() _running_job_ids: dict[str, ProcessingEvent] = {} - _finished_job_ids = TTLSet(ttl=600) - _failed_job_ids = TTLSet(ttl=600) + _finished_job_ids = TTLSet(ttl=600) # type: ignore + _failed_job_ids = TTLSet(ttl=600) # type: ignore def __init__( self, service_name: str, processor_name: str, sql_client: Optional["RDSClient"] = None diff --git a/WrenchCL/Connect/RdsServiceGateway.py b/WrenchCL/Connect/RdsServiceGateway.py index 12b4ed6..b7a37e9 100644 --- a/WrenchCL/Connect/RdsServiceGateway.py +++ b/WrenchCL/Connect/RdsServiceGateway.py @@ -2,6 +2,7 @@ # Author: Willem van der Schans. # Licensed under the MIT License (https://opensource.org/license/mit). +import importlib.util import json import math from datetime import datetime, timedelta @@ -9,7 +10,12 @@ from uuid import UUID if TYPE_CHECKING: + import pandas as pd from mypy_boto3_rds.client import RDSClient +elif importlib.util.find_spec("pandas") is not None: + import pandas as pd # type: ignore[assignment] +else: + from .._Internal._MockPandas import _MockPandas as pd # type: ignore[assignment] import psycopg2 import psycopg2.extensions @@ -17,16 +23,9 @@ from psycopg2.pool import ThreadedConnectionPool from .. import logger -from .._Internal._MockPandas import _MockPandas from ..Decorators.SingletonClass import SingletonClass from .AwsClientHub import AwsClientHub -try: - import pandas as pd -except ImportError: - pd = _MockPandas() -DataFrame = pd.DataFrame - @SingletonClass class RdsServiceGateway: @@ -57,7 +56,7 @@ def __init__( if self.multithreaded: # Initialize a threaded connection pool using the URI - self.pool: Optional[psycopg2.pool] = ThreadedConnectionPool( + self.pool: Optional[ThreadedConnectionPool] = ThreadedConnectionPool( minconn=min_pool_size, maxconn=max_pool_size, dsn=self.db_uri ) else: @@ -76,6 +75,7 @@ def get_connection(self) -> Union["psycopg2.extensions.connection", "RDSClient"] :rtype: psycopg2.extensions.connection """ if self.multithreaded: + assert self.pool is not None return self.pool.getconn() return self.connection @@ -86,6 +86,7 @@ def release_connection(self, conn: "psycopg2.extensions.connection"): """ if self.multithreaded: + assert self.pool is not None self.pool.putconn(conn) def get_data( @@ -175,7 +176,7 @@ def update_database( try: # Convert payload into a tuple if it's a single value or list - payload = self.convert_payload(payload) + payload = self.convert_payload(payload) # type: ignore logger._internal.log_internal(f"Converted payload: {payload}") if isinstance(payload, tuple): @@ -301,7 +302,7 @@ def convert_payload(self, payload: Tuple[Any, ...]) -> pd.DataFrame | tuple[Any, :return: A tuple with converted values. :rtype: Tuple[Any, ...] """ - if isinstance(payload, DataFrame): + if isinstance(payload, pd.DataFrame): return self._convert_dataframe_types(payload) else: return tuple(self._convert_value(val) for val in payload) diff --git a/WrenchCL/Connect/S3ServiceGateway.py b/WrenchCL/Connect/S3ServiceGateway.py index d9d455f..bab5a37 100644 --- a/WrenchCL/Connect/S3ServiceGateway.py +++ b/WrenchCL/Connect/S3ServiceGateway.py @@ -44,7 +44,7 @@ def __init__(self, config: Optional["Config"] = None): logger._internal.log_internal("S3ServiceGateway initialized with S3 client.") @staticmethod - def _get_mime_extension(mime_type: str) -> str: + def _get_mime_extension(mime_type: str) -> Optional[str]: """Get the file extension for a given MIME type.""" return mimetypes.guess_extension(mime_type) @@ -112,9 +112,9 @@ def upload_file( if not self.test_mode: self.s3_client.upload_fileobj(file_obj, bucket_name, object_key) elif hasattr(file, "read") and callable(file.read): - if file.seek(0, 2) == 0: # Move to the end of the file and check the position + if file.seek(0, 2) == 0: # type: ignore raise ValueError("The file-like object is empty.") - file.seek(0) # Move back to the beginning of the file + file.seek(0) # type: ignore logger.info( f"Uploading file-like object to bucket: {bucket_name} as object: {object_key}" ) @@ -274,7 +274,7 @@ def check_object_existence(self, bucket_name: str, object_key: str) -> bool: raise @Retryable() - def list_objects(self, bucket_name: str, prefix: str = None) -> list: + def list_objects(self, bucket_name: str, prefix: Optional[str] = None) -> list: """ Lists objects in an S3 bucket, optionally filtered by a prefix. diff --git a/WrenchCL/Connect/_Internal/_boto_cache.py b/WrenchCL/Connect/_Internal/_boto_cache.py index 210d88f..42cdee1 100644 --- a/WrenchCL/Connect/_Internal/_boto_cache.py +++ b/WrenchCL/Connect/_Internal/_boto_cache.py @@ -25,7 +25,7 @@ def _get_s3_client(profile: str, region: str, config: Optional[Config] = None) - return client except ImportError: - _get_boto3_session = None - _fetch_secret_from_secretsmanager = None - _get_s3_client = None - Config = None + _get_boto3_session = None # type: ignore + _fetch_secret_from_secretsmanager = None # type: ignore + _get_s3_client = None # type: ignore + Config = None # type: ignore diff --git a/WrenchCL/Connect/__init__.py b/WrenchCL/Connect/__init__.py index 8e3a842..1a7ea6c 100644 --- a/WrenchCL/Connect/__init__.py +++ b/WrenchCL/Connect/__init__.py @@ -1,47 +1,38 @@ """AWS service integrations - requires 'aws' extra.""" -try: - # Test all required AWS dependencies first - import boto3 - import botocore - import paramiko - import psycopg2 - from sshtunnel import SSHTunnelForwarder - - assert SSHTunnelForwarder - assert psycopg2 - assert paramiko - assert boto3 - assert botocore - # Now try to import our classes (which may have additional dependencies) - from .AwsClientHub import AwsClientHub - from .Lambda import handle_lambda_response - from .RdsServiceGateway import RdsServiceGateway - from .S3ServiceGateway import S3ServiceGateway - -except ImportError as e: - # Create a more specific error message based on what failed - error_details = str(e) - messages = [] - # Map common errors to specific packages - if "boto3" in error_details: - messages.append("boto3 and related AWS packages") - elif "psycopg2" in error_details: - messages.append("psycopg2-binary (PostgreSQL adapter)") - elif "paramiko" in error_details: - messages.append("paramiko (SSH client)") - elif "sshtunnel" in error_details: - messages.append("sshtunnel (SSH tunneling)") - elif "botocore" in error_details: - messages.append("botocore and related AWS type stubs") - else: - messages.append("AWS-related dependencies") - missing_pkg = "\n -".join(messages) +import importlib.util +import sys + + +def _dep_available(name: str) -> bool: + if name in sys.modules: + return True + try: + return importlib.util.find_spec(name) is not None + except (ValueError, ModuleNotFoundError): + return False + + +_aws_deps = { + "boto3": "boto3", + "botocore": "botocore", + "paramiko": "paramiko", + "psycopg2": "psycopg2-binary", + "sshtunnel": "sshtunnel", +} +_missing = [pkg for mod, pkg in _aws_deps.items() if not _dep_available(mod)] + +if _missing: + missing_str = "\n -".join(_missing) raise ImportError( f"AWS functionality requires additional dependencies.\n" - f"Missing Packages:\n -{missing_pkg}\n" - f"Install with: pip install 'WrenchCL[aws]'\n" - f"Original error: {error_details}" - ) from e + f"Missing Packages:\n -{missing_str}\n" + f"Install with: pip install 'WrenchCL[aws]'" + ) from None + +from .AwsClientHub import AwsClientHub +from .Lambda import handle_lambda_response +from .RdsServiceGateway import RdsServiceGateway +from .S3ServiceGateway import S3ServiceGateway __all__ = ["AwsClientHub", "RdsServiceGateway", "S3ServiceGateway", "handle_lambda_response"] diff --git a/WrenchCL/Decorators/Deprecated.py b/WrenchCL/Decorators/Deprecated.py index 9ad913b..a3ab351 100644 --- a/WrenchCL/Decorators/Deprecated.py +++ b/WrenchCL/Decorators/Deprecated.py @@ -3,11 +3,12 @@ # Licensed under the MIT License (https://opensource.org/license/mit). import warnings from functools import wraps +from typing import Optional __depr_tracker__ = set() -def Deprecated(message: str = None): +def Deprecated(message: Optional[str] = None): """ Wraps a function with a decorator that warns the user the function is Deprecated. It also allows an optional custom message to be displayed when the function is used. diff --git a/WrenchCL/Decorators/Retryable.py b/WrenchCL/Decorators/Retryable.py index 13bd2dc..4b3b2b2 100644 --- a/WrenchCL/Decorators/Retryable.py +++ b/WrenchCL/Decorators/Retryable.py @@ -17,8 +17,8 @@ try: from botocore.exceptions import BotoCoreError, ClientError except ImportError: - ClientError = Exception # Fallback to base Exception - BotoCoreError = Exception + ClientError = Exception # type: ignore + BotoCoreError = Exception # type: ignore def Retryable(_func=None, *, max_retries=2, retry_on_exceptions=None, delay=2, verbose=False): diff --git a/WrenchCL/Decorators/SingletonClass.py b/WrenchCL/Decorators/SingletonClass.py index 725aad4..e35b3fc 100644 --- a/WrenchCL/Decorators/SingletonClass.py +++ b/WrenchCL/Decorators/SingletonClass.py @@ -1,10 +1,14 @@ # Copyright (c) 2024-2025. # Author: Willem van der Schans. # Licensed under the MIT License (https://opensource.org/license/mit). +from typing import Type, TypeVar, cast + from ..Exceptions._internal import _SingletonViolationException +_T = TypeVar("_T") + -def SingletonClass(cls: type) -> type: +def SingletonClass(cls: Type[_T]) -> Type[_T]: # type: ignore[shadowed-type-variable,unsupported-base] """ Enforces singleton behavior by wrapping the class in a custom subclass. @@ -18,10 +22,10 @@ def SingletonClass(cls: type) -> type: if "__cls_instance" in cls.__dict__: raise _SingletonViolationException(cls) - class SingletonWrapper(cls): + class SingletonWrapper(cls): # type: ignore __cls_instance = None - def __new__(cls_, *args, **kwargs): + def __new__(cls_, *args, **kwargs): # type: ignore[shadowed-type-variable] if cls_.__cls_instance is None: cls_.__cls_instance = super(SingletonWrapper, cls_).__new__(cls_) return cls_.__cls_instance @@ -34,4 +38,4 @@ def __init__(self, *args, **kwargs): SingletonWrapper.__name__ = cls.__name__ SingletonWrapper.__qualname__ = cls.__qualname__ SingletonWrapper.__doc__ = cls.__doc__ - return SingletonWrapper + return cast(Type[_T], SingletonWrapper) diff --git a/WrenchCL/Exceptions/ExceptionSuggestor.py b/WrenchCL/Exceptions/ExceptionSuggestor.py index 86d3c0e..3b2c418 100644 --- a/WrenchCL/Exceptions/ExceptionSuggestor.py +++ b/WrenchCL/Exceptions/ExceptionSuggestor.py @@ -5,7 +5,7 @@ import re from collections.abc import Mapping from difflib import get_close_matches -from typing import TYPE_CHECKING, Iterable, List, Optional, Union +from typing import TYPE_CHECKING, Iterable, List, Optional, Union, cast if TYPE_CHECKING: import pandas @@ -53,7 +53,7 @@ def suggest_similar( def suggest_for_pandas_column( cls, missing_column: str, dataframe_columns: Iterable[str] ) -> Optional[str]: - return cls.suggest_similar( + return cast(Optional[str], cls.suggest_similar( missing_key=missing_column, available_keys=dataframe_columns, n_suggestions=1, @@ -61,11 +61,11 @@ def suggest_for_pandas_column( case_insensitive=True, return_message=True, custom_message="Column '{}' not found. Did you mean: {}?".format(missing_column, "{}"), - ) + )) @classmethod def suggest_for_dict_key(cls, missing_key: str, dict_keys: Iterable[str]) -> Optional[str]: - return cls.suggest_similar( + return cast(Optional[str], cls.suggest_similar( missing_key=missing_key, available_keys=dict_keys, n_suggestions=3, @@ -73,13 +73,13 @@ def suggest_for_dict_key(cls, missing_key: str, dict_keys: Iterable[str]) -> Opt case_insensitive=True, return_message=True, custom_message="Key '{}' not found. Possible matches: {}".format(missing_key, "{}"), - ) + )) @classmethod def suggest_for_cli_option( cls, invalid_option: str, valid_options: Iterable[str] ) -> Optional[str]: - return cls.suggest_similar( + return cast(Optional[str], cls.suggest_similar( missing_key=invalid_option, available_keys=valid_options, n_suggestions=3, @@ -89,13 +89,13 @@ def suggest_for_cli_option( custom_message="Unrecognized option '{}'. Did you mean: {}?".format( invalid_option, "{}" ), - ) + )) @classmethod def suggest_for_api_field( cls, missing_field: str, valid_fields: Iterable[str] ) -> Optional[str]: - return cls.suggest_similar( + return cast(Optional[str], cls.suggest_similar( missing_key=missing_field, available_keys=valid_fields, n_suggestions=2, @@ -103,7 +103,7 @@ def suggest_for_api_field( case_insensitive=True, return_message=True, custom_message="Field '{}' not found. Closest matches: {}".format(missing_field, "{}"), - ) + )) @classmethod def _is_pandas_df(cls, obj): @@ -113,7 +113,7 @@ def _is_pandas_df(cls, obj): if isinstance(obj, pd.DataFrame): return True except ImportError: - pd = None + pd = None # type: ignore pass try: @@ -122,7 +122,7 @@ def _is_pandas_df(cls, obj): if isinstance(obj, _MockPandas.DataFrame): return True except ImportError: - _MockPandas = None + _MockPandas = None # type: ignore pass return False @@ -167,7 +167,7 @@ def _suggest_for_exception( def suggest( cls, obj: Union[BaseException, "pandas.DataFrame", dict, tuple, set, list, object], - missing_key: str = None, + missing_key: Optional[str] = None, ) -> Optional[str]: """ Auto-detect object type and route to appropriate suggestion method. @@ -177,17 +177,19 @@ def suggest( elif missing_key is None and isinstance(obj, BaseException): return cls._suggest_for_exception(obj) + assert missing_key is not None + if cls._is_pandas_df(obj): - return cls.suggest_for_pandas_column(missing_key, obj.columns) + return cls.suggest_for_pandas_column(missing_key, obj.columns) # type: ignore elif isinstance(obj, Mapping): - return cls.suggest_for_dict_key(missing_key, obj.keys()) + return cls.suggest_for_dict_key(missing_key, obj.keys()) # type: ignore elif isinstance(obj, (list, tuple, set)): - return cls.suggest_for_cli_option(missing_key, obj) + return cls.suggest_for_cli_option(missing_key, obj) # type: ignore else: - return cls.suggest_similar( + return cast(Optional[str], cls.suggest_similar( missing_key=missing_key, available_keys=dir(obj), n_suggestions=2, @@ -197,4 +199,4 @@ def suggest( custom_message="Attribute '{}' not found. Closest matches: {}".format( missing_key, "{}" ), - ) + )) diff --git a/WrenchCL/Exceptions/_internal.py b/WrenchCL/Exceptions/_internal.py index b2e353c..8b90687 100644 --- a/WrenchCL/Exceptions/_internal.py +++ b/WrenchCL/Exceptions/_internal.py @@ -1,7 +1,7 @@ # Copyright (c) 2025. # Author: Willem van der Schans. # Licensed under the MIT License (https://opensource.org/license/mit). -from typing import Any, Type +from typing import Any, Optional, Type class _SingletonViolationException(Exception): @@ -9,7 +9,7 @@ class _SingletonViolationException(Exception): Raised when a class using @SingletonClass improperly defines its own __new__ method. """ - def __init__(self, cls: Type[Any] = None) -> None: + def __init__(self, cls: Optional[Type[Any]] = None) -> None: cls_name = getattr(cls, "__name__", "") msg = ( f"Singleton violation in '{cls_name}':\n" diff --git a/WrenchCL/Tools/FileTyper.py b/WrenchCL/Tools/FileTyper.py index 0894c6a..dac6f28 100644 --- a/WrenchCL/Tools/FileTyper.py +++ b/WrenchCL/Tools/FileTyper.py @@ -40,7 +40,7 @@ def get_file_type( if isinstance(file_source, (str, Path)): if validate_base64(file_source): - base64_data = base64.b64decode(file_source) + base64_data = base64.b64decode(str(file_source)) else: mime_type, _ = mimetypes.guess_type(str(file_source)) if mime_type: diff --git a/WrenchCL/Tools/Image2B64.py b/WrenchCL/Tools/Image2B64.py index 1b0c862..a4740be 100644 --- a/WrenchCL/Tools/Image2B64.py +++ b/WrenchCL/Tools/Image2B64.py @@ -3,6 +3,7 @@ # Licensed under the MIT License (https://opensource.org/license/mit). import base64 +import binascii import hashlib from io import BytesIO @@ -68,5 +69,5 @@ def validate_base64(b64_string): # Decode the base64 string base64.b64decode(b64_string, validate=True) return True - except (base64.binascii.Error, ValueError): + except (binascii.Error, ValueError): return False diff --git a/WrenchCL/Tools/JsonParser.py b/WrenchCL/Tools/JsonParser.py index ae7328f..5f47a34 100644 --- a/WrenchCL/Tools/JsonParser.py +++ b/WrenchCL/Tools/JsonParser.py @@ -50,6 +50,7 @@ def parse_json( if verbose: logger._internal.log_internal(f"Starting JSON parsing. Max depth: {max_depth}") parsed_json = recur_parse_json(response, max_depth=max_depth, verbose=verbose) + assert isinstance(parsed_json, dict) if print_tree: show_json_tree(parsed_json) return parsed_json @@ -151,9 +152,9 @@ def recur_parse_json( if not isinstance(d, dict) and depth == 0: raise TypeError(f"Expected dictionary but got {type(d).__name__}") - elif not isinstance(d, dict) and depth != 0: + elif not isinstance(d, dict): return d - else: + elif isinstance(d, dict): for k, v in d.items(): if isinstance(v, dict): d[k] = recur_parse_json(v, depth=depth + 1, max_depth=max_depth, verbose=verbose) @@ -177,6 +178,7 @@ def recur_parse_json( f"{indent}>Parsed key '{k}': to type {type(d[k]).__name__}" ) return d + return d def list_loader(v: Any, depth: int = 0, max_depth: int = 25, verbose=False) -> list: diff --git a/WrenchCL/Tools/JsonSerializer.py b/WrenchCL/Tools/JsonSerializer.py index 55e057b..87ebe6f 100644 --- a/WrenchCL/Tools/JsonSerializer.py +++ b/WrenchCL/Tools/JsonSerializer.py @@ -3,11 +3,12 @@ # Licensed under the MIT License (https://opensource.org/license/mit). import json import re +from collections.abc import Callable from datetime import date, datetime from decimal import Decimal from enum import Enum from pathlib import Path -from typing import Any +from typing import Any, Optional from uuid import UUID @@ -83,8 +84,8 @@ class RobustJSONEncoder(json.JSONEncoder): JSONEncoder subclass that uses robust_serializer for unsupported objects. """ - def default(self, obj: Any) -> Any: - return robust_serializer(obj) + def default(self, o: object) -> Any: + return robust_serializer(o) class single_quote_decoder(json.JSONDecoder): @@ -111,9 +112,9 @@ class single_quote_decoder(json.JSONDecoder): {'name': 'John', 'age': 30, 'city': 'New York'} """ - def __init__(self, object_hook=None, *args, **kwargs): + def __init__(self, object_hook: Optional[Callable[[dict[str, Any]], Any]] = None, *args, **kwargs): super().__init__(object_hook=object_hook, *args, **kwargs) - self.object_hook = object_hook + self.object_hook = object_hook # type: ignore def decode(self, s, *args, **kwargs): # Remove everything before ```json or ```python, including the marker itself @@ -167,3 +168,4 @@ def sanitize_unescaped_quotes_and_load_json_str(s: str, strict=False) -> dict: # Escape it to \" js_str = js_str[:prev_quote_index] + "\\" + js_str[prev_quote_index:] + raise json.JSONDecodeError("Failed to parse JSON string", js_str, prev_pos) diff --git a/WrenchCL/Tools/StandardizeNone.py b/WrenchCL/Tools/StandardizeNone.py index 97c54cc..a6f24f9 100644 --- a/WrenchCL/Tools/StandardizeNone.py +++ b/WrenchCL/Tools/StandardizeNone.py @@ -1,18 +1,19 @@ # Copyright (c) 2024-2025. # Author: Willem van der Schans. # Licensed under the MIT License (https://opensource.org/license/mit). -from typing import Any +import importlib.util +from typing import TYPE_CHECKING, Any, Optional -from .._Internal._MockPandas import _MockPandas - -try: +if TYPE_CHECKING: import pandas as pd -except ImportError: - pd = _MockPandas() +elif importlib.util.find_spec("pandas") is not None: + import pandas as pd # type: ignore[assignment] +else: + from .._Internal._MockPandas import _MockPandas as pd # type: ignore[assignment] def standardize_none( - data: Any, none_like_values: set = None, evaluate_as_string: bool = False + data: Any, none_like_values: Optional[set[str]] = None, evaluate_as_string: bool = False ) -> Any: """ Recursively standardizes mistyped None values to proper None. diff --git a/WrenchCL/_Internal/Logging/Api/__init__.py b/WrenchCL/_Internal/Logging/Api/__init__.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/Api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/Api/__pycache__/__init__.cpython-311.pyc b/WrenchCL/_Internal/Logging/Api/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index f8b9018..0000000 Binary files a/WrenchCL/_Internal/Logging/Api/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/Api/__pycache__/__init__.cpython-314.pyc b/WrenchCL/_Internal/Logging/Api/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index b51c0b1..0000000 Binary files a/WrenchCL/_Internal/Logging/Api/__pycache__/__init__.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/Api/__pycache__/base_logger.cpython-311.pyc b/WrenchCL/_Internal/Logging/Api/__pycache__/base_logger.cpython-311.pyc deleted file mode 100644 index 5e8d793..0000000 Binary files a/WrenchCL/_Internal/Logging/Api/__pycache__/base_logger.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/Api/__pycache__/base_logger.cpython-314.pyc b/WrenchCL/_Internal/Logging/Api/__pycache__/base_logger.cpython-314.pyc deleted file mode 100644 index 16ae577..0000000 Binary files a/WrenchCL/_Internal/Logging/Api/__pycache__/base_logger.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/Api/__pycache__/internal_api.cpython-311.pyc b/WrenchCL/_Internal/Logging/Api/__pycache__/internal_api.cpython-311.pyc deleted file mode 100644 index d1bc9da..0000000 Binary files a/WrenchCL/_Internal/Logging/Api/__pycache__/internal_api.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/Api/__pycache__/internal_api.cpython-314.pyc b/WrenchCL/_Internal/Logging/Api/__pycache__/internal_api.cpython-314.pyc deleted file mode 100644 index 3853a07..0000000 Binary files a/WrenchCL/_Internal/Logging/Api/__pycache__/internal_api.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/Api/__pycache__/managed_loggers.cpython-311.pyc b/WrenchCL/_Internal/Logging/Api/__pycache__/managed_loggers.cpython-311.pyc deleted file mode 100644 index 48c2236..0000000 Binary files a/WrenchCL/_Internal/Logging/Api/__pycache__/managed_loggers.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/Api/__pycache__/managed_loggers.cpython-314.pyc b/WrenchCL/_Internal/Logging/Api/__pycache__/managed_loggers.cpython-314.pyc deleted file mode 100644 index eac6d86..0000000 Binary files a/WrenchCL/_Internal/Logging/Api/__pycache__/managed_loggers.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/Api/__pycache__/stream_manager.cpython-311.pyc b/WrenchCL/_Internal/Logging/Api/__pycache__/stream_manager.cpython-311.pyc deleted file mode 100644 index 2bce4f4..0000000 Binary files a/WrenchCL/_Internal/Logging/Api/__pycache__/stream_manager.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/Api/__pycache__/stream_manager.cpython-314.pyc b/WrenchCL/_Internal/Logging/Api/__pycache__/stream_manager.cpython-314.pyc deleted file mode 100644 index 7619332..0000000 Binary files a/WrenchCL/_Internal/Logging/Api/__pycache__/stream_manager.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/Api/base_logger.py b/WrenchCL/_Internal/Logging/Api/base_logger.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/Api/base_logger.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/Api/internal_api.py b/WrenchCL/_Internal/Logging/Api/internal_api.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/Api/internal_api.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/Api/managed_loggers.py b/WrenchCL/_Internal/Logging/Api/managed_loggers.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/Api/managed_loggers.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/Api/stream_manager.py b/WrenchCL/_Internal/Logging/Api/stream_manager.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/Api/stream_manager.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/Api/usage_example.py b/WrenchCL/_Internal/Logging/Api/usage_example.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/Api/usage_example.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/ColorService.py b/WrenchCL/_Internal/Logging/ColorService.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/ColorService.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/ContextFilter.py b/WrenchCL/_Internal/Logging/ContextFilter.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/ContextFilter.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/DataClasses.py b/WrenchCL/_Internal/Logging/DataClasses.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/DataClasses.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/DatadogTraceInjectionFilter.py b/WrenchCL/_Internal/Logging/DatadogTraceInjectionFilter.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/DatadogTraceInjectionFilter.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/Formatters.py b/WrenchCL/_Internal/Logging/Formatters.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/Formatters.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/LogManagers.py b/WrenchCL/_Internal/Logging/LogManagers.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/LogManagers.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/LoggerConfigState.py b/WrenchCL/_Internal/Logging/LoggerConfigState.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/LoggerConfigState.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/MarkupHandlers.py b/WrenchCL/_Internal/Logging/MarkupHandlers.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/MarkupHandlers.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/MessageProcessors.py b/WrenchCL/_Internal/Logging/MessageProcessors.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/MessageProcessors.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/__init__.py b/WrenchCL/_Internal/Logging/__init__.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/Logging/__pycache__/ColorService.cpython-311.pyc b/WrenchCL/_Internal/Logging/__pycache__/ColorService.cpython-311.pyc deleted file mode 100644 index 7c04795..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/ColorService.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/ColorService.cpython-314.pyc b/WrenchCL/_Internal/Logging/__pycache__/ColorService.cpython-314.pyc deleted file mode 100644 index 93c2210..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/ColorService.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/ContextFilter.cpython-311.pyc b/WrenchCL/_Internal/Logging/__pycache__/ContextFilter.cpython-311.pyc deleted file mode 100644 index 81d8745..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/ContextFilter.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/ContextFilter.cpython-314.pyc b/WrenchCL/_Internal/Logging/__pycache__/ContextFilter.cpython-314.pyc deleted file mode 100644 index a2fe4ef..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/ContextFilter.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/DataClasses.cpython-311.pyc b/WrenchCL/_Internal/Logging/__pycache__/DataClasses.cpython-311.pyc deleted file mode 100644 index 0726a12..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/DataClasses.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/DataClasses.cpython-314.pyc b/WrenchCL/_Internal/Logging/__pycache__/DataClasses.cpython-314.pyc deleted file mode 100644 index ce6e0df..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/DataClasses.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/DatadogTraceInjectionFilter.cpython-311.pyc b/WrenchCL/_Internal/Logging/__pycache__/DatadogTraceInjectionFilter.cpython-311.pyc deleted file mode 100644 index 38c1a2b..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/DatadogTraceInjectionFilter.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/Formatters.cpython-311.pyc b/WrenchCL/_Internal/Logging/__pycache__/Formatters.cpython-311.pyc deleted file mode 100644 index a4c2873..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/Formatters.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/Formatters.cpython-314.pyc b/WrenchCL/_Internal/Logging/__pycache__/Formatters.cpython-314.pyc deleted file mode 100644 index 222ea5d..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/Formatters.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/LogManagers.cpython-311.pyc b/WrenchCL/_Internal/Logging/__pycache__/LogManagers.cpython-311.pyc deleted file mode 100644 index 2792895..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/LogManagers.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/LogManagers.cpython-314.pyc b/WrenchCL/_Internal/Logging/__pycache__/LogManagers.cpython-314.pyc deleted file mode 100644 index 4ed6cdc..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/LogManagers.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/LoggerConfigState.cpython-311.pyc b/WrenchCL/_Internal/Logging/__pycache__/LoggerConfigState.cpython-311.pyc deleted file mode 100644 index a4ce1e7..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/LoggerConfigState.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/LoggerConfigState.cpython-314.pyc b/WrenchCL/_Internal/Logging/__pycache__/LoggerConfigState.cpython-314.pyc deleted file mode 100644 index 75386f7..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/LoggerConfigState.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/MarkupHandlers.cpython-311.pyc b/WrenchCL/_Internal/Logging/__pycache__/MarkupHandlers.cpython-311.pyc deleted file mode 100644 index cddc90b..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/MarkupHandlers.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/MarkupHandlers.cpython-314.pyc b/WrenchCL/_Internal/Logging/__pycache__/MarkupHandlers.cpython-314.pyc deleted file mode 100644 index 758e9d5..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/MarkupHandlers.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/MessageProcessors.cpython-311.pyc b/WrenchCL/_Internal/Logging/__pycache__/MessageProcessors.cpython-311.pyc deleted file mode 100644 index 1b4c1a2..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/MessageProcessors.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/MessageProcessors.cpython-314.pyc b/WrenchCL/_Internal/Logging/__pycache__/MessageProcessors.cpython-314.pyc deleted file mode 100644 index 3208b57..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/MessageProcessors.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/__init__.cpython-311.pyc b/WrenchCL/_Internal/Logging/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 0ce29cf..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/__init__.cpython-314.pyc b/WrenchCL/_Internal/Logging/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index f977aba..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/__init__.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/logging_utils.cpython-311.pyc b/WrenchCL/_Internal/Logging/__pycache__/logging_utils.cpython-311.pyc deleted file mode 100644 index a62c862..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/logging_utils.cpython-311.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/__pycache__/logging_utils.cpython-314.pyc b/WrenchCL/_Internal/Logging/__pycache__/logging_utils.cpython-314.pyc deleted file mode 100644 index 84313ed..0000000 Binary files a/WrenchCL/_Internal/Logging/__pycache__/logging_utils.cpython-314.pyc and /dev/null differ diff --git a/WrenchCL/_Internal/Logging/logging_utils.py b/WrenchCL/_Internal/Logging/logging_utils.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/Logging/logging_utils.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/_Internal/WrenchLogger.py b/WrenchCL/_Internal/WrenchLogger.py index 490cd6d..9b5c03c 100644 --- a/WrenchCL/_Internal/WrenchLogger.py +++ b/WrenchCL/_Internal/WrenchLogger.py @@ -7,12 +7,11 @@ import warnings from typing import Any, Self -from logspark.Core.SparkLogger import SparkLogger as _SparkLoggerDecorated +from logspark.Core.SparkLogger import SparkLogger from logspark.Handlers import SparkJsonHandler, SparkTerminalHandler from WrenchCL.Decorators import SingletonClass -_BaseSparkLogger = _SparkLoggerDecorated.__bases__[0] _run_id_var: contextvars.ContextVar[str | None] = contextvars.ContextVar( "wrench_run_id", default=None, @@ -38,8 +37,8 @@ def filter(self, record: logging.LogRecord) -> bool: @SingletonClass -class WrenchLogger(_BaseSparkLogger): - def configure( +class WrenchLogger(SparkLogger): + def configure( # type: ignore self, mode: str | None = None, level: str | int = "INFO", diff --git a/WrenchCL/_Internal/_ExceptionSuggestor.py b/WrenchCL/_Internal/_ExceptionSuggestor.py deleted file mode 100644 index b161ca8..0000000 --- a/WrenchCL/_Internal/_ExceptionSuggestor.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) 2025. -# Author: Willem van der Schans. -# Licensed under the MIT License (https://opensource.org/license/mit). - -import inspect -import re -from difflib import get_close_matches -from typing import Optional - - -class _ExceptionSuggestor: - @staticmethod - def suggest_similar( - error: BaseException, frame_depth=20, n_suggestions=1, cutoff=0.6 - ) -> Optional[str]: - if not isinstance(error, BaseException): - return None - error_msg = error.args[0] - if error.__class__.__name__.lower() not in error_msg.lower(): - error_msg = f" {error.__class__.__name__}: {error_msg}" - else: - error_msg = f" {error_msg}" - - obj_match = re.search(r"'(\w+)' object has no attribute", error_msg) - key_match = re.search(r"has no attribute '(\w+)'", error_msg) - - if not key_match: - return error_msg - - source_obj = obj_match.group(1) if obj_match else None - missing_attr = key_match.group(1) - try: - for frame in reversed(inspect.stack()[:frame_depth]): - for var in frame.frame.f_locals.values(): - if not hasattr(var, "__class__"): - continue - if var.__class__.__name__ == source_obj: - keys = [k for k in dir(var) if not k.startswith("__")] - matches = get_close_matches( - missing_attr, keys, n=n_suggestions, cutoff=cutoff - ) - if matches: - return f"{error_msg}\n Did you mean: {', '.join(matches)}?\n" - except Exception: - pass - return error_msg diff --git a/WrenchCL/_Internal/_MockPandas.py b/WrenchCL/_Internal/_MockPandas.py index 7fef633..c5dc4ab 100644 --- a/WrenchCL/_Internal/_MockPandas.py +++ b/WrenchCL/_Internal/_MockPandas.py @@ -65,8 +65,3 @@ def __init__(self): """Mock pandas.options.""" self.options = {} - -try: - import pandas as pd -except ImportError: - pd = _MockPandas() diff --git a/WrenchCL/_Internal/__init__.py b/WrenchCL/_Internal/__init__.py index 047268e..429eb1a 100644 --- a/WrenchCL/_Internal/__init__.py +++ b/WrenchCL/_Internal/__init__.py @@ -1,7 +1,6 @@ """Internal utilities - some require AWS dependencies.""" # Always available -from ._MockPandas import pd from .WrenchLogger import WrenchLogger -__all__ = ["pd", "WrenchLogger"] +__all__ = ["WrenchLogger"] diff --git a/WrenchCL/_Internal/_custom_types.py b/WrenchCL/_Internal/_custom_types.py deleted file mode 100644 index ff712ac..0000000 --- a/WrenchCL/_Internal/_custom_types.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2025. -# Author: Willem van der Schans. -# Licensed under the MIT License (https://opensource.org/license/mit). -from enum import Enum - - -class StdStreamMode(str, Enum): - NONE = "none" - STDERR = "stderr" - BOTH = "both" diff --git a/WrenchCL/_Internal/cLogger.py b/WrenchCL/_Internal/cLogger.py deleted file mode 100644 index 841360d..0000000 --- a/WrenchCL/_Internal/cLogger.py +++ /dev/null @@ -1 +0,0 @@ -# Removed in WrenchCL v6 — replaced by WrenchCL._Internal.WrenchLogger diff --git a/WrenchCL/__init__.py b/WrenchCL/__init__.py index 25abdcc..e0d0b90 100644 --- a/WrenchCL/__init__.py +++ b/WrenchCL/__init__.py @@ -1,10 +1,8 @@ """WrenchCL - Core functionality always available.""" -from typing import Any - from ._Internal import WrenchLogger # noinspection PyUnusedFunction,PySameParameterValue -logger: Any = WrenchLogger() +logger: WrenchLogger = WrenchLogger() __all__ = ["logger"] diff --git a/WrenchCL/py.typed b/WrenchCL/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index d996fab..c54994d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,6 @@ authors = [ ] maintainers = [ { name = "Willem van der Schans", email = "willem@wrench.ai" }, - { name = "Jeong Kim", email = "jeong@wrench.ai" } ] keywords = ["aws", "openai", "utilities", "database", "cloud", "sdk"] classifiers = [ @@ -33,7 +32,7 @@ dependencies = [ "filetype>=1.2,<2.0", "ansi2txt>=0.2.0,<1.0.0", "ftfy>=6.3.1,<7.0.0", - "logspark[json]~=0.11.0", + "logspark[json]~=0.12.0", "python-json-logger>=4.0,<5.0", ] @@ -161,6 +160,7 @@ include = [ [tool.hatch.build.targets.wheel] packages = ["WrenchCL"] +include = ["WrenchCL/py.typed"] [tool.ruff] line-length = 100 diff --git a/tests/test_optional_imports.py b/tests/test_optional_imports.py index 62975b9..5abd86c 100644 --- a/tests/test_optional_imports.py +++ b/tests/test_optional_imports.py @@ -123,17 +123,18 @@ def test_aws_imports_available(self, clean_imports): def test_aws_import_fails_missing_boto3(self, clean_imports): """Test import fails when boto3 is missing.""" - sys.modules.pop("boto3", None) - original_import = builtins.__import__ + import importlib.util as _iutil + original_find_spec = _iutil.find_spec - def mock_import(name, *args, **kwargs): + def mock_find_spec(name, *args, **kwargs): if name == 'boto3': - raise ImportError("No module named 'boto3'") - return original_import(name, *args, **kwargs) + return None + return original_find_spec(name, *args, **kwargs) - with patch('builtins.__import__', side_effect=mock_import): + sys.modules.pop('boto3', None) + with patch('importlib.util.find_spec', side_effect=mock_find_spec): with pytest.raises(ImportError) as exc_info: - from WrenchCL.Connect import AwsClientHub # <-- added fix + from WrenchCL.Connect import AwsClientHub error_msg = str(exc_info.value) assert "AWS functionality requires additional dependencies" in error_msg @@ -143,17 +144,18 @@ def mock_import(name, *args, **kwargs): def test_aws_import_fails_missing_psycopg2(self, clean_imports): """Test import fails when psycopg2 is missing.""" - sys.modules.pop("psycopg2", None) - original_import = builtins.__import__ + import importlib.util as _iutil + original_find_spec = _iutil.find_spec - def mock_import(name, *args, **kwargs): - if name == 'psycopg2': - raise ImportError("No module named 'psycopg2'") - return original_import(name, *args, **kwargs) + def mock_find_spec(name, *args, **kwargs): + if 'psycopg2' in name: + return None + return original_find_spec(name, *args, **kwargs) - with patch('builtins.__import__', side_effect=mock_import): + sys.modules.pop('psycopg2', None) + with patch('importlib.util.find_spec', side_effect=mock_find_spec): with pytest.raises(ImportError) as exc_info: - from WrenchCL.Connect import AwsClientHub # <-- added fix + from WrenchCL.Connect import AwsClientHub error_msg = str(exc_info.value) assert "AWS functionality requires additional dependencies" in error_msg @@ -188,17 +190,18 @@ def test_successful_import_and_instantiation(self, clean_imports): @pytest.mark.parametrize("missing_module", ["boto3", "psycopg2", "paramiko", "sshtunnel"]) def test_specific_missing_modules(self, clean_imports, missing_module): - sys.modules.pop(missing_module, None) - original_import = builtins.__import__ + import importlib.util as _iutil + original_find_spec = _iutil.find_spec - def mock_import(name, *args, **kwargs): + def mock_find_spec(name, *args, **kwargs): if missing_module in name: - raise ImportError(f"No module named '{missing_module}'") - return original_import(name, *args, **kwargs) + return None + return original_find_spec(name, *args, **kwargs) - with patch('builtins.__import__', side_effect=mock_import): + sys.modules.pop(missing_module, None) + with patch('importlib.util.find_spec', side_effect=mock_find_spec): with pytest.raises(ImportError) as exc_info: - from WrenchCL.Connect import AwsClientHub # <-- added fix + from WrenchCL.Connect import AwsClientHub error_msg = str(exc_info.value) assert "pip install 'WrenchCL[aws]'" in error_msg diff --git a/uv.lock b/uv.lock index ce955be..0d02cad 100644 --- a/uv.lock +++ b/uv.lock @@ -691,11 +691,11 @@ wheels = [ [[package]] name = "logspark" -version = "0.11.0" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8f/d6/389a80a3304a649a2b91a44864a6bcd40d0bec9d48566bd8a5f63ed2e409/logspark-0.11.0.tar.gz", hash = "sha256:8b253400049e5047da14c1e65f2cef47828e15caa54da1b3eb60bf966dc1449e", size = 38157, upload-time = "2026-06-12T20:33:29.692Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/71a141480c3bf1175ead68c2b6a688eb348d55dbe409611653bcef6f6466/logspark-0.12.0.tar.gz", hash = "sha256:952beba95970a6e6849ebd112e1ba0922f959bb02f5dc8accc620dc7b8e4f459", size = 38837, upload-time = "2026-06-16T23:29:25.374Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/d9/1bf737feb13ba6339fc27f27c82f2c228a22924b25a3f52471c80020ac78/logspark-0.11.0-py3-none-any.whl", hash = "sha256:37d873d5396b0f5de3561933189b7bd881658f5ebdcd660d349610fc0a3b9131", size = 53849, upload-time = "2026-06-12T20:33:28.359Z" }, + { url = "https://files.pythonhosted.org/packages/f0/14/6c07d6072e840adddac849554464cee67ee84c62117bf007743006048348/logspark-0.12.0-py3-none-any.whl", hash = "sha256:c5e5739b6e4a838a52b55ce0ea20c80427ccc962acfc280ffcad2860d336a054", size = 55063, upload-time = "2026-06-16T23:29:24.144Z" }, ] [package.optional-dependencies] @@ -2104,7 +2104,7 @@ requires-dist = [ { name = "ddtrace", marker = "extra == 'trace'", specifier = ">=3.2.0,<3.3.0" }, { name = "filetype", specifier = ">=1.2,<2.0" }, { name = "ftfy", specifier = ">=6.3.1,<7.0.0" }, - { name = "logspark", extras = ["json"], specifier = "~=0.11.0" }, + { name = "logspark", extras = ["json"], specifier = "~=0.12.0" }, { name = "paramiko", marker = "extra == 'all'", specifier = "==3.5.1" }, { name = "paramiko", marker = "extra == 'aws'", specifier = "==3.5.1" }, { name = "psycopg2", marker = "extra == 'all'", specifier = ">=2.9,<3.0" },