diff --git a/src/PowerPlatform/Dataverse/__init__.py b/src/PowerPlatform/Dataverse/__init__.py index 1a5dca2..d5684cc 100644 --- a/src/PowerPlatform/Dataverse/__init__.py +++ b/src/PowerPlatform/Dataverse/__init__.py @@ -1,48 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -""" -Microsoft Dataverse SDK for Python. - -This package provides a high-level Python client for interacting with Microsoft Dataverse -environments through the Web API. It supports CRUD operations, SQL queries, table metadata -management, and file uploads with Azure Identity authentication. - -Key Features: - - OData CRUD operations (create, read, update, delete) - - SQL query support via Web API - - Table metadata operations (create, inspect, delete custom tables) - - File column upload capabilities - - Pandas integration for DataFrame-based operations - - Azure Identity credential support - -.. note:: - This SDK requires Azure Identity credentials for authentication. See the - `Azure Identity documentation `_ - for supported credential types. - -Example: - Basic client initialization and usage:: - - from azure.identity import InteractiveBrowserCredential - from PowerPlatform.Dataverse import DataverseClient - - credential = InteractiveBrowserCredential() - client = DataverseClient( - "https://org.crm.dynamics.com", - credential - ) - - # Create a record - account_id = client.create("account", {"name": "Contoso"})[0] - - # Query records - accounts = client.get("account", filter="name eq 'Contoso'") - for batch in accounts: - for record in batch: - print(record["name"]) -""" - from .__version__ import __version__ from .client import DataverseClient diff --git a/src/PowerPlatform/Dataverse/client.py b/src/PowerPlatform/Dataverse/client.py index 9a18fc8..9307077 100644 --- a/src/PowerPlatform/Dataverse/client.py +++ b/src/PowerPlatform/Dataverse/client.py @@ -23,12 +23,12 @@ class DataverseClient: Key capabilities: - OData CRUD operations: create, read, update, delete records - SQL queries: execute read-only SQL via Web API ``?sql`` parameter - - Table metadata: create, inspect, and delete custom tables + - Table metadata: create, inspect, and delete custom tables; create and delete columns - File uploads: upload files to file columns with chunking support :param base_url: Your Dataverse environment URL, for example ``"https://org.crm.dynamics.com"``. Trailing slash is automatically removed. - :type base_url: str + :type base_url: ``str`` :param credential: Azure Identity credential for authentication. :type credential: ~azure.core.credentials.TokenCredential :param config: Optional configuration for language, timeouts, and retries. @@ -38,8 +38,7 @@ class DataverseClient: :raises ValueError: If ``base_url`` is missing or empty after trimming. .. note:: - The client lazily initializes its internal OData client on first use, allowing - lightweight construction without immediate network calls. + The client lazily initializes its internal OData client on first use, allowing lightweight construction without immediate network calls. Example: Create a client and perform basic operations:: @@ -106,13 +105,13 @@ def create(self, logical_name: str, records: Union[Dict[str, Any], List[Dict[str Create one or more records by logical (singular) entity name. :param logical_name: Logical (singular) entity name, e.g. ``"account"`` or ``"contact"``. - :type logical_name: str + :type logical_name: ``str`` :param records: A single record dictionary or a list of record dictionaries. Each dictionary should contain attribute logical names as keys. - :type records: dict or list[dict] + :type records: ``dict`` or ``list[dict]`` :return: List of created record GUIDs. Returns a single-element list for a single input. - :rtype: list[str] + :rtype: ``list[str]`` :raises TypeError: If ``records`` is not a dict or list[dict], or if the internal client returns an unexpected type. @@ -159,21 +158,19 @@ def update(self, logical_name: str, ids: Union[str, List[str]], changes: Union[D 3. Paired updates: ``update("account", [id1, id2], [changes1, changes2])`` - one-to-one mapping :param logical_name: Logical (singular) entity name, e.g. ``"account"``. - :type logical_name: str + :type logical_name: ``str`` :param ids: Single GUID string or list of GUID strings to update. - :type ids: str or list[str] + :type ids: ``str`` or ``list[str]`` :param changes: Dictionary of changes for single/broadcast mode, or list of dictionaries for paired mode. When ``ids`` is a list and ``changes`` is a single dict, the same changes are broadcast to all records. When both are lists, they must have equal length for one-to-one mapping. - :type changes: dict or list[dict] + :type changes: ``dict`` or ``list[dict]`` :raises TypeError: If ``ids`` is not str or list[str], or if ``changes`` type doesn't match usage pattern. .. note:: - Single updates discard the response representation for better performance. - For broadcast or paired updates, the method delegates to the internal client's - batch update logic. + Single updates discard the response representation for better performance. For broadcast or paired updates, the method delegates to the internal client's batch update logic. Example: Single record update:: @@ -214,18 +211,18 @@ def delete( Delete one or more records by GUID. :param logical_name: Logical (singular) entity name, e.g. ``"account"``. - :type logical_name: str + :type logical_name: ``str`` :param ids: Single GUID string or list of GUID strings to delete. - :type ids: str or list[str] + :type ids: ``str`` or ``list[str]`` :param use_bulk_delete: When ``True`` (default) and ``ids`` is a list, execute the BulkDelete action and return its async job identifier. When ``False`` each record is deleted sequentially. - :type use_bulk_delete: bool + :type use_bulk_delete: ``bool`` :raises TypeError: If ``ids`` is not str or list[str]. :raises HttpError: If the underlying Web API delete request fails. :return: BulkDelete job ID when deleting multiple records via BulkDelete; otherwise ``None``. - :rtype: str or None + :rtype: ``str`` or ``None`` Example: Delete a single record:: @@ -270,25 +267,25 @@ def get( When ``record_id`` is None, returns a generator yielding batches of records. :param logical_name: Logical (singular) entity name, e.g. ``"account"``. - :type logical_name: str + :type logical_name: ``str`` :param record_id: Optional GUID to fetch a specific record. If None, queries multiple records. - :type record_id: str or None + :type record_id: ``str`` or ``None`` :param select: Optional list of attribute logical names to retrieve. - :type select: list[str] or None + :type select: ``list[str]`` or ``None`` :param filter: Optional OData filter string, e.g. ``"name eq 'Contoso'"``. - :type filter: str or None + :type filter: ``str`` or ``None`` :param orderby: Optional list of attributes to sort by, e.g. ``["name asc", "createdon desc"]``. - :type orderby: list[str] or None + :type orderby: ``list[str]`` or ``None`` :param top: Optional maximum number of records to return. - :type top: int or None + :type top: ``int`` or ``None`` :param expand: Optional list of navigation properties to expand. - :type expand: list[str] or None + :type expand: ``list[str]`` or ``None`` :param page_size: Optional number of records per page for pagination. - :type page_size: int or None + :type page_size: ``int`` or ``None`` :return: Single record dict if ``record_id`` is provided, otherwise a generator yielding lists of record dictionaries (one list per page). - :rtype: dict or Iterable[list[dict]] + :rtype: ``dict`` or ``Iterable[list[dict]]`` :raises TypeError: If ``record_id`` is provided but not a string. @@ -343,18 +340,16 @@ def query_sql(self, sql: str): table alias after FROM. :param sql: Supported SQL SELECT statement. - :type sql: str + :type sql: ``str`` :return: List of result row dictionaries. Returns an empty list if no rows match. - :rtype: list[dict] + :rtype: ``list[dict]`` :raises ~PowerPlatform.Dataverse.core.errors.SQLParseError: If the SQL query uses unsupported syntax. :raises ~PowerPlatform.Dataverse.core.errors.HttpError: If the Web API returns an error. .. note:: - The SQL support is limited to read-only queries. Complex joins, subqueries, - and certain SQL functions may not be supported. Consult the Dataverse - documentation for the current feature set. + The SQL support is limited to read-only queries. Complex joins, subqueries, and certain SQL functions may not be supported. Consult the Dataverse documentation for the current feature set. Example: Basic SQL query:: @@ -378,12 +373,12 @@ def get_table_info(self, tablename: str) -> Optional[Dict[str, Any]]: :param tablename: Table friendly name (e.g. ``"SampleItem"``) or full schema name (e.g. ``"new_SampleItem"``). - :type tablename: str + :type tablename: ``str`` :return: Dictionary containing table metadata with keys ``entity_schema``, ``entity_logical_name``, ``entity_set_name``, and ``metadata_id``. Returns None if the table is not found. - :rtype: dict or None + :rtype: ``dict`` or ``None`` Example: Retrieve table metadata:: @@ -407,7 +402,7 @@ def create_table( :param tablename: Table friendly name (e.g. ``"SampleItem"``) or full schema name (e.g. ``"new_SampleItem"``). If a publisher prefix is not included, the default publisher prefix will be applied. - :type tablename: str + :type tablename: ``str`` :param schema: Dictionary mapping column logical names (without prefix) to their types. Supported types: @@ -423,14 +418,14 @@ class ItemStatus(IntEnum): 1036: {"Active": "Actif", "Inactive": "Inactif"} } - :type schema: dict[str, Any] + :type schema: ``dict[str, Any]`` :param solution_unique_name: Optional solution unique name that should own the new table. When omitted the table is created in the default solution. - :type solution_unique_name: str or None + :type solution_unique_name: ``str`` or ``None`` :return: Dictionary containing table metadata including ``entity_schema``, ``entity_set_name``, ``entity_logical_name``, ``metadata_id``, and ``columns_created``. - :rtype: dict + :rtype: ``dict`` :raises ~PowerPlatform.Dataverse.core.errors.MetadataError: If table creation fails or the schema is invalid. @@ -467,7 +462,7 @@ def delete_table(self, tablename: str) -> None: :param tablename: Table friendly name (e.g. ``"SampleItem"``) or full schema name (e.g. ``"new_SampleItem"``). - :type tablename: str + :type tablename: ``str`` :raises ~PowerPlatform.Dataverse.core.errors.MetadataError: If the table does not exist or deletion fails. @@ -487,7 +482,7 @@ def list_tables(self) -> list[str]: List all custom tables in the Dataverse environment. :return: List of custom table names. - :rtype: list[str] + :rtype: ``list[str]`` Example: List all custom tables:: @@ -507,13 +502,13 @@ def create_columns( Create one or more columns on an existing table using a schema-style mapping. :param tablename: Friendly name ("SampleItem") or full schema name ("new_SampleItem"). - :type tablename: str + :type tablename: ``str`` :param columns: Mapping of logical names (without prefix) to supported types. Primitive types include ``string``, ``int``, ``decimal``, ``float``, ``datetime``, and ``bool``. Enum subclasses (IntEnum preferred) generate a local option set and can specify localized labels via ``__labels__``. - :type columns: Dict[str, Any] + :type columns: ``Dict[str, Any]`` :returns: Schema names for the columns that were created. - :rtype: list[str] + :rtype: ``list[str]`` Example: Create two columns on the custom table:: @@ -540,12 +535,12 @@ def delete_columns( Delete one or more columns from a table. :param tablename: Friendly or schema name of the table. - :type tablename: str + :type tablename: ``str`` :param columns: Column name or list of column names to remove. Friendly names are normalized to schema names using the same prefix logic as ``create_columns``. - :type columns: str | list[str] + :type columns: ``str`` | ``list[str]`` :returns: Schema names for the columns that were removed. - :rtype: list[str] + :rtype: ``list[str]`` Example: Remove two custom columns by schema name: @@ -575,32 +570,31 @@ def upload_file( Upload a file to a Dataverse file column. :param logical_name: Singular logical table name, e.g. ``"account"``. - :type logical_name: str + :type logical_name: ``str`` :param record_id: GUID of the target record. - :type record_id: str + :type record_id: ``str`` :param file_name_attribute: Logical name of the file column attribute. - :type file_name_attribute: str + :type file_name_attribute: ``str`` :param path: Local filesystem path to the file. The stored filename will be the basename of this path. - :type path: str + :type path: ``str`` :param mode: Upload strategy: ``"auto"`` (default), ``"small"``, or ``"chunk"``. Auto mode selects small or chunked upload based on file size. - :type mode: str or None + :type mode: ``str`` or ``None`` :param mime_type: Explicit MIME type to store with the file (e.g. ``"application/pdf"``). If not provided, the MIME type may be inferred from the file extension. - :type mime_type: str or None + :type mime_type: ``str`` or ``None`` :param if_none_match: When True (default), sends ``If-None-Match: null`` header to only succeed if the column is currently empty. Set False to always overwrite using ``If-Match: *``. Used for small and chunk modes only. - :type if_none_match: bool + :type if_none_match: ``bool`` :raises ~PowerPlatform.Dataverse.core.errors.HttpError: If the upload fails or the file column is not empty when ``if_none_match=True``. :raises FileNotFoundError: If the specified file path does not exist. .. note:: - Large files are automatically chunked to avoid request size limits. The chunk - mode performs multiple requests with resumable upload support. + Large files are automatically chunked to avoid request size limits. The chunk mode performs multiple requests with resumable upload support. Example: Upload a PDF file:: @@ -647,10 +641,10 @@ def flush_cache(self, kind) -> int: Future kinds (e.g. ``"entityset"``, ``"primaryid"``) may be added without breaking this signature. - :type kind: str + :type kind: ``str`` :return: Number of cache entries removed. - :rtype: int + :rtype: ``int`` Example: Clear the picklist cache:: diff --git a/src/PowerPlatform/Dataverse/core/auth.py b/src/PowerPlatform/Dataverse/core/auth.py index 9207eb9..f5b6973 100644 --- a/src/PowerPlatform/Dataverse/core/auth.py +++ b/src/PowerPlatform/Dataverse/core/auth.py @@ -3,6 +3,14 @@ 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 +storing the acquired token alongside its scope. +""" + from dataclasses import dataclass from azure.core.credentials import TokenCredential @@ -14,9 +22,9 @@ 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: ``str`` :param access_token: The access token string. - :type access_token: str + :type access_token: ``str`` """ resource: str access_token: str @@ -43,7 +51,7 @@ def acquire_token(self, scope: str) -> TokenPair: Acquire an access token for the specified OAuth2 scope. :param scope: OAuth2 scope string, typically ``"https://.crm.dynamics.com/.default"``. - :type scope: str + :type scope: ``str`` :return: Token pair containing the scope and access token. :rtype: ~PowerPlatform.Dataverse.core.auth.TokenPair :raises ~azure.core.exceptions.ClientAuthenticationError: If token acquisition fails. diff --git a/src/PowerPlatform/Dataverse/core/config.py b/src/PowerPlatform/Dataverse/core/config.py index 2b74134..acef530 100644 --- a/src/PowerPlatform/Dataverse/core/config.py +++ b/src/PowerPlatform/Dataverse/core/config.py @@ -3,6 +3,14 @@ from __future__ import annotations +""" +Dataverse client configuration. + +Provides :class:`~PowerPlatform.Dataverse.core.config.DataverseConfig`, a lightweight +immutable container for locale and (reserved) HTTP tuning options plus the +convenience constructor :meth:`~PowerPlatform.Dataverse.core.config.DataverseConfig.from_env`. +""" + from dataclasses import dataclass from typing import Optional @@ -13,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: ``int`` :param http_retries: Optional maximum number of retry attempts for transient HTTP errors. Reserved for future use. - :type http_retries: int or None + :type http_retries: ``int`` | ``None`` :param http_backoff: Optional backoff multiplier (in seconds) between retry attempts. Reserved for future use. - :type http_backoff: float or None + :type http_backoff: ``float`` | ``None`` :param http_timeout: Optional request timeout in seconds. Reserved for future use. - :type http_timeout: float or None + :type http_timeout: ``float`` | ``None`` """ language_code: int = 1033 diff --git a/src/PowerPlatform/Dataverse/core/error_codes.py b/src/PowerPlatform/Dataverse/core/error_codes.py index 139132f..0ff36d3 100644 --- a/src/PowerPlatform/Dataverse/core/error_codes.py +++ b/src/PowerPlatform/Dataverse/core/error_codes.py @@ -81,9 +81,9 @@ 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: ``int`` :return: Error subcode string (e.g., "http_400", "http_404"). - :rtype: str + :rtype: ``str`` """ return HTTP_STATUS_TO_SUBCODE.get(status, f"http_{status}") @@ -95,8 +95,8 @@ def is_transient_status(status: int) -> bool: 503 (Service Unavailable), and 504 (Gateway Timeout). :param status: HTTP status code to check. - :type status: int + :type status: ``int`` :return: True if the status code is considered transient. - :rtype: bool + :rtype: ``bool`` """ return status in TRANSIENT_STATUS diff --git a/src/PowerPlatform/Dataverse/core/errors.py b/src/PowerPlatform/Dataverse/core/errors.py index 428263f..28c535f 100644 --- a/src/PowerPlatform/Dataverse/core/errors.py +++ b/src/PowerPlatform/Dataverse/core/errors.py @@ -1,6 +1,17 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +""" +Structured Dataverse exception hierarchy. + +This module provides :class:`~PowerPlatform.Dataverse.core.errors.DataverseError` and +specialized :class:`~PowerPlatform.Dataverse.core.errors.ValidationError`, +:class:`~PowerPlatform.Dataverse.core.errors.MetadataError`, +:class:`~PowerPlatform.Dataverse.core.errors.SQLParseError`, and +:class:`~PowerPlatform.Dataverse.core.errors.HttpError` for validation, metadata, +SQL parsing, and Web API HTTP failures. +""" + from __future__ import annotations from typing import Any, Dict, Optional import datetime as _dt @@ -10,24 +21,23 @@ class DataverseError(Exception): Base structured exception for the Dataverse SDK. :param message: Human-readable error message. - :type message: str + :type message: ``str`` :param code: Error category code (e.g. ``"validation_error"``, ``"http_error"``). - :type code: str + :type code: ``str`` :param subcode: Optional subcategory or specific error identifier. - :type subcode: str or None + :type subcode: ``str`` | ``None`` :param status_code: Optional HTTP status code if the error originated from an HTTP response. - :type status_code: int or None + :type status_code: ``int`` | ``None`` :param details: Optional dictionary containing additional diagnostic information. - :type details: dict or None + :type details: ``dict`` | ``None`` :param source: Error source, either ``"client"`` or ``"server"``. - :type source: str + :type source: ``str`` :param is_transient: Whether the error is potentially transient and may succeed on retry. - :type is_transient: bool + :type is_transient: ``bool`` """ def __init__( self, message: str, - *, code: str, subcode: Optional[str] = None, status_code: Optional[int] = None, @@ -50,7 +60,7 @@ def to_dict(self) -> Dict[str, Any]: Convert the error to a dictionary representation. :return: Dictionary containing all error properties. - :rtype: dict + :rtype: ``dict`` """ return { "message": self.message, @@ -71,11 +81,11 @@ class ValidationError(DataverseError): Exception raised for client-side validation failures. :param message: Human-readable validation error message. - :type message: str + :type message: ``str`` :param subcode: Optional specific validation error identifier. - :type subcode: str or None + :type subcode: ``str`` | ``None`` :param details: Optional dictionary with additional validation context. - :type details: dict or None + :type details: ``dict`` | ``None`` """ def __init__(self, message: str, *, subcode: Optional[str] = None, details: Optional[Dict[str, Any]] = None): super().__init__(message, code="validation_error", subcode=subcode, details=details, source="client") @@ -85,11 +95,11 @@ class MetadataError(DataverseError): Exception raised for metadata operation failures. :param message: Human-readable metadata error message. - :type message: str + :type message: ``str`` :param subcode: Optional specific metadata error identifier. - :type subcode: str or None + :type subcode: ``str`` | ``None`` :param details: Optional dictionary with additional metadata context. - :type details: dict or None + :type details: ``dict`` | ``None`` """ def __init__(self, message: str, *, subcode: Optional[str] = None, details: Optional[Dict[str, Any]] = None): super().__init__(message, code="metadata_error", subcode=subcode, details=details, source="client") @@ -99,11 +109,11 @@ class SQLParseError(DataverseError): Exception raised for SQL query parsing failures. :param message: Human-readable SQL parsing error message. - :type message: str + :type message: ``str`` :param subcode: Optional specific SQL parsing error identifier. - :type subcode: str or None + :type subcode: ``str`` | ``None`` :param details: Optional dictionary with SQL query context and parse information. - :type details: dict or None + :type details: ``dict`` | ``None`` """ def __init__(self, message: str, *, subcode: Optional[str] = None, details: Optional[Dict[str, Any]] = None): super().__init__(message, code="sql_parse_error", subcode=subcode, details=details, source="client") @@ -113,27 +123,27 @@ class HttpError(DataverseError): Exception raised for HTTP request failures from the Dataverse Web API. :param message: Human-readable HTTP error message, typically from the API error response. - :type message: str + :type message: ``str`` :param status_code: HTTP status code (e.g. 400, 404, 500). - :type status_code: int + :type status_code: ``int`` :param is_transient: Whether the error is transient (429, 503, 504) and may succeed on retry. - :type is_transient: bool + :type is_transient: ``bool`` :param subcode: Optional HTTP status category (e.g. ``"4xx"``, ``"5xx"``). - :type subcode: str or None + :type subcode: ``str`` | ``None`` :param service_error_code: Optional Dataverse-specific error code from the API response. - :type service_error_code: str or None + :type service_error_code: ``str`` | ``None`` :param correlation_id: Optional correlation ID for tracking requests across services. - :type correlation_id: str or None + :type correlation_id: ``str`` | ``None`` :param request_id: Optional request ID from the API response headers. - :type request_id: str or None + :type request_id: ``str`` | ``None`` :param traceparent: Optional W3C trace context for distributed tracing. - :type traceparent: str or None + :type traceparent: ``str`` | ``None`` :param body_excerpt: Optional excerpt of the response body for diagnostics. - :type body_excerpt: str or None + :type body_excerpt: ``str`` | ``None`` :param retry_after: Optional number of seconds to wait before retrying (from Retry-After header). - :type retry_after: int or None + :type retry_after: ``int`` | ``None`` :param details: Optional additional diagnostic details. - :type details: dict or None + :type details: ``dict`` | ``None`` """ def __init__( self, diff --git a/src/PowerPlatform/Dataverse/core/http.py b/src/PowerPlatform/Dataverse/core/http.py index 286a439..f19c068 100644 --- a/src/PowerPlatform/Dataverse/core/http.py +++ b/src/PowerPlatform/Dataverse/core/http.py @@ -4,9 +4,9 @@ """ 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. +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. """ from __future__ import annotations @@ -25,16 +25,15 @@ class HttpClient: management for different HTTP methods. :param retries: Maximum number of retry attempts for transient errors. Default is 5. - :type retries: int or None + :type retries: ``int`` | ``None`` :param backoff: Base delay in seconds between retry attempts. Default is 0.5. - :type backoff: float or None + :type backoff: ``float`` | ``None`` :param timeout: Default request timeout in seconds. If None, uses per-method defaults. - :type timeout: float or None + :type timeout: ``float`` | ``None`` """ def __init__( self, - *, retries: Optional[int] = None, backoff: Optional[float] = None, timeout: Optional[float] = None, @@ -51,12 +50,12 @@ def request(self, method: str, url: str, **kwargs: Any) -> requests.Response: and retries on network errors with exponential backoff. :param method: HTTP method (GET, POST, PUT, DELETE, etc.). - :type method: str + :type method: ``str`` :param url: Target URL for the request. - :type url: str + :type url: ``str`` :param kwargs: Additional arguments passed to ``requests.request()``, including headers, data, etc. :return: HTTP response object. - :rtype: requests.Response + :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; diff --git a/src/PowerPlatform/Dataverse/data/odata.py b/src/PowerPlatform/Dataverse/data/odata.py index c2880bd..bdcff06 100644 --- a/src/PowerPlatform/Dataverse/data/odata.py +++ b/src/PowerPlatform/Dataverse/data/odata.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +"""Dataverse Web API client with CRUD, SQL query, and table/column metadata management.""" + from __future__ import annotations from typing import Any, Dict, Optional, List, Union, Iterable @@ -38,6 +40,18 @@ def __init__( base_url: str, config=None, ) -> None: + """Initialize the OData client. + + Sets up authentication, base URL, configuration, and internal caches. + + :param auth: Authentication manager providing ``acquire_token(scope)`` that returns an object with ``access_token``. + :type auth: ~PowerPlatform.Dataverse.core.auth.AuthManager + :param base_url: Organization base URL (e.g. ``"https://.crm.dynamics.com"``). + :type base_url: ``str`` + :param config: Optional Dataverse configuration (HTTP retry, backoff, timeout, language code). If omitted ``DataverseConfig.from_env()`` is used. + :type config: ~PowerPlatform.Dataverse.core.config.DataverseConfig | ``None`` + :raises ValueError: If ``base_url`` is empty after stripping. + """ self.auth = auth self.base_url = (base_url or "").rstrip("/") if not self.base_url: @@ -136,24 +150,18 @@ def _request(self, method: str, url: str, *, expected: tuple[int, ...] = (200, 2 def _create(self, entity_set: str, logical_name: str, record: Dict[str, Any]) -> str: """Create a single record and return its GUID. - Parameters - ------- - entity_set : str - Resolved entity set (plural) name. - logical_name : str - Singular logical entity name. - record : dict[str, Any] - Attribute payload mapped by logical column names. + :param entity_set: Resolved entity set (plural) name. + :type entity_set: ``str`` + :param logical_name: Singular logical entity name. + :type logical_name: ``str`` + :param record: Attribute payload mapped by logical column names. + :type record: ``dict[str, Any]`` - Returns - ------- - str - Created record GUID. + :return: Created record GUID. + :rtype: ``str`` - Notes - ------- - Relies on OData-EntityId (canonical) or Location header. No response body parsing is performed. - Raises RuntimeError if neither header contains a GUID. + .. note:: + Relies on ``OData-EntityId`` (canonical) or ``Location`` response header. No response body parsing is performed. Raises ``RuntimeError`` if neither header contains a GUID. """ record = self._convert_labels_to_ints(logical_name, record) url = f"{self.api}/{entity_set}" @@ -175,26 +183,20 @@ def _create(self, entity_set: str, logical_name: str, record: Dict[str, Any]) -> ) def _create_multiple(self, entity_set: str, logical_name: str, records: List[Dict[str, Any]]) -> List[str]: - """Create multiple records using the collection-bound CreateMultiple action. - - Parameters - ---------- - entity_set : str - Resolved entity set (plural) name. - logical_name : str - Singular logical entity name. - records : list[dict[str, Any]] - Payloads mapped by logical attribute names. - - Multi-create logical name resolution - ------------------------------------ - - If any payload omits ``@odata.type`` the client stamps ``Microsoft.Dynamics.CRM.``. - - If all payloads already include ``@odata.type`` no modification occurs. - - Returns - ------- - list[str] - List of created IDs. + """Create multiple records using the collection-bound ``CreateMultiple`` action. + + :param entity_set: Resolved entity set (plural) name. + :type entity_set: ``str`` + :param logical_name: Singular logical entity name. + :type logical_name: ``str`` + :param records: Payload dictionaries mapped by logical attribute names. + :type records: ``list[dict[str, Any]]`` + + :return: List of created record GUIDs (may be empty if response lacks IDs). + :rtype: ``list[str]`` + + .. note:: + Logical type stamping: if any payload omits ``@odata.type`` the client injects ``Microsoft.Dynamics.CRM.``. If all payloads already include ``@odata.type`` no modification occurs. """ if not all(isinstance(r, dict) for r in records): raise TypeError("All items for multi-create must be dicts") @@ -254,16 +256,17 @@ def _primary_id_attr(self, logical_name: str) -> str: ) def _update_by_ids(self, logical_name: str, ids: List[str], changes: Union[Dict[str, Any], List[Dict[str, Any]]]) -> None: - """Update many records by GUID list using UpdateMultiple under the hood. - - Parameters - ---------- - logical_name : str - Logical name (singular). - ids : list[str] - GUIDs of target records. - changes : dict | list[dict] - Broadcast patch (dict) applied to all IDs, or list of per-record patches (1:1 with ids). + """Update many records by GUID list using the collection-bound ``UpdateMultiple`` action. + + :param logical_name: Logical (singular) entity name. + :type logical_name: ``str`` + :param ids: GUIDs of target records. + :type ids: ``list[str]`` + :param changes: Broadcast patch (``dict``) applied to all IDs, or list of per-record patches (1:1 with ``ids``). + :type changes: ``dict`` | ``list[dict]`` + + :return: ``None`` + :rtype: ``None`` """ if not isinstance(ids, list): raise TypeError("ids must be list[str]") @@ -292,9 +295,15 @@ def _delete_multiple( logical_name: str, ids: List[str], ) -> Optional[str]: - """Delete many records by GUID list. + """Delete many records by GUID list via the ``BulkDelete`` action. - Returns the asynchronous job identifier reported by the BulkDelete action. + :param logical_name: Logical (singular) entity name. + :type logical_name: ``str`` + :param ids: GUIDs of records to delete. + :type ids: ``list[str]`` + + :return: BulkDelete asynchronous job identifier when executed in bulk; ``None`` if no IDs provided or single deletes performed. + :rtype: ``str`` | ``None`` """ targets = [rid for rid in ids if rid] if not targets: @@ -366,20 +375,16 @@ def esc(match): return f"({k})" def _update(self, logical_name: str, key: str, data: Dict[str, Any]) -> None: - """Update an existing record. - - Parameters - ---------- - logical_name : str - Logical (singular) entity name. - key : str - Record GUID (with or without parentheses) or alternate key. - data : dict - Partial entity payload. - - Returns - ------- - None + """Update an existing record by GUID or alternate key. + + :param logical_name: Logical (singular) entity name. + :type logical_name: ``str`` + :param key: Record GUID (with or without parentheses) or alternate key syntax. + :type key: ``str`` + :param data: Partial entity payload (attributes to patch). + :type data: ``dict[str, Any]`` + :return: ``None`` + :rtype: ``None`` """ data = self._convert_labels_to_ints(logical_name, data) entity_set = self._entity_set_from_logical(logical_name) @@ -387,34 +392,22 @@ def _update(self, logical_name: str, key: str, data: Dict[str, Any]) -> None: r = self._request("patch", url, headers={"If-Match": "*"}, json=data) def _update_multiple(self, entity_set: str, logical_name: str, records: List[Dict[str, Any]]) -> None: - """Bulk update existing records via the collection-bound UpdateMultiple action. - - Parameters - ---------- - entity_set : str - Resolved entity set name. - logical_name : str - Logical (singular) name, e.g. "account". - records : list[dict] - Each dict must include the real primary key attribute for the entity (e.g. ``accountid``) and one or more - fields to update. If ``@odata.type`` is omitted in any payload, the logical name is resolved once and - stamped into those payloads as ``Microsoft.Dynamics.CRM.`` (same behaviour as bulk create). - - Behaviour - --------- - - POST ``/{entity_set}/Microsoft.Dynamics.CRM.UpdateMultiple`` with body ``{"Targets": [...]}``. - - Expects Dataverse transactional semantics: if any individual update fails the entire request is rolled back. - - Response content is ignored; no stable contract for returned IDs or representations. - - Returns - ------- - None - No representation is returned (symmetry with single update). - - Notes - ----- - - Caller must include the correct primary key attribute (e.g. ``accountid``) in every record. - - Both single and multiple updates return None. + """Bulk update existing records via the collection-bound ``UpdateMultiple`` action. + + :param entity_set: Resolved entity set (plural) name. + :type entity_set: ``str`` + :param logical_name: Logical (singular) name (e.g. ``"account"``). + :type logical_name: ``str`` + :param records: List of patch dictionaries. Each must include the true primary key attribute (e.g. ``accountid``) and one or more fields to update. + :type records: ``list[dict[str, Any]]`` + :return: ``None`` + :rtype: ``None`` + + .. note:: + - Endpoint: ``POST /{entity_set}/Microsoft.Dynamics.CRM.UpdateMultiple`` with body ``{"Targets": [...]}``. + - Transactional semantics: if any individual update fails, the entire request rolls back. + - Response content is ignored; no stable contract for returned IDs/representations. + - Caller must supply the correct primary key attribute (e.g. ``accountid``) in every record. """ if not isinstance(records, list) or not records or not all(isinstance(r, dict) for r in records): raise TypeError("records must be a non-empty list[dict]") @@ -438,7 +431,16 @@ def _update_multiple(self, entity_set: str, logical_name: str, records: List[Dic return None def _delete(self, logical_name: str, key: str) -> None: - """Delete a record by GUID or alternate key.""" + """Delete a record by GUID or alternate key. + + :param logical_name: Singular logical entity name. + :type logical_name: ``str`` + :param key: Record GUID (with or without parentheses) or alternate key syntax. + :type key: ``str`` + + :return: ``None`` + :rtype: ``None`` + """ entity_set = self._entity_set_from_logical(logical_name) url = f"{self.api}/{entity_set}{self._format_key(key)}" self._request("delete", url, headers={"If-Match": "*"}) @@ -446,14 +448,15 @@ def _delete(self, logical_name: str, key: str) -> None: def _get(self, logical_name: str, key: str, select: Optional[str] = None) -> Dict[str, Any]: """Retrieve a single record. - Parameters - ---------- - logical_name : str - Logical (singular) name. - key : str - Record GUID (with or without parentheses) or alternate key syntax. - select : str | None - Comma separated columns for $select. + :param logical_name: Singular logical entity name. + :type logical_name: ``str`` + :param key: Record GUID (with or without parentheses) or alternate key syntax. + :type key: ``str`` + :param select: Comma separated columns for ``$select`` (optional). + :type select: ``str`` | ``None`` + + :return: Retrieved record dictionary (may be empty if no selected attributes). + :rtype: ``dict[str, Any]`` """ params = {} if select: @@ -475,27 +478,23 @@ def _get_multiple( ) -> Iterable[List[Dict[str, Any]]]: """Iterate records from an entity set, yielding one page (list of dicts) at a time. - Parameters - ---------- - logical_name : str - Logical (singular) entity name. - select : list[str] | None - Columns to select; joined with commas into $select. - filter : str | None - OData $filter expression as a string. - orderby : list[str] | None - Order expressions; joined with commas into $orderby. - top : int | None - Max number of records across all pages. Passed as $top on the first request; the server will paginate via nextLink as needed. - expand : list[str] | None - Navigation properties to expand; joined with commas into $expand. - page_size : int | None - Hint for per-page size using Prefer: ``odata.maxpagesize``. - - Yields - ------ - list[dict] - A page of records from the Web API (the "value" array for each page). + :param logical_name: Singular logical entity name. + :type logical_name: ``str`` + :param select: Columns to include (``$select``) or ``None``. + :type select: ``list[str]`` | ``None`` + :param filter: OData ``$filter`` expression or ``None``. + :type filter: ``str`` | ``None`` + :param orderby: Order expressions (``$orderby``) or ``None``. + :type orderby: ``list[str]`` | ``None`` + :param top: Max total records (applied on first request as ``$top``) or ``None``. + :type top: ``int`` | ``None`` + :param expand: Navigation properties to expand (``$expand``) or ``None``. + :type expand: ``list[str]`` | ``None`` + :param page_size: Per-page size hint via ``Prefer: odata.maxpagesize``. + :type page_size: ``int`` | ``None`` + + :return: Iterator yielding pages (each page is a ``list`` of record dicts). + :rtype: ``Iterable[list[dict[str, Any]]]`` """ extra_headers: Dict[str, str] = {} @@ -544,30 +543,19 @@ def _do_request(url: str, *, params: Optional[Dict[str, Any]] = None) -> Dict[st # --------------------------- SQL Custom API ------------------------- def _query_sql(self, sql: str) -> list[dict[str, Any]]: - """Execute a read-only SQL query using the Dataverse Web API `?sql=` capability. - - The platform supports a constrained subset of SQL SELECT statements directly on entity set endpoints: - GET /{entity_set}?sql= - - This client extracts the logical table name from the query, resolves the corresponding - entity set name (cached) and invokes the Web API using the `sql` query parameter. - - Parameters - ---------- - sql : str - Single SELECT statement within supported subset. - - Returns - ------- - list[dict] - Result rows (empty list if none). - - Raises - ------ - ValueError - If the SQL is empty or malformed, or if the table logical name cannot be determined. - RuntimeError - If metadata lookup for the logical name fails. + """Execute a read-only SQL SELECT using the Dataverse Web API ``?sql=`` capability. + + :param sql: Single SELECT statement within the supported subset. + :type sql: ``str`` + + :return: Result rows (empty list if none). + :rtype: ``list[dict[str, Any]]`` + + :raises ValidationError: If ``sql`` is not a ``str`` or is empty. + :raises MetadataError: If logical table name resolution fails. + + .. note:: + Endpoint form: ``GET /{entity_set}?sql=``. The client extracts the logical table name, resolves the entity set (metadata cached), then issues the request. Only a constrained SELECT subset is supported by the platform. """ if not isinstance(sql, str): raise ValidationError("sql must be a string", subcode=ec.VALIDATION_SQL_NOT_STRING) @@ -1113,15 +1101,11 @@ def _attribute_payload(self, schema_name: str, dtype: Any, *, is_primary_name: b def _get_table_info(self, tablename: str) -> Optional[Dict[str, Any]]: """Return basic metadata for a custom table if it exists. - Parameters - ---------- - tablename : str - Friendly name or full schema name (with publisher prefix and underscore). + :param tablename: Friendly name (without prefix) or full schema name including publisher prefix (e.g. ``new_Sample``). + :type tablename: ``str`` - Returns - ------- - dict | None - Metadata summary or ``None`` if not found. + :return: Metadata summary or ``None`` if not found. + :rtype: ``dict[str, Any]`` | ``None`` """ ent = self._get_entity_by_schema(tablename) if not ent: @@ -1135,7 +1119,13 @@ def _get_table_info(self, tablename: str) -> Optional[Dict[str, Any]]: } def _list_tables(self) -> List[Dict[str, Any]]: - """List all tables in the Dataverse, excluding private tables (IsPrivate=true).""" + """List all non-private tables (``IsPrivate eq false``). + + :return: Metadata entries for non-private tables (may be empty). + :rtype: ``list[dict[str, Any]]`` + + :raises HttpError: If the metadata request fails. + """ url = f"{self.api}/EntityDefinitions" params = { "$filter": "IsPrivate eq false" @@ -1144,6 +1134,17 @@ def _list_tables(self) -> List[Dict[str, Any]]: return r.json().get("value", []) def _delete_table(self, tablename: str) -> None: + """Delete a custom table by friendly or full schema name. + + :param tablename: Friendly name (without publisher prefix) or full schema name (e.g. ``new_Sample``). + :type tablename: ``str`` + + :return: ``None`` + :rtype: ``None`` + + :raises MetadataError: If the table does not exist. + :raises HttpError: If the delete request fails. + """ entity_schema = self._normalize_entity_schema(tablename) ent = self._get_entity_by_schema(entity_schema) if not ent or not ent.get("MetadataId"): @@ -1161,6 +1162,27 @@ def _create_table( schema: Dict[str, Any], solution_unique_name: Optional[str] = None, ) -> Dict[str, Any]: + """Create a custom table with specified columns. + + Accepts a friendly base name (prefix omitted) or a full schema name (with publisher prefix). If + the provided ``tablename`` lacks an underscore, a schema name of the form ``new_`` is + synthesized; otherwise the given name is treated as the full schema name. + + :param tablename: Friendly base name (e.g. ``"sample"``) or full schema name (e.g. ``"new_Sample"``). + :type tablename: ``str`` + :param schema: Mapping of column name (friendly or full) -> type spec (``str`` or ``Enum`` subclass). + :type schema: ``dict[str, Any]`` + :param solution_unique_name: Optional solution container for the new table; if provided must be non-empty. + :type solution_unique_name: ``str`` | ``None`` + + :return: Metadata summary for the created table including created column schema names. + :rtype: ``dict[str, Any]`` + + :raises MetadataError: If the table already exists. + :raises ValueError: If a column type is unsupported or ``solution_unique_name`` is empty. + :raises TypeError: If ``solution_unique_name`` is not a ``str`` when provided. + :raises HttpError: If underlying HTTP requests fail. + """ # Accept a friendly name and construct a default schema under 'new_'. # If a full SchemaName is passed (contains '_'), use as-is. entity_schema = self._normalize_entity_schema(tablename) @@ -1210,6 +1232,21 @@ def _create_columns( tablename: str, columns: Dict[str, Any], ) -> List[str]: + """Create new columns on an existing table. + + :param tablename: Friendly base name or full schema name of the table. + :type tablename: ``str`` + :param columns: Mapping of column name (friendly or full) -> type spec (``str`` or ``Enum`` subclass). + :type columns: ``dict[str, Any]`` + + :return: List of created column schema names. + :rtype: ``list[str]`` + + :raises TypeError: If ``columns`` is not a non-empty dict. + :raises MetadataError: If the target table does not exist. + :raises ValueError: If a column type is unsupported. + :raises HttpError: If an underlying HTTP request fails. + """ if not isinstance(columns, dict) or not columns: raise TypeError("columns must be a non-empty dict[name -> type]") entity_schema = self._normalize_entity_schema(tablename) @@ -1248,6 +1285,22 @@ def _delete_columns( tablename: str, columns: Union[str, List[str]], ) -> List[str]: + """Delete one or more columns from a table. + + :param tablename: Friendly base name or full schema name of the table. + :type tablename: ``str`` + :param columns: Single column name or list of column names (friendly or full schema forms). + :type columns: ``str`` | ``list[str]`` + + :return: List of deleted column schema names (empty if none removed). + :rtype: ``list[str]`` + + :raises TypeError: If ``columns`` is neither a ``str`` nor ``list[str]``. + :raises ValueError: If any provided column name is empty. + :raises MetadataError: If the table or a specified column does not exist. + :raises RuntimeError: If column metadata lacks a required ``MetadataId``. + :raises HttpError: If an underlying delete request fails. + """ if isinstance(columns, str): names = [columns] elif isinstance(columns, list): @@ -1309,20 +1362,11 @@ def _flush_cache( ) -> int: """Flush cached client metadata/state. - Currently supported kinds: - - 'picklist': clears entries from the picklist label cache used by label -> int conversion. - - Parameters - ---------- - kind : str - Cache kind to flush. Only 'picklist' is implemented today. Future kinds - (e.g. 'entityset', 'primaryid') can be added without breaking the signature. - - Returns - ------- - int - Number of cache entries removed. - + :param kind: Cache kind to flush (only ``"picklist"`` supported). + :type kind: ``str`` + :return: Number of cache entries removed. + :rtype: ``int`` + :raises ValidationError: If ``kind`` is unsupported. """ k = (kind or "").strip().lower() if k != "picklist": diff --git a/src/PowerPlatform/Dataverse/data/upload.py b/src/PowerPlatform/Dataverse/data/upload.py index b169fdf..874706a 100644 --- a/src/PowerPlatform/Dataverse/data/upload.py +++ b/src/PowerPlatform/Dataverse/data/upload.py @@ -1,11 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -"""File upload helpers separated from core OData client. - -Provide a mixin class `ODataFileUploadMixin` so these methods can be reused without -duplicating logic inside the main `odata.py` file. -""" +"""File upload helpers.""" from __future__ import annotations @@ -13,14 +9,7 @@ class ODataFileUpload: - """Mixin adding file upload capabilities (small + chunk) with auto selection. - - Expects the host client to implement: - - self.api : str base url (e.g. https://org.crm.dynamics.com/api/data/v9.2) - - self._headers() -> dict[str,str] - - self._request(method, url, **kwargs) - - self._format_key(key: str) -> str - """ + """File upload capabilities (small + chunk) with auto selection.""" def upload_file( self, @@ -36,19 +25,19 @@ def upload_file( Parameters ---------- - entity_set : str + entity_set : ``str`` Target entity set (plural logical name), e.g. "accounts". - record_id : str + record_id : ``str`` GUID of the target record. - file_name_attribute : str + file_name_attribute : ``str`` Logical name of the file column attribute - path : str + path : ``str`` Local filesystem path to the file. - mode : str | None + mode : ``str`` | ``None`` Upload strategy: "auto" (default), "small", or "chunk". - mime_type : str | None + mime_type : ``str`` | ``None`` Explicit MIME type. If omitted falls back to application/octet-stream. - if_none_match : bool + if_none_match : ``bool`` When True (default) only succeeds if column empty. When False overwrites (If-Match: *). """ import os @@ -123,20 +112,20 @@ def _upload_file_chunk( Parameters ---------- - entity_set : str + entity_set : ``str`` Target entity set (plural logical name), e.g. "accounts". - record_id : str + record_id : ``str`` GUID of the target record. - file_name_attribute : str + file_name_attribute : ``str`` Logical name of the file column attribute. - path : str + path : ``str`` Local filesystem path to the file. - if_none_match : bool + if_none_match : ``bool`` When True sends ``If-None-Match: null`` to only succeed if the column is currently empty. Set False to always overwrite (uses ``If-Match: *``). Returns ------- - None + ``None`` Returns nothing on success. Any failure raises an exception. """ import os, math diff --git a/src/PowerPlatform/Dataverse/extensions/__init__.py b/src/PowerPlatform/Dataverse/extensions/__init__.py index 2a69d30..74eb25a 100644 --- a/src/PowerPlatform/Dataverse/extensions/__init__.py +++ b/src/PowerPlatform/Dataverse/extensions/__init__.py @@ -2,10 +2,7 @@ # Licensed under the MIT license. """ -Optional extensions for the Dataverse SDK. - -This module contains optional features like CLI interfaces, async clients, -and other extended functionality. +Optional extensions for the Dataverse SDK. Currently a placeholder. """ # Will be populated with extensions as they are created diff --git a/src/PowerPlatform/Dataverse/models/__init__.py b/src/PowerPlatform/Dataverse/models/__init__.py index c6c679e..95a12a8 100644 --- a/src/PowerPlatform/Dataverse/models/__init__.py +++ b/src/PowerPlatform/Dataverse/models/__init__.py @@ -2,10 +2,7 @@ # Licensed under the MIT license. """ -Data models and type definitions for the Dataverse SDK. - -This module contains entity models, response models, enums, and other -type definitions used throughout the SDK. +Data models and type definitions for the Dataverse SDK. Currently a placeholder. """ # Will be populated with models as they are created diff --git a/src/PowerPlatform/Dataverse/utils/__init__.py b/src/PowerPlatform/Dataverse/utils/__init__.py index e524cc1..2f02dfb 100644 --- a/src/PowerPlatform/Dataverse/utils/__init__.py +++ b/src/PowerPlatform/Dataverse/utils/__init__.py @@ -4,8 +4,7 @@ """ Utilities and adapters for the Dataverse SDK. -This module contains helper functions, adapters (like Pandas integration), -logging utilities, and validation helpers. +This module contains adapters (like Pandas integration). """ from .pandas_adapter import PandasODataClient diff --git a/src/PowerPlatform/Dataverse/utils/pandas_adapter.py b/src/PowerPlatform/Dataverse/utils/pandas_adapter.py index fcd398d..4458457 100644 --- a/src/PowerPlatform/Dataverse/utils/pandas_adapter.py +++ b/src/PowerPlatform/Dataverse/utils/pandas_adapter.py @@ -4,8 +4,8 @@ """ Pandas-friendly wrappers for Dataverse OData operations. -This module provides :class:`PandasODataClient`, a high-level wrapper that enables -DataFrame-based CRUD and query operations. +This module provides :class:`PowerPlatform.Dataverse.utils.pandas_adapter.PandasODataClient`, +a high-level wrapper that enables DataFrame-based CRUD and query operations. """ from __future__ import annotations @@ -26,9 +26,9 @@ class RowError: Container for row-level error information. :param index: Zero-based row index where the error occurred. - :type index: int + :type index: ``int`` :param message: Error message describing the failure. - :type message: str + :type message: ``str`` """ index: int message: str @@ -51,11 +51,11 @@ def create_df(self, logical_name: str, record: pd.Series) -> str: Create a single record from a pandas Series and return the GUID. :param logical_name: Logical (singular) entity name, e.g. ``"account"``. - :type logical_name: str + :type logical_name: ``str`` :param record: Series whose index labels are field logical names and values are field values. - :type record: pandas.Series + :type record: ``pandas.Series`` :return: The created record's GUID. - :rtype: str + :rtype: ``str`` :raises TypeError: If ``record`` is not a pandas Series. :raises RuntimeError: If the internal create operation returns an unexpected format. """ @@ -73,11 +73,11 @@ def update(self, logical_name: str, record_id: str, entity_data: pd.Series) -> N Update a single record with values from a pandas Series. :param logical_name: Logical (singular) entity name, e.g. ``"account"``. - :type logical_name: str + :type logical_name: ``str`` :param record_id: GUID of the record to update. - :type record_id: str + :type record_id: ``str`` :param entity_data: Series whose index labels are field logical names. NaN values are ignored. - :type entity_data: pandas.Series + :type entity_data: ``pandas.Series`` :raises TypeError: If ``entity_data`` is not a pandas Series. """ if not isinstance(entity_data, pd.Series): @@ -93,11 +93,11 @@ def delete_ids(self, logical_name: str, ids: Sequence[str] | pd.Series | pd.Inde Delete a collection of record IDs and return a summary DataFrame. :param logical_name: Logical (singular) entity name, e.g. ``"account"``. - :type logical_name: str + :type logical_name: ``str`` :param ids: Collection of GUIDs to delete. Can be a list, pandas Series, or pandas Index. - :type ids: Sequence[str] or pandas.Series or pandas.Index - :return: DataFrame with columns: ``id`` (str), ``success`` (bool), ``error`` (str or None). - :rtype: pandas.DataFrame + :type ids: ``Sequence[str]`` or ``pandas.Series`` or ``pandas.Index`` + :return: DataFrame with columns: ``id`` (``str``), ``success`` (``bool``), ``error`` (``str`` | ``None``). + :rtype: ``pandas.DataFrame`` """ if isinstance(ids, (pd.Series, pd.Index)): id_list = [str(x) for x in ids.tolist()] @@ -118,13 +118,13 @@ def get_ids(self, logical_name: str, ids: Sequence[str] | pd.Series | pd.Index, Fetch multiple records by ID and return a DataFrame. :param logical_name: Logical (singular) entity name, e.g. ``"account"``. - :type logical_name: str + :type logical_name: ``str`` :param ids: Collection of GUIDs to fetch. Can be a list, pandas Series, or pandas Index. - :type ids: Sequence[str] or pandas.Series or pandas.Index + :type ids: ``Sequence[str]`` or ``pandas.Series`` or ``pandas.Index`` :param select: Optional iterable of field logical names to retrieve. If None, all fields are returned. - :type select: Iterable[str] or None + :type select: ``Iterable[str]`` | ``None`` :return: DataFrame containing fetched records. Failed fetches will have an ``error`` column. - :rtype: pandas.DataFrame + :rtype: ``pandas.DataFrame`` """ if isinstance(ids, (pd.Series, pd.Index)): id_list = [str(x) for x in ids.tolist()] @@ -155,9 +155,9 @@ def query_sql_df(self, sql: str) -> pd.DataFrame: Execute a SQL query via the Dataverse Web API and return a DataFrame. :param sql: SQL SELECT statement following Dataverse Web API SQL syntax. - :type sql: str + :type sql: ``str`` :return: DataFrame containing query results. Returns an empty DataFrame if no rows match. - :rtype: pandas.DataFrame + :rtype: ``pandas.DataFrame`` :raises ValueError: If the API returns a malformed JSON response. """ rows: Any = self._c.query_sql(sql) diff --git a/src/PowerPlatform/__init__.py b/src/PowerPlatform/__init__.py index 4cef197..a52b03f 100644 --- a/src/PowerPlatform/__init__.py +++ b/src/PowerPlatform/__init__.py @@ -1,6 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -"""PowerPlatform namespace package.""" - __path__ = __import__('pkgutil').extend_path(__path__, __name__) \ No newline at end of file