diff --git a/pyproject.toml b/pyproject.toml index 291acc8..8806ca2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,12 +11,18 @@ authors = [{name = "Microsoft Corporation"}] license = "MIT" license-files = ["LICENSE"] requires-python = ">=3.10" +keywords = ["dataverse", "powerapps", "powerplatform", "crm", "dynamics", "odata"] classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", ] dependencies = [ "azure-identity>=1.17.0", @@ -27,6 +33,19 @@ dependencies = [ [project.urls] "Homepage" = "https://github.com/microsoft/PowerPlatform-DataverseClient-Python" +"Repository" = "https://github.com/microsoft/PowerPlatform-DataverseClient-Python.git" +"Issues" = "https://github.com/microsoft/PowerPlatform-DataverseClient-Python/issues" +"Documentation" = "https://github.com/microsoft/PowerPlatform-DataverseClient-Python#readme" + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-cov>=4.0.0", + "black>=23.0.0", + "isort>=5.12.0", + "mypy>=1.0.0", + "ruff>=0.1.0", +] [tool.setuptools] package-dir = {"" = "src"} @@ -36,3 +55,33 @@ zip-safe = false where = ["src"] include = ["PowerPlatform*"] namespaces = false + +[tool.setuptools.package-data] +"*" = ["py.typed"] + +# Microsoft Python Standards - Linting & Formatting +[tool.black] +line-length = 120 +target-version = ['py310'] + +[tool.isort] +profile = "black" +line_length = 120 + +[tool.mypy] +python_version = "3.10" +strict = true +warn_return_any = true +warn_unused_configs = true + +[tool.ruff] +line-length = 120 +target-version = "py310" +select = [ + "E", "W", # pycodestyle + "F", # pyflakes + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + "B", # flake8-bugbear +] diff --git a/src/PowerPlatform/Dataverse/core/error_codes.py b/src/PowerPlatform/Dataverse/core/error_codes.py index 9689e78..139132f 100644 --- a/src/PowerPlatform/Dataverse/core/error_codes.py +++ b/src/PowerPlatform/Dataverse/core/error_codes.py @@ -1,6 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +""" +Error code constants and utilities for Dataverse SDK exceptions. + +This module defines error subcodes used throughout the SDK for categorizing +different types of failures, including HTTP errors, validation errors, +SQL parsing errors, and metadata operation errors. +""" + # HTTP subcode constants HTTP_400 = "http_400" HTTP_401 = "http_401" @@ -69,7 +77,26 @@ TRANSIENT_STATUS = {429, 502, 503, 504} def http_subcode(status: int) -> str: + """ + Convert HTTP status code to error subcode string. + + :param status: HTTP status code (e.g., 400, 404, 500). + :type status: int + :return: Error subcode string (e.g., "http_400", "http_404"). + :rtype: str + """ return HTTP_STATUS_TO_SUBCODE.get(status, f"http_{status}") def is_transient_status(status: int) -> bool: + """ + Check if an HTTP status code indicates a transient error that may succeed on retry. + + Transient status codes include: 429 (Too Many Requests), 502 (Bad Gateway), + 503 (Service Unavailable), and 504 (Gateway Timeout). + + :param status: HTTP status code to check. + :type status: int + :return: True if the status code is considered transient. + :rtype: bool + """ return status in TRANSIENT_STATUS diff --git a/src/PowerPlatform/Dataverse/core/http.py b/src/PowerPlatform/Dataverse/core/http.py index 2b5924f..286a439 100644 --- a/src/PowerPlatform/Dataverse/core/http.py +++ b/src/PowerPlatform/Dataverse/core/http.py @@ -1,6 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +""" +HTTP client with automatic retry logic and timeout handling. + +This module provides :class:`HttpClient`, a wrapper around the requests library +that adds configurable retry behavior for transient network errors and +intelligent timeout management based on HTTP method types. +""" + from __future__ import annotations import time @@ -10,6 +18,20 @@ class HttpClient: + """ + HTTP client with configurable retry logic and timeout handling. + + Provides automatic retry behavior for transient failures and default timeout + management for different HTTP methods. + + :param retries: Maximum number of retry attempts for transient errors. Default is 5. + :type retries: int or None + :param backoff: Base delay in seconds between retry attempts. Default is 0.5. + :type backoff: float or None + :param timeout: Default request timeout in seconds. If None, uses per-method defaults. + :type timeout: float or None + """ + def __init__( self, *, @@ -22,8 +44,23 @@ def __init__( self.default_timeout: Optional[float] = timeout def request(self, method: str, url: str, **kwargs: Any) -> requests.Response: - # Apply per-method default timeouts if not provided - # Apply default timeout if not provided; fall back to per-method defaults + """ + Execute an HTTP request with automatic retry logic and timeout management. + + Applies default timeouts based on HTTP method (120s for POST/DELETE, 10s for others) + and retries on network errors with exponential backoff. + + :param method: HTTP method (GET, POST, PUT, DELETE, etc.). + :type method: str + :param url: Target URL for the request. + :type url: str + :param kwargs: Additional arguments passed to ``requests.request()``, including headers, data, etc. + :return: HTTP response object. + :rtype: requests.Response + :raises requests.exceptions.RequestException: If all retry attempts fail. + """ + # If no timeout is provided, use the user-specified default timeout if set; + # otherwise, apply per-method defaults (120s for POST/DELETE, 10s for others). if "timeout" not in kwargs: if self.default_timeout is not None: kwargs["timeout"] = self.default_timeout diff --git a/src/PowerPlatform/Dataverse/py.typed b/src/PowerPlatform/Dataverse/py.typed new file mode 100644 index 0000000..e69de29