From b30b4fbb4b4f310531a1ade4fed4ee7445e1005c Mon Sep 17 00:00:00 2001 From: Suyash Kshirsagar Date: Tue, 11 Nov 2025 21:28:27 -0800 Subject: [PATCH 1/7] Add Microsoft Python standards compliance - Add py.typed markers for both packages (enables type checking) - Enhance pyproject.toml with complete metadata and dev dependencies - Add missing docstrings for HttpClient, error_codes functions - Add module docstrings for better discoverability - Configure linting tools (black, isort, mypy, ruff) per Microsoft standards --- pyproject.toml | 56 ++++++++++++++++++- .../Dataverse/core/error_codes.py | 27 +++++++++ src/PowerPlatform/Dataverse/core/http.py | 37 ++++++++++++ src/PowerPlatform/Dataverse/py.typed | 0 src/dataverse_sdk/py.typed | 0 5 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/PowerPlatform/Dataverse/py.typed create mode 100644 src/dataverse_sdk/py.typed diff --git a/pyproject.toml b/pyproject.toml index 291acc8..430c00c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,15 +8,22 @@ version = "0.1.0b1" description = "Python SDK for Microsoft Dataverse" readme = {file = "README.md", content-type = "text/markdown"} authors = [{name = "Microsoft Corporation"}] -license = "MIT" +license = {text = "MIT"} license-files = ["LICENSE"] requires-python = ">=3.10" +keywords = ["dataverse", "powerapps", "powerplatform", "crm", "dynamics", "odata"] classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "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 +34,23 @@ 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" + +[tool.setuptools] +package-dir = {"" = "src"} +zip-safe = false + +[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 +60,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..b6d4fdc 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,6 +44,21 @@ def __init__( self.default_timeout: Optional[float] = timeout def request(self, method: str, url: str, **kwargs: Any) -> requests.Response: + """ + 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. + """ # Apply per-method default timeouts if not provided # Apply default timeout if not provided; fall back to per-method defaults if "timeout" not in kwargs: diff --git a/src/PowerPlatform/Dataverse/py.typed b/src/PowerPlatform/Dataverse/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/dataverse_sdk/py.typed b/src/dataverse_sdk/py.typed new file mode 100644 index 0000000..e69de29 From be68499a4b222d5afae0f2243e864b904e61706e Mon Sep 17 00:00:00 2001 From: Suyash Kshirsagar Date: Tue, 11 Nov 2025 21:33:37 -0800 Subject: [PATCH 2/7] Clean up package structure - Remove old dataverse_sdk folder (obsolete structure) - Fix duplicate [tool.setuptools] section in pyproject.toml - Keep only PowerPlatform.Dataverse as the correct package structure --- pyproject.toml | 4 ---- src/dataverse_sdk/py.typed | 0 2 files changed, 4 deletions(-) delete mode 100644 src/dataverse_sdk/py.typed diff --git a/pyproject.toml b/pyproject.toml index 430c00c..7541e0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,10 +38,6 @@ dependencies = [ "Issues" = "https://github.com/microsoft/PowerPlatform-DataverseClient-Python/issues" "Documentation" = "https://github.com/microsoft/PowerPlatform-DataverseClient-Python#readme" -[tool.setuptools] -package-dir = {"" = "src"} -zip-safe = false - [project.optional-dependencies] dev = [ "pytest>=7.0.0", diff --git a/src/dataverse_sdk/py.typed b/src/dataverse_sdk/py.typed deleted file mode 100644 index e69de29..0000000 From cb4590aa77ac73045284119a98cfe5f548ac1dbf Mon Sep 17 00:00:00 2001 From: suyask-msft <158708948+suyask-msft@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:35:19 -0800 Subject: [PATCH 3/7] Update src/PowerPlatform/Dataverse/core/http.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/PowerPlatform/Dataverse/core/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerPlatform/Dataverse/core/http.py b/src/PowerPlatform/Dataverse/core/http.py index b6d4fdc..47a2711 100644 --- a/src/PowerPlatform/Dataverse/core/http.py +++ b/src/PowerPlatform/Dataverse/core/http.py @@ -27,7 +27,7 @@ class HttpClient: :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 + :type backoff: float or None :param timeout: Default request timeout in seconds. If None, uses per-method defaults. :type timeout: float or None """ From 534e802d50390aa825235ae3f06862357fce0025 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 05:35:40 +0000 Subject: [PATCH 4/7] Initial plan From 57b5fe0ecbf2f632d25afd55e3358615ef37e4a9 Mon Sep 17 00:00:00 2001 From: suyask-msft <158708948+suyask-msft@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:35:50 -0800 Subject: [PATCH 5/7] Update src/PowerPlatform/Dataverse/core/http.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/PowerPlatform/Dataverse/core/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PowerPlatform/Dataverse/core/http.py b/src/PowerPlatform/Dataverse/core/http.py index 47a2711..286a439 100644 --- a/src/PowerPlatform/Dataverse/core/http.py +++ b/src/PowerPlatform/Dataverse/core/http.py @@ -59,8 +59,8 @@ def request(self, method: str, url: str, **kwargs: Any) -> requests.Response: :rtype: requests.Response :raises requests.exceptions.RequestException: If all retry attempts fail. """ - # Apply per-method default timeouts if not provided - # Apply default timeout if not provided; fall back to per-method defaults + # 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 From dd2896a0b0369b1de23d71f704257b132074aadf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 05:39:28 +0000 Subject: [PATCH 6/7] Remove trailing whitespace from pyproject.toml Co-authored-by: suyask-msft <158708948+suyask-msft@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7541e0f..7d04d66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ line-length = 120 target-version = "py310" select = [ "E", "W", # pycodestyle - "F", # pyflakes + "F", # pyflakes "I", # isort "N", # pep8-naming "UP", # pyupgrade From bf1cbe5662dd7bfbdd8c50a1df8e76de60a84dc3 Mon Sep 17 00:00:00 2001 From: Suyash Kshirsagar Date: Tue, 11 Nov 2025 21:42:48 -0800 Subject: [PATCH 7/7] Fix pyproject.toml license configuration for modern setuptools - Change license from {text: 'MIT'} to 'MIT' (newer setuptools format) - Remove license classifier to avoid conflicts with license field - Verified package builds successfully with py.typed included --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7541e0f..396f4dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,14 +8,13 @@ version = "0.1.0b1" description = "Python SDK for Microsoft Dataverse" readme = {file = "README.md", content-type = "text/markdown"} authors = [{name = "Microsoft Corporation"}] -license = {text = "MIT"} +license = "MIT" license-files = ["LICENSE"] requires-python = ">=3.10" keywords = ["dataverse", "powerapps", "powerplatform", "crm", "dynamics", "odata"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11",