Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"}
Expand All @@ -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
]
27 changes: 27 additions & 0 deletions src/PowerPlatform/Dataverse/core/error_codes.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
41 changes: 39 additions & 2 deletions src/PowerPlatform/Dataverse/core/http.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
*,
Expand All @@ -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
Expand Down
Empty file.
Loading