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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ from azure.identity import (
ClientCertificateCredential,
AzureCliCredential
)
from PowerPlatform.Dataverse import DataverseClient
from PowerPlatform.Dataverse.client import DataverseClient

# Development options
credential = InteractiveBrowserCredential() # Browser authentication
Expand Down Expand Up @@ -111,7 +111,7 @@ The SDK provides a simple, pythonic interface for Dataverse operations:

```python
from azure.identity import InteractiveBrowserCredential
from PowerPlatform.Dataverse import DataverseClient
from PowerPlatform.Dataverse.client import DataverseClient

# Connect to Dataverse
credential = InteractiveBrowserCredential()
Expand Down Expand Up @@ -285,7 +285,7 @@ For comprehensive information on Microsoft Dataverse and related technologies:
The client raises structured exceptions for different error scenarios:

```python
from PowerPlatform.Dataverse import DataverseClient
from PowerPlatform.Dataverse.client import DataverseClient
from PowerPlatform.Dataverse.core.errors import HttpError, ValidationError

try:
Expand Down
2 changes: 1 addition & 1 deletion examples/advanced/file_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
# Uncomment for local development from source
# sys.path.append(str(Path(__file__).resolve().parents[2] / "src"))

from PowerPlatform.Dataverse import DataverseClient
from PowerPlatform.Dataverse.client import DataverseClient
from azure.identity import InteractiveBrowserCredential # type: ignore
import requests

Expand Down
2 changes: 1 addition & 1 deletion examples/basic/functional_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from datetime import datetime

# Import SDK components (assumes installation is already validated)
from PowerPlatform.Dataverse import DataverseClient
from PowerPlatform.Dataverse.client import DataverseClient
from PowerPlatform.Dataverse.core.errors import HttpError, MetadataError
from azure.identity import InteractiveBrowserCredential

Expand Down
17 changes: 9 additions & 8 deletions examples/basic/installation_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,13 @@ def validate_imports():
print("-" * 50)

try:
# Test main namespace import
from PowerPlatform.Dataverse import DataverseClient, __version__
# Test main namespace and client import
from PowerPlatform.Dataverse import __version__
from PowerPlatform.Dataverse.client import DataverseClient

print(f" ✅ Main namespace: PowerPlatform.Dataverse")
print(f" ✅ Namespace: PowerPlatform.Dataverse")
print(f" ✅ Package version: {__version__}")
print(f" ✅ DataverseClient class: {DataverseClient}")
print(f" ✅ Client class: PowerPlatform.Dataverse.client.DataverseClient")

# Test submodule imports
from PowerPlatform.Dataverse.core.errors import HttpError, MetadataError
Expand All @@ -83,9 +84,9 @@ def validate_imports():

print(f" ✅ Core config: DataverseConfig")

from PowerPlatform.Dataverse.data.odata import ODataClient
from PowerPlatform.Dataverse.data._odata import _ODataClient

print(f" ✅ Data layer: ODataClient")
print(f" ✅ Data layer: _ODataClient")

# Test Azure Identity import
from azure.identity import InteractiveBrowserCredential
Expand Down Expand Up @@ -176,7 +177,7 @@ def show_usage_examples():
"""
🔧 Basic Setup:
```python
from PowerPlatform.Dataverse import DataverseClient
from PowerPlatform.Dataverse.client import DataverseClient
from azure.identity import InteractiveBrowserCredential

# Set up authentication
Expand Down Expand Up @@ -271,7 +272,7 @@ def interactive_test():
return

try:
from PowerPlatform.Dataverse import DataverseClient
from PowerPlatform.Dataverse.client import DataverseClient
from azure.identity import InteractiveBrowserCredential

print(" 🔐 Setting up authentication...")
Expand Down
3 changes: 1 addition & 2 deletions src/PowerPlatform/Dataverse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@
# Licensed under the MIT license.

from .__version__ import __version__
from .client import DataverseClient

__all__ = ["DataverseClient", "__version__"]
__all__ = ["__version__"]
112 changes: 56 additions & 56 deletions src/PowerPlatform/Dataverse/client.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from __future__ import annotations

"""
Authentication helpers for Dataverse.

This module provides :class:`~PowerPlatform.Dataverse.core.auth.AuthManager`, a thin wrapper over any Azure Identity
``TokenCredential`` for acquiring OAuth2 access tokens, and :class:`~PowerPlatform.Dataverse.core.auth.TokenPair` for
This module provides :class:`~PowerPlatform.Dataverse.core._auth._AuthManager`, a thin wrapper over any Azure Identity
``TokenCredential`` for acquiring OAuth2 access tokens, and :class:`~PowerPlatform.Dataverse.core._auth._TokenPair` for
storing the acquired token alongside its scope.
"""

from __future__ import annotations

from dataclasses import dataclass

from azure.core.credentials import TokenCredential


@dataclass
class TokenPair:
class _TokenPair:
"""
Container for an OAuth2 access token and its associated resource scope.

:param resource: The OAuth2 scope/resource for which the token was acquired.
:type resource: ``str``
:type resource: :class:`str`
:param access_token: The access token string.
:type access_token: ``str``
:type access_token: :class:`str`
"""

resource: str
access_token: str


class AuthManager:
class _AuthManager:
"""
Azure Identity-based authentication manager for Dataverse.

Expand All @@ -45,15 +45,15 @@ def __init__(self, credential: TokenCredential) -> None:
raise TypeError("credential must implement azure.core.credentials.TokenCredential.")
self.credential: TokenCredential = credential

def acquire_token(self, scope: str) -> TokenPair:
def _acquire_token(self, scope: str) -> _TokenPair:
"""
Acquire an access token for the specified OAuth2 scope.

:param scope: OAuth2 scope string, typically ``"https://<org>.crm.dynamics.com/.default"``.
:type scope: ``str``
:type scope: :class:`str`
:return: Token pair containing the scope and access token.
:rtype: ~PowerPlatform.Dataverse.core.auth.TokenPair
:rtype: ~PowerPlatform.Dataverse.core._auth._TokenPair
:raises ~azure.core.exceptions.ClientAuthenticationError: If token acquisition fails.
"""
token = self.credential.get_token(scope)
return TokenPair(resource=scope, access_token=token.token)
return _TokenPair(resource=scope, access_token=token.token)
Original file line number Diff line number Diff line change
Expand Up @@ -77,28 +77,28 @@
TRANSIENT_STATUS = {429, 502, 503, 504}


def http_subcode(status: int) -> str:
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``
:type status: :class:`int`
:return: Error subcode string (e.g., "http_400", "http_404").
:rtype: ``str``
:rtype: :class:`str`
"""
return HTTP_STATUS_TO_SUBCODE.get(status, f"http_{status}")


def is_transient_status(status: int) -> bool:
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``
:type status: :class:`int`
:return: True if the status code is considered transient.
:rtype: ``bool``
:rtype: :class:`bool`
"""
return status in TRANSIENT_STATUS
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""
HTTP client with automatic retry logic and timeout handling.

This module provides :class:`~PowerPlatform.Dataverse.core.http.HttpClient`, a wrapper
This module provides :class:`~PowerPlatform.Dataverse.core._http._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.
"""
Expand All @@ -17,19 +17,19 @@
import requests


class HttpClient:
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`` | ``None``
:type retries: :class:`int` | None
:param backoff: Base delay in seconds between retry attempts. Default is 0.5.
:type backoff: ``float`` | ``None``
:type backoff: :class:`float` | None
:param timeout: Default request timeout in seconds. If None, uses per-method defaults.
:type timeout: ``float`` | ``None``
:type timeout: :class:`float` | None
"""

def __init__(
Expand All @@ -42,20 +42,20 @@ def __init__(
self.base_delay = backoff if backoff is not None else 0.5
self.default_timeout: Optional[float] = timeout

def request(self, method: str, url: str, **kwargs: Any) -> requests.Response:
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``
:type method: :class:`str`
:param url: Target URL for the request.
:type url: ``str``
:type url: :class:`str`
:param kwargs: Additional arguments passed to ``requests.request()``, including headers, data, etc.
:return: HTTP response object.
:rtype: ``requests.Response``
:rtype: :class:`requests.Response`
:raises requests.exceptions.RequestException: If all retry attempts fail.
"""
# If no timeout is provided, use the user-specified default timeout if set;
Expand Down
12 changes: 6 additions & 6 deletions src/PowerPlatform/Dataverse/core/config.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from __future__ import annotations

"""
Dataverse client configuration.

Expand All @@ -11,6 +9,8 @@
convenience constructor :meth:`~PowerPlatform.Dataverse.core.config.DataverseConfig.from_env`.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

Expand All @@ -21,13 +21,13 @@ class DataverseConfig:
Configuration settings for Dataverse client operations.

:param language_code: LCID (Locale ID) for localized labels and messages. Default is 1033 (English - United States).
:type language_code: ``int``
:type language_code: :class:`int`
:param http_retries: Optional maximum number of retry attempts for transient HTTP errors. Reserved for future use.
:type http_retries: ``int`` | ``None``
:type http_retries: :class:`int` or None
:param http_backoff: Optional backoff multiplier (in seconds) between retry attempts. Reserved for future use.
:type http_backoff: ``float`` | ``None``
:type http_backoff: :class:`float` or None
:param http_timeout: Optional request timeout in seconds. Reserved for future use.
:type http_timeout: ``float`` | ``None``
:type http_timeout: :class:`float` or None
"""

language_code: int = 1033
Expand Down
Loading
Loading