diff --git a/pyproject.toml b/pyproject.toml index 01c3e6c..e101974 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -266,33 +266,46 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" # Public API functions can use boolean parameters "src/ipsdk/connection.py" = [ + "E402", # Module level import not at top of file (after module docstring) "FBT001", # Boolean-typed positional argument (part of public API) "FBT002", # Boolean default positional argument (part of public API) "TRY301", # Abstract raise to inner function (acceptable for error handling) ] "src/ipsdk/platform.py" = [ + "E402", # Module level import not at top of file (after module docstring) "FBT001", # Boolean-typed positional argument (part of public API) "FBT002", # Boolean default positional argument (part of public API) "TRY301", # Abstract raise to inner function (acceptable for error handling) ] "src/ipsdk/gateway.py" = [ + "E402", # Module level import not at top of file (after module docstring) "FBT001", # Boolean-typed positional argument (part of public API) "FBT002", # Boolean default positional argument (part of public API) ] "src/ipsdk/exceptions.py" = [ + "E402", # Module level import not at top of file (after module docstring) "S110", # try-except-pass (intentional for error handling) ] "src/ipsdk/logging.py" = [ + "E402", # Module level import not at top of file (after module docstring) "FBT001", # Boolean-typed positional argument (part of public API) "FBT002", # Boolean default positional argument (part of public API) "G004", # Logging statement uses f-string (acceptable for informational logging) "E501", # Line too long (docstrings can be longer) ] +"src/ipsdk/heuristics.py" = [ + "E402", # Module level import not at top of file (after module docstring) +] + +"src/ipsdk/http.py" = [ + "E402", # Module level import not at top of file (after module docstring) +] + [tool.ruff.lint.isort] known-first-party = ["ipsdk"] force-single-line = true diff --git a/src/ipsdk/connection.py b/src/ipsdk/connection.py index 4ef703d..a7bd017 100644 --- a/src/ipsdk/connection.py +++ b/src/ipsdk/connection.py @@ -1,6 +1,8 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import annotations + """HTTP connection implementations for the Itential Python SDK. This module provides both synchronous and asynchronous HTTP client implementations @@ -130,9 +132,6 @@ async def fetch_devices(): import urllib.parse from typing import Any -from typing import Dict -from typing import Optional -from typing import Union import httpx @@ -144,7 +143,7 @@ async def fetch_devices(): class ConnectionBase: - client: Union[httpx.Client, httpx.AsyncClient] + client: httpx.Client | httpx.AsyncClient def __init__( self, @@ -196,7 +195,7 @@ def __init__( self.token = None self.authenticated = False - self._auth_lock: Optional[Any] = None + self._auth_lock: Any | None = None self.client = self.__init_client__( base_url=self._make_base_url(host, port, base_path, use_tls), @@ -247,8 +246,8 @@ def _build_request( self, method: HTTPMethod, path: str, - json: Union[str, bytes, dict, list] | None = None, - params: dict[str, Any] | None = None, + json: str | bytes | dict | list | None = None, + params: dict[str, Any | None] | None = None, ) -> httpx.Request: """Build an HTTP request object. @@ -307,8 +306,8 @@ def _validate_request_args( self, method: HTTPMethod, path: str, - params: Optional[Dict[str, Any]] = None, - json: Optional[Union[str, bytes, dict, list]] = None, + params: dict[str, Any | None] | None = None, + json: str | bytes | dict | (list | None) = None, ) -> None: """ Validate request arguments to ensure they have correct types. @@ -321,8 +320,8 @@ def _validate_request_args( Args: method (HTTPMethod): The HTTP method enum value to validate path (str): The request path to validate - params (Optional[Dict[str, Any]]): Query parameters dict to validate - json (Optional[Union[str, bytes, dict, list]]): JSON body to validate + params (dict[str, Any | None]): Query parameters dict to validate + json (Union[str, bytes, dict, list | None]): JSON body to validate Returns: None @@ -353,7 +352,7 @@ def _validate_request_args( @abc.abstractmethod def __init_client__( self, base_url: str | None = None, verify: bool = True, timeout: int = 30 - ) -> Union[httpx.Client, httpx.AsyncClient]: + ) -> httpx.Client | httpx.AsyncClient: """Initialize the HTTP client. Abstract method to be implemented by subclasses to create either a @@ -365,7 +364,7 @@ def __init_client__( timeout: Connection timeout in seconds. Defaults to 30. Returns: - Union[httpx.Client, httpx.AsyncClient]: The initialized HTTP client. + httpx.Client | httpx.AsyncClient: The initialized HTTP client. Raises: None @@ -416,8 +415,8 @@ def _send_request( self, method: HTTPMethod, path: str, - params: dict[str, Any] | None = None, - json: Union[str, bytes, dict, list] | None = None, + params: dict[str, Any | None] | None = None, + json: str | bytes | dict | list | None = None, ) -> Response: """Send an HTTP request to the API endpoint. @@ -473,7 +472,7 @@ def _send_request( return Response(res) - def get(self, path: str, params: dict[str, Any] | None = None) -> Response: + def get(self, path: str, params: dict[str, Any | None] | None = None) -> Response: """Send an HTTP GET request to the server. Args: @@ -490,7 +489,9 @@ def get(self, path: str, params: dict[str, Any] | None = None) -> Response: logging.trace(self.get, modname=__name__, clsname=self.__class__) return self._send_request(HTTPMethod.GET, path=path, params=params) - def delete(self, path: str, params: dict[str, Any] | None = None) -> Response: + def delete( + self, path: str, params: dict[str, Any | None] | None = None + ) -> Response: """Send an HTTP DELETE request to the server. Args: @@ -510,8 +511,8 @@ def delete(self, path: str, params: dict[str, Any] | None = None) -> Response: def post( self, path: str, - params: dict[str, Any] | None = None, - json: Union[str, bytes, list, dict] | None = None, + params: dict[str, Any | None] | None = None, + json: str | bytes | list | dict | None = None, ) -> Response: """Send an HTTP POST request to the server. @@ -534,8 +535,8 @@ def post( def put( self, path: str, - params: dict[str, Any] | None = None, - json: Union[str, bytes, list, dict] | None = None, + params: dict[str, Any | None] | None = None, + json: str | bytes | list | dict | None = None, ) -> Response: """Send an HTTP PUT request to the server. @@ -558,8 +559,8 @@ def put( def patch( self, path: str, - params: dict[str, Any] | None = None, - json: Union[str, bytes, list, dict] | None = None, + params: dict[str, Any | None] | None = None, + json: str | bytes | list | dict | None = None, ) -> Response: """Send an HTTP PATCH request to the server. @@ -588,7 +589,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self._auth_lock = asyncio.Lock() def __init_client__( - self, base_url: Optional[str] = None, verify: bool = True, timeout: int = 30 + self, base_url: str | None = None, verify: bool = True, timeout: int = 30 ) -> httpx.AsyncClient: """ Initialize the httpx.AsyncClient instance @@ -625,8 +626,8 @@ async def _send_request( self, method: HTTPMethod, path: str, - params: dict[str, Any] | None = None, - json: Union[str, bytes, dict, list] | None = None, + params: dict[str, Any | None] | None = None, + json: str | bytes | dict | list | None = None, ) -> Response: """Send an asynchronous HTTP request to the API endpoint. @@ -682,7 +683,9 @@ async def _send_request( return Response(res) - async def get(self, path: str, params: dict[str, Any] | None = None) -> Response: + async def get( + self, path: str, params: dict[str, Any | None] | None = None + ) -> Response: """ Send a HTTP GET request to the server and return the response. @@ -705,7 +708,7 @@ async def get(self, path: str, params: dict[str, Any] | None = None) -> Response return await self._send_request(HTTPMethod.GET, path=path, params=params) async def delete( - self, path: str, params: dict[str, Any] | None = None + self, path: str, params: dict[str, Any | None] | None = None ) -> Response: """ Send a HTTP DELETE request to the server and return the response. @@ -731,8 +734,8 @@ async def delete( async def post( self, path: str, - params: dict[str, Any] | None = None, - json: Union[str, bytes, dict, list] | None = None, + params: dict[str, Any | None] | None = None, + json: str | bytes | dict | list | None = None, ) -> Response: """ Send a HTTP POST request to the server and return the response. @@ -766,8 +769,8 @@ async def post( async def put( self, path: str, - params: dict[str, Any] | None = None, - json: Union[str, bytes, dict, list] | None = None, + params: dict[str, Any | None] | None = None, + json: str | bytes | dict | list | None = None, ) -> Response: """ Send a HTTP PUT request to the server and return the response. @@ -801,8 +804,8 @@ async def put( async def patch( self, path: str, - params: dict[str, Any] | None = None, - json: Union[str, bytes, dict, list] | None = None, + params: dict[str, Any | None] | None = None, + json: str | bytes | dict | list | None = None, ) -> Response: """ Send a HTTP PATCH request to the server and return the response. diff --git a/src/ipsdk/exceptions.py b/src/ipsdk/exceptions.py index f11b65e..4fae85d 100644 --- a/src/ipsdk/exceptions.py +++ b/src/ipsdk/exceptions.py @@ -1,6 +1,8 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import annotations + """ Exception hierarchy for the Itential Python SDK. @@ -73,13 +75,14 @@ print(f"Response body: {e.response.text}") """ +from typing import TYPE_CHECKING from typing import Any -from typing import Optional - -import httpx from . import logging +if TYPE_CHECKING: + import httpx + class IpsdkError(Exception): """ @@ -93,7 +96,7 @@ class IpsdkError(Exception): details (dict): Additional error details and context """ - def __init__(self, message: str, exc: Optional[httpx.HTTPError] = None) -> None: + def __init__(self, message: str, exc: httpx.HTTPError | None = None) -> None: """ Initialize the base SDK exception. diff --git a/src/ipsdk/gateway.py b/src/ipsdk/gateway.py index ad731de..b90cf75 100644 --- a/src/ipsdk/gateway.py +++ b/src/ipsdk/gateway.py @@ -1,6 +1,8 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import annotations + """Itential Automation Gateway client implementation for the SDK. This module provides client implementations for connecting to and interacting @@ -149,7 +151,6 @@ async def get_devices(): """ from typing import Any -from typing import Optional import httpx @@ -206,8 +207,8 @@ class AuthMixin: """ # Attributes that should be provided by ConnectionBase - user: Optional[str] - password: Optional[str] + user: str | None + password: str | None client: httpx.Client def authenticate(self) -> None: @@ -242,8 +243,8 @@ class AsyncAuthMixin: """ # Attributes that should be provided by ConnectionBase - user: Optional[str] - password: Optional[str] + user: str | None + password: str | None client: httpx.AsyncClient async def authenticate(self) -> None: diff --git a/src/ipsdk/heuristics.py b/src/ipsdk/heuristics.py index f61967e..7c3d9e2 100644 --- a/src/ipsdk/heuristics.py +++ b/src/ipsdk/heuristics.py @@ -1,6 +1,8 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import annotations + """Heuristics scanner for filtering sensitive data from log messages. This module provides functionality to detect and redact sensitive information @@ -11,9 +13,6 @@ import re from typing import Callable -from typing import Dict -from typing import List -from typing import Optional from typing import Pattern @@ -32,14 +31,14 @@ class Scanner: redacted = scanner.scan_and_redact("API_KEY=secret123456789") """ - _instance: Optional["Scanner"] = None + _instance: Scanner | None = None _initialized: bool = False - def __new__(cls, _custom_patterns: Optional[Dict[str, str]] = None) -> "Scanner": + def __new__(cls, _custom_patterns: dict[str, str | None] | None = None) -> Scanner: """Create or return the singleton instance. Args: - _custom_patterns (Optional[Dict[str, str]]): Additional patterns + _custom_patterns (dict[str, str | None]): Additional patterns to scan for, where keys are pattern names and values are regex patterns. Passed to __init__ after instance creation. @@ -53,14 +52,14 @@ def __new__(cls, _custom_patterns: Optional[Dict[str, str]] = None) -> "Scanner" cls._instance = super().__new__(cls) return cls._instance - def __init__(self, custom_patterns: Optional[Dict[str, str]] = None) -> None: + def __init__(self, custom_patterns: dict[str, str | None] | None = None) -> None: """Initialize the sensitive data scanner. This method will only initialize the instance once due to the Singleton pattern. Subsequent calls will not re-initialize the patterns. Args: - custom_patterns (Optional[Dict[str, str]]): Additional patterns to scan for, + custom_patterns (dict[str, str | None]): Additional patterns to scan for, where keys are pattern names and values are regex patterns. Only applied on first initialization. @@ -71,9 +70,9 @@ def __init__(self, custom_patterns: Optional[Dict[str, str]] = None) -> None: re.error: If any of the regex patterns are invalid. """ # Only initialize once due to Singleton pattern - if self._initialized is False: - self._patterns: Dict[str, Pattern] = {} - self._redaction_functions: Dict[str, Callable[[str], str]] = {} + if not self._initialized: + self._patterns: dict[str, Pattern] = {} + self._redaction_functions: dict[str, Callable[[str], str]] = {} # Initialize default patterns self._init_default_patterns() @@ -149,14 +148,14 @@ def add_pattern( self, name: str, pattern: str, - redaction_func: Optional[Callable[[str], str]] = None, + redaction_func: Callable[[str | None, str]] | None = None, ) -> None: """Add a new sensitive data pattern to scan for. Args: name (str): Name of the pattern for identification. pattern (str): Regular expression pattern to match sensitive data. - redaction_func (Optional[Callable[[str], str]]): Custom function + redaction_func (Callable[[str | None, str]]): Custom function to redact matches. If None, uses default redaction with pattern name. @@ -196,11 +195,11 @@ def remove_pattern(self, name: str) -> bool: return True return False - def list_patterns(self) -> List[str]: + def list_patterns(self) -> list[str]: """Get a list of all pattern names currently registered. Returns: - List[str]: List of pattern names. + list[str]: List of pattern names. Raises: None @@ -250,14 +249,14 @@ def has_sensitive_data(self, text: str) -> bool: return any(pattern.search(text) for pattern in self._patterns.values()) - def get_sensitive_data_types(self, text: str) -> List[str]: + def get_sensitive_data_types(self, text: str) -> list[str]: """Get a list of sensitive data types detected in the text. Args: text (str): The text to analyze. Returns: - List[str]: List of pattern names that matched in the text. + list[str]: List of pattern names that matched in the text. Raises: None @@ -305,7 +304,7 @@ def get_scanner() -> Scanner: def configure_scanner( - custom_patterns: Optional[Dict[str, str]] = None, + custom_patterns: dict[str, str | None] | None = None, ) -> Scanner: """Configure the global scanner with custom patterns. @@ -314,7 +313,7 @@ def configure_scanner( scanner, use reset_singleton() first. Args: - custom_patterns (Optional[Dict[str, str]]): Custom patterns to add + custom_patterns (dict[str, str | None]): Custom patterns to add to the scanner. Returns: diff --git a/src/ipsdk/http.py b/src/ipsdk/http.py index 9ce0172..b0a86d7 100644 --- a/src/ipsdk/http.py +++ b/src/ipsdk/http.py @@ -1,6 +1,8 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import annotations + """HTTP utilities and enumerations for the Itential Python SDK. This module provides HTTP-related enumerations with compatibility across @@ -9,15 +11,14 @@ """ from http import HTTPStatus +from typing import TYPE_CHECKING from typing import Any -from typing import Dict -from typing import Optional -from typing import Union - -import httpx from . import logging +if TYPE_CHECKING: + import httpx + # Import HTTPMethod from standard library (Python 3.11+) or define fallback try: from http import HTTPMethod @@ -66,8 +67,8 @@ class Request: Args: method (str): The HTTP method (GET, POST, PUT, DELETE, PATCH) path (str): The URL path for the request - params (Dict[str, Any], optional): Query parameters for the request - headers (Dict[str, str], optional): HTTP headers for the request + params (dict[str, Any], optional): Query parameters for the request + headers (dict[str, str], optional): HTTP headers for the request json (Union[str, bytes, dict, list], optional): JSON data for the request body Raises: @@ -78,9 +79,9 @@ def __init__( self, method: str, path: str, - params: Optional[Dict[str, Any]] = None, - headers: Optional[Dict[str, str]] = None, - json: Optional[Union[str, bytes, dict, list]] = None, + params: dict[str, Any | None] | None = None, + headers: dict[str, str | None] | None = None, + json: str | bytes | dict | (list | None) = None, ) -> None: logging.trace(self.__init__, modname=__name__, clsname=self.__class__) self.method = method @@ -195,12 +196,12 @@ def request(self) -> httpx.Request: """ return self._response.request - def json(self) -> Dict[str, Any]: + def json(self) -> dict[str, Any]: """ Parse the response content as JSON Returns: - Dict[str, Any]: The parsed JSON response + dict[str, Any]: The parsed JSON response Raises: ValueError: If the response content is not valid JSON diff --git a/src/ipsdk/logging.py b/src/ipsdk/logging.py index 0d0cd70..9f6b474 100644 --- a/src/ipsdk/logging.py +++ b/src/ipsdk/logging.py @@ -1,6 +1,8 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import annotations + r"""Comprehensive logging system for the Itential Python SDK. This module provides a full-featured logging implementation with support for @@ -103,9 +105,6 @@ def process_data(data): from functools import cache from functools import partial from typing import Callable -from typing import Dict -from typing import List -from typing import Optional from . import heuristics from . import metadata @@ -178,8 +177,8 @@ def log(lvl: int, msg: str) -> None: def trace( f: Callable, - modname: Optional[str] = None, - clsname: Optional[str] = None + modname: str | None = None, + clsname: str | None = None ) -> None: """Log a trace message for function invocation. @@ -381,7 +380,7 @@ def is_sensitive_data_filtering_enabled() -> bool: def configure_sensitive_data_patterns( - custom_patterns: Optional[Dict[str, str]] = None, + custom_patterns: dict[str, str | None] | None = None, ) -> None: """Configure custom patterns for sensitive data detection. @@ -390,7 +389,7 @@ def configure_sensitive_data_patterns( match sensitive data that needs to be protected. Args: - custom_patterns (Optional[Dict[str, str]]): Dictionary of custom regex + custom_patterns (dict[str, str | None]): Dictionary of custom regex patterns to add to the sensitive data scanner. Keys are pattern names (for identification) and values are regex patterns to match sensitive data. If None, no patterns are added @@ -404,7 +403,7 @@ def configure_sensitive_data_patterns( heuristics.configure_scanner(custom_patterns) -def get_sensitive_data_patterns() -> List[str]: +def get_sensitive_data_patterns() -> list[str]: """Get a list of all sensitive data patterns currently configured. Returns the names of all patterns currently registered with the sensitive @@ -414,7 +413,7 @@ def get_sensitive_data_patterns() -> List[str]: None Returns: - List[str]: List of pattern names that are being scanned for + list[str]: List of pattern names that are being scanned for Raises: None diff --git a/src/ipsdk/platform.py b/src/ipsdk/platform.py index 678d258..318f6c6 100644 --- a/src/ipsdk/platform.py +++ b/src/ipsdk/platform.py @@ -1,6 +1,8 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import annotations + """Itential Platform client implementation for the SDK. This module provides client implementations for connecting to and interacting @@ -219,7 +221,6 @@ async def create_workflow(name): """ from typing import Any -from typing import Optional import httpx @@ -349,12 +350,12 @@ class AuthMixin: """ # Attributes that should be provided by ConnectionBase - user: Optional[str] - password: Optional[str] - client_id: Optional[str] - client_secret: Optional[str] + user: str | None + password: str | None + client_id: str | None + client_secret: str | None client: httpx.Client - token: Optional[str] + token: str | None def authenticate(self) -> None: """ @@ -443,12 +444,12 @@ class AsyncAuthMixin: """ # Attributes that should be provided by ConnectionBase - user: Optional[str] - password: Optional[str] - client_id: Optional[str] - client_secret: Optional[str] + user: str | None + password: str | None + client_id: str | None + client_secret: str | None client: httpx.AsyncClient - token: Optional[str] + token: str | None async def authenticate(self) -> None: """ @@ -542,8 +543,8 @@ def platform_factory( verify: bool = True, user: str = "admin", password: str = "admin", - client_id: Optional[str] = None, - client_secret: Optional[str] = None, + client_id: str | None = None, + client_secret: str | None = None, timeout: int = 30, want_async: bool = False, ) -> Any: