diff --git a/pyproject.toml b/pyproject.toml index 972e2b21..26a081d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,6 @@ documentation = "https://github.com/microsoftgraph/msgraph-sdk-python-core/docs" [tool.mypy] warn_unused_configs = true files = "src" -ignore_missing_imports = true [tool.yapf] based_on_style = "pep8" diff --git a/requirements-dev.txt b/requirements-dev.txt index d8acc9f2..0715f6e6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -88,6 +88,8 @@ python-dotenv==1.0.1 pytest-trio==0.8.0 +pytest-asyncio==0.24.0 + pywin32==308 ; platform_system == 'Windows' requests==2.32.3 ; python_version >= '3.7' @@ -143,14 +145,18 @@ httpx[http2]==0.28.1 hyperframe==6.0.1 ; python_full_version >= '3.6.1' -microsoft-kiota-abstractions==1.3.3 +microsoft-kiota-abstractions==1.6.6 -microsoft-kiota-authentication-azure==1.1.0 +microsoft-kiota-authentication-azure==1.6.6 -microsoft-kiota-http==1.3.3 +microsoft-kiota-http==1.6.6 multidict==6.1.0 ; python_version >= '3.7' uritemplate==4.1.1 ; python_version >= '3.6' yarl==1.15.2 ; python_version >= '3.7' + +deprecated==1.2.15 + +types-Deprecated==1.2.15.20241117 diff --git a/src/msgraph_core/authentication/azure_identity_authentication_provider.py b/src/msgraph_core/authentication/azure_identity_authentication_provider.py index cffa36b0..6f45233b 100644 --- a/src/msgraph_core/authentication/azure_identity_authentication_provider.py +++ b/src/msgraph_core/authentication/azure_identity_authentication_provider.py @@ -19,7 +19,7 @@ def __init__( credentials: Union["TokenCredential", "AsyncTokenCredential"], options: Optional[Dict] = {}, scopes: List[str] = [], - allowed_hosts: Optional[List[str]] = [nc.value for nc in NationalClouds] + allowed_hosts: List[str] = [nc.value for nc in NationalClouds] ) -> None: """[summary] diff --git a/src/msgraph_core/graph_client_factory.py b/src/msgraph_core/graph_client_factory.py index 39344715..5dfcee80 100644 --- a/src/msgraph_core/graph_client_factory.py +++ b/src/msgraph_core/graph_client_factory.py @@ -22,7 +22,8 @@ class GraphClientFactory(KiotaClientFactory): """ @staticmethod - def create_with_default_middleware( + def create_with_default_middleware( # type: ignore + # Breaking change to remove KiotaClientFactory as base class api_version: APIVersion = APIVersion.v1, client: Optional[httpx.AsyncClient] = None, host: NationalClouds = NationalClouds.Global, @@ -53,7 +54,8 @@ def create_with_default_middleware( return GraphClientFactory._load_middleware_to_client(client, middleware) @staticmethod - def create_with_custom_middleware( + def create_with_custom_middleware( # type: ignore + # Breaking change to remove Kiota client factory as base class middleware: Optional[List[BaseMiddleware]], api_version: APIVersion = APIVersion.v1, client: Optional[httpx.AsyncClient] = None, @@ -91,7 +93,9 @@ def _get_telemetry_handler( options""" if options: - graph_telemetry_options = options.get(GraphTelemetryHandlerOption().get_key()) + graph_telemetry_options: GraphTelemetryHandlerOption = options.get( + GraphTelemetryHandlerOption().get_key() + ) # type: ignore # Unable to down cast type if graph_telemetry_options: return GraphTelemetryHandler(options=graph_telemetry_options) return GraphTelemetryHandler() diff --git a/src/msgraph_core/models/large_file_upload_session.py b/src/msgraph_core/models/large_file_upload_session.py index 4d5ee4f1..5faf30cd 100644 --- a/src/msgraph_core/models/large_file_upload_session.py +++ b/src/msgraph_core/models/large_file_upload_session.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING, Union +from typing import Any, Callable, Dict, List, Optional import datetime from dataclasses import dataclass, field @@ -25,7 +25,7 @@ def create_from_discriminator_value( ) -> LargeFileUploadSession: """ Creates a new instance of the appropriate class based - on discriminator value param parse_node: The parse node + on discriminator value param parse_node: The parse node to use to read the discriminator value and create the object Returns: UploadSession """ diff --git a/src/msgraph_core/models/page_result.py b/src/msgraph_core/models/page_result.py index 7effd54e..4d430201 100644 --- a/src/msgraph_core/models/page_result.py +++ b/src/msgraph_core/models/page_result.py @@ -46,8 +46,17 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: object where each entry is a property key with its deserialization callback. """ return { - "@odata.nextLink": lambda x: setattr(self, "odata_next_link", x.get_str_value()), - "value": lambda x: setattr(self, "value", x.get_collection_of_object_values(Parsable)) + "@odata.nextLink": + lambda x: setattr(self, "odata_next_link", x.get_str_value()), + "value": + lambda x: setattr( + self, + "value", + x.get_collection_of_object_values( + Parsable # type: ignore + # Bug. Should get a collection of primitive dictionary objects + ) + ) } def serialize(self, writer: SerializationWriter) -> None: diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 0c238b8d..5462d0f5 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -4,7 +4,7 @@ from kiota_abstractions.request_adapter import RequestAdapter from kiota_abstractions.request_information import RequestInformation from kiota_abstractions.method import Method -from kiota_abstractions.serialization import Parsable +from kiota_abstractions.serialization import Parsable, ParsableFactory from kiota_abstractions.headers_collection import HeadersCollection from kiota_abstractions.api_error import APIError @@ -26,7 +26,7 @@ class BatchRequestBuilder: def __init__( self, request_adapter: RequestAdapter, - error_map: Optional[Dict[str, Type[Parsable]]] = None + error_map: Optional[Dict[str, Type[ParsableFactory]]] = None ): if request_adapter is None: raise ValueError("request_adapter cannot be Null.") @@ -37,20 +37,19 @@ def __init__( async def post( self, batch_request_content: Union[BatchRequestContent, BatchRequestContentCollection], - error_map: Optional[Dict[str, Type[Parsable]]] = None, - ) -> Union[T, BatchResponseContentCollection]: + error_map: Optional[Dict[str, Type[ParsableFactory]]] = None, + ) -> Union[BatchResponseContent, BatchResponseContentCollection]: """ Sends a batch request and returns the batch response content. - + Args: - batch_request_content (Union[BatchRequestContent, + batch_request_content (Union[BatchRequestContent, BatchRequestContentCollection]): The batch request content. - response_type: Optional[Type[T]] : The type to deserialize the response into. - Optional[Dict[str, Type[Parsable]]] = None: + Optional[Dict[str, Type[ParsableFactory]]] = None: Error mappings for response handling. Returns: - Union[T, BatchResponseContentCollection]: The batch response content + Union[BatchResponseContent, BatchResponseContentCollection]: The batch response content or the specified response type. """ @@ -60,11 +59,6 @@ async def post( if isinstance(batch_request_content, BatchRequestContent): request_info = await self.to_post_request_information(batch_request_content) - bytes_content = request_info.content - json_content = bytes_content.decode("utf-8") - updated_str = '{"requests":' + json_content + '}' - updated_bytes = updated_str.encode("utf-8") - request_info.content = updated_bytes error_map = error_map or self.error_map response = None try: @@ -87,15 +81,15 @@ async def post( async def _post_batch_collection( self, batch_request_content_collection: BatchRequestContentCollection, - error_map: Optional[Dict[str, Type[Parsable]]] = None, + error_map: Optional[Dict[str, Type[ParsableFactory]]] = None, ) -> BatchResponseContentCollection: """ Sends a collection of batch requests and returns a collection of batch response contents. - + Args: - batch_request_content_collection (BatchRequestContentCollection): The + batch_request_content_collection (BatchRequestContentCollection): The collection of batch request contents. - Optional[Dict[str, Type[Parsable]]] = None: + Optional[Dict[str, Type[ParsableFactory]]] = None: Error mappings for response handling. Returns: @@ -108,7 +102,8 @@ async def _post_batch_collection( batch_requests = batch_request_content_collection.get_batch_requests_for_execution() for batch_request_content in batch_requests: response = await self.post(batch_request_content, error_map) - batch_responses.add_response(response) + if isinstance(response, BatchResponseContent): + batch_responses.add_response(response) return batch_responses @@ -117,7 +112,7 @@ async def to_post_request_information( ) -> RequestInformation: """ Creates request information for a batch POST request. - + Args: batch_request_content (BatchRequestContent): The batch request content. @@ -127,7 +122,6 @@ async def to_post_request_information( if batch_request_content is None: raise ValueError("batch_request_content cannot be Null.") - batch_request_items = list(batch_request_content.requests.values()) request_info = RequestInformation() request_info.http_method = Method.POST @@ -135,7 +129,7 @@ async def to_post_request_information( request_info.headers = HeadersCollection() request_info.headers.try_add("Content-Type", APPLICATION_JSON) request_info.set_content_from_parsable( - self._request_adapter, APPLICATION_JSON, batch_request_items + self._request_adapter, APPLICATION_JSON, batch_request_content ) return request_info diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index 8e99d69c..67d1fea3 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -1,5 +1,6 @@ import uuid from typing import List, Dict, Union, Optional +from urllib.request import Request from kiota_abstractions.request_information import RequestInformation from kiota_abstractions.serialization import Parsable, ParseNode @@ -15,20 +16,27 @@ class BatchRequestContent(Parsable): MAX_REQUESTS = 20 - def __init__(self, requests: Dict[str, Union['BatchRequestItem', 'RequestInformation']] = {}): + def __init__(self, requests: Dict[str, Union[BatchRequestItem, RequestInformation]] = {}): """ Initializes a new instance of the BatchRequestContent class. + Args: + Requests (Dict[str, Union[BatchRequestItem, RequestInformation]]): The requests to add. """ - self._requests: Dict[str, Union[BatchRequestItem, 'RequestInformation']] = requests or {} + self._requests: Dict[str, BatchRequestItem] = {} self.is_finalized = False for request_id, request in requests.items(): + if isinstance(request, RequestInformation): + self.add_request_information(request, request_id) + continue self.add_request(request_id, request) @property - def requests(self) -> Dict: + def requests(self) -> Dict[str, BatchRequestItem]: """ Gets the requests. + Returns: + Dict[str, BatchRequestItem]: requests in the batch request content. """ return self._requests @@ -36,6 +44,8 @@ def requests(self) -> Dict: def requests(self, requests: List[BatchRequestItem]) -> None: """ Sets the requests. + Args: + requests (List[BatchRequestItem]): The requests to set. """ if len(requests) >= BatchRequestContent.MAX_REQUESTS: raise ValueError(f"Maximum number of requests is {BatchRequestContent.MAX_REQUESTS}") @@ -45,43 +55,56 @@ def requests(self, requests: List[BatchRequestItem]) -> None: def add_request(self, request_id: Optional[str], request: BatchRequestItem) -> None: """ Adds a request to the batch request content. + Args: + request_id (Optional[str]): The request id to add. + request (BatchRequestItem): The request to add. """ if len(self.requests) >= BatchRequestContent.MAX_REQUESTS: raise RuntimeError(f"Maximum number of requests is {BatchRequestContent.MAX_REQUESTS}") if not request.id: - request.id = str(uuid.uuid4()) + request.id = request_id if request_id else str(uuid.uuid4()) if hasattr(request, 'depends_on') and request.depends_on: for dependent_id in request.depends_on: - if dependent_id not in self.requests: - dependent_request = self._request_by_id(dependent_id) - if dependent_request: - self._requests[dependent_id] = dependent_request + if not self._request_by_id(dependent_id): + raise ValueError( + f""" + Request depends on request id: {dependent_id} + which was not found in requests. Add request id: {dependent_id} first""" + ) self._requests[request.id] = request - def add_request_information(self, request_information: RequestInformation) -> None: - """ + def add_request_information( + self, request_information: RequestInformation, request_id: Optional[str] = None + ) -> None: + """ Adds a request to the batch request content. Args: request_information (RequestInformation): The request information to add. + request_id: Optional[str]: The request id to add. """ - request_id = str(uuid.uuid4()) + request_id = request_id if request_id else str(uuid.uuid4()) self.add_request(request_id, BatchRequestItem(request_information)) - def add_urllib_request(self, request) -> None: + def add_urllib_request(self, request: Request, request_id: Optional[str] = None) -> None: """ Adds a request to the batch request content. + Args: + request (Request): The request to add. + request_id: Optional[str]: The request id to add. """ - request_id = str(uuid.uuid4()) + request_id = request_id if request_id else str(uuid.uuid4()) self.add_request(request_id, BatchRequestItem.create_with_urllib_request(request)) def remove(self, request_id: str) -> None: """ Removes a request from the batch request content. - Also removes the request from the depends_on list of + Also removes the request from the depends_on list of other requests. + Args: + request_id (str): The request id to remove. """ request_to_remove = None - for request in self.requests: + for request in self.requests.values(): if request.id == request_id: request_to_remove = request if hasattr(request, 'depends_on') and request.depends_on: @@ -108,12 +131,12 @@ def finalize(self): def _request_by_id(self, request_id: str) -> Optional[BatchRequestItem]: """ Finds a request by its ID. - + Args: request_id (str): The ID of the request to find. Returns: - The request with the given ID, or None if not found. + Optional[BatchRequestItem]: The request with the given ID, or None if not found. """ return self._requests.get(request_id) @@ -137,4 +160,4 @@ def serialize(self, writer: SerializationWriter) -> None: Args: writer: Serialization writer to use to serialize this model """ - writer.write_collection_of_object_values("requests", self.requests) + writer.write_collection_of_object_values("requests", list(self.requests.values())) diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py index fa67540d..1393035a 100644 --- a/src/msgraph_core/requests/batch_request_content_collection.py +++ b/src/msgraph_core/requests/batch_request_content_collection.py @@ -13,15 +13,15 @@ class BatchRequestContentCollection: def __init__(self) -> None: """ Initializes a new instance of the BatchRequestContentCollection class. - - + + """ self.max_requests_per_batch = BatchRequestContent.MAX_REQUESTS self.batches: List[BatchRequestContent] = [] self.current_batch: BatchRequestContent = BatchRequestContent() def add_batch_request_item(self, request: BatchRequestItem) -> None: - """ + """ Adds a request item to the collection. Args: request (BatchRequestItem): The request item to add. @@ -33,17 +33,15 @@ def add_batch_request_item(self, request: BatchRequestItem) -> None: self.batches.append(self.current_batch) def remove_batch_request_item(self, request_id: str) -> None: - """ + """ Removes a request item from the collection. Args: request_id (str): The ID of the request item to remove. """ for batch in self.batches: if request_id in batch.requests: - del batch.requests[request_id] + batch.remove(request_id) return - if request_id in self.current_batch.requests: - del self.current_batch.requests[request_id] def new_batch_with_failed_requests(self) -> Optional[BatchRequestContent]: """ @@ -55,9 +53,14 @@ def new_batch_with_failed_requests(self) -> Optional[BatchRequestContent]: batch_with_failed_responses: Optional[BatchRequestContent] = BatchRequestContent() for batch in self.batches: for request in batch.requests: - if request.status_code not in [200, 201, 202, 203, 204, 205, 206, 207, 208, 226]: + if request.status_code not in [ # type: ignore # Method should be deprecated + 200, 201, 202, 203, 204, 205, 206, 207, 208, 226 + ]: if batch_with_failed_responses is not None: - batch_with_failed_responses.add_request(request.id, request) + batch_with_failed_responses.add_request( + request.id, # type: ignore # Bug. Method should be deprecated + request # type: ignore + ) else: raise ValueError("batch_with_failed_responses is None") return batch_with_failed_responses @@ -68,9 +71,6 @@ def get_batch_requests_for_execution(self) -> List[BatchRequestContent]: Returns: List[BatchRequestContent]: The batch requests for execution. """ - # if not self.current_batch.is_finalized: - # self.current_batch.finalize() - # self.batches.append(self.current_batch) return self.batches def serialize(self, writer: SerializationWriter) -> None: @@ -80,5 +80,3 @@ def serialize(self, writer: SerializationWriter) -> None: writer: Serialization writer to use to serialize this model """ pass - # print(f"serializing {self.batches}") - # writer.write_collection_of_object_values("requests", self.batches) diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index 3bab3453..141f2cb5 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -9,6 +9,7 @@ from urllib.parse import urlparse from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders +from kiota_abstractions.method import Method from kiota_abstractions.request_information import RequestInformation from kiota_abstractions.serialization import Parsable from kiota_abstractions.serialization import SerializationWriter @@ -29,7 +30,7 @@ def __init__( id: str = "", depends_on: Optional[List[Union[str, 'BatchRequestItem']]] = [] ): - """ + """ Initializes a new instance of the BatchRequestItem class. Args: request_information (RequestInformation): The request information. @@ -44,7 +45,7 @@ def __init__( self._method = request_information.http_method.name else: self._method = request_information.http_method - self._headers = request_information.request_headers + self._headers: Optional[Dict[str, str]] = request_information.request_headers self._body = request_information.content self.url = request_information.url.replace('/users/me-token-to-replace', '/me', 1) self._depends_on: Optional[List[str]] = [] @@ -57,30 +58,38 @@ def create_with_urllib_request( id: str = "", depends_on: Optional[List[str]] = None ) -> 'BatchRequestItem': - """ + """ Creates a new instance of the BatchRequestItem class from a urllib request. Args: request (urllib.request.Request): The urllib request. id (str, optional): The ID of the request item. Defaults to "". depends_on (Optional[List[str]], optional): The IDs of the requests that this request depends on. Defaults to None. - Returns: + Returns: BatchRequestItem: A new instance of the BatchRequestItem class. """ request_info = RequestInformation() - request_info.http_method = request.get_method() + try: + request_info.http_method = Method[request.get_method().upper()] + except KeyError: + raise KeyError(f"Request Method: {request.get_method()} is invalid") + request_info.url = request.full_url request_info.headers = RequestHeaders() for key, value in request.headers.items(): request_info.headers.try_add(header_name=key, header_value=value) - request_info.content = request.data - return BatchRequestItem(request_info, id, depends_on) + request_info.content = request.data # type: ignore + return BatchRequestItem( + request_info, + id, + depends_on # type: ignore # union types not analysed correctly + ) def set_depends_on(self, requests: Optional[List[Union[str, 'BatchRequestItem']]]) -> None: """ Sets the IDs of the requests that this request depends on. Args: - requests (Optional[List[Union[str, BatchRequestItem]]): The + requests (Optional[List[Union[str, BatchRequestItem]]): The IDs of the requests that this request depends on. """ if requests: @@ -119,7 +128,7 @@ def set_url(self, url: str) -> None: @property def id(self) -> str: - """ + """ Gets the ID of the request item. Returns: str: The ID of the request item. @@ -136,11 +145,11 @@ def id(self, value: str) -> None: self._id = value @property - def headers(self) -> List[RequestHeaders]: + def headers(self) -> Optional[Dict[str, str]]: """ Gets the headers of the request item. Returns: - List[RequestHeaders]: The headers of the request item. + Optional[Dict[str, str]]: The headers of the request item. """ return self._headers @@ -151,15 +160,22 @@ def headers(self, headers: Dict[str, Union[List[str], str]]) -> None: Args: headers (Dict[str, Union[List[str], str]]): The headers of the request item. """ - self._headers.clear() - self._headers.update(headers) + if self._headers: + self._headers.clear() + else: + self._headers = {} + headers_collection = RequestHeaders() + for header, value in headers.items(): + headers_collection.add(header, value) + for key, values in headers_collection.get_all().items(): + self._headers[key] = ', '.join(values) @property - def body(self) -> None: + def body(self) -> Optional[bytes]: """ Gets the body of the request item. Returns: - None: The body of the request item. + Optional[bytes]: The body of the request item. """ return self._body @@ -170,7 +186,7 @@ def body(self, body: BytesIO) -> None: Args: body : (BytesIO): The body of the request item. """ - self._body = body + self._body = body.getvalue() @property def method(self) -> str: @@ -207,7 +223,7 @@ def create_from_discriminator_value( ) -> 'BatchRequestItem': """ Creates a new instance of the appropriate class based - on discriminator value param parse_node: The parse node + on discriminator value param parse_node: The parse node to use to read the discriminator value and create the object Returns: BatchRequestItem """ @@ -216,10 +232,10 @@ def create_from_discriminator_value( return BatchRequestItem() def get_field_deserializers(self) -> Dict[str, Any]: - """ + """ Gets the deserialization information for this object. Returns: - Dict[str, Any]: The deserialization information for + Dict[str, Any]: The deserialization information for this object where each entry is a property key with its deserialization callback. """ @@ -233,7 +249,7 @@ def get_field_deserializers(self) -> Dict[str, Any]: } def serialize(self, writer: SerializationWriter) -> None: - """ + """ Writes the objects properties to the current writer. Args: writer (SerializationWriter): The writer to write to. @@ -242,12 +258,19 @@ def serialize(self, writer: SerializationWriter) -> None: writer.write_str_value('method', self.method) writer.write_str_value('url', self.url) writer.write_collection_of_primitive_values('depends_on', self._depends_on) - headers = {key: ", ".join(val) for key, val in self._headers.items()} - writer.write_collection_of_object_values('headers', headers) + writer.write_collection_of_object_values( + 'headers', + self._headers # type: ignore # need method to serialize dicts + ) if self._body: json_object = json.loads(self._body) is_json_string = json_object and isinstance(json_object, dict) - writer.write_collection_of_object_values( - 'body', - json_object if is_json_string else base64.b64encode(self._body).decode('utf-8') - ) + # /$batch API expects JSON object or base 64 encoded value for the body + if is_json_string: + writer.write_collection_of_object_values( # type: ignore + # need method to serialize dicts + 'body', + json_object + ) + else: + writer.write_str_value('body', base64.b64encode(self._body).decode('utf-8')) diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index c7e78b54..1faf7d8a 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -1,15 +1,15 @@ -from typing import Optional, Dict, Type, TypeVar, Callable +from typing import Optional, Dict, Type, TypeVar, Callable, Union from io import BytesIO import base64 -from kiota_abstractions.serialization import Parsable +from kiota_abstractions.serialization import Parsable, ParsableFactory from kiota_abstractions.serialization import ParseNode from kiota_abstractions.serialization import ParseNodeFactoryRegistry from kiota_abstractions.serialization import SerializationWriter from .batch_response_item import BatchResponseItem -T = TypeVar('T', bound='Parsable') +T = TypeVar('T', bound=ParsableFactory) class BatchResponseContent(Parsable): @@ -20,10 +20,10 @@ def __init__(self) -> None: BatchResponseContent is a collection of BatchResponseItem items, each with a unique request ID. """ - self._responses: Optional[Dict[str, 'BatchResponseItem']] = {} + self._responses: Optional[Dict[str, BatchResponseItem]] = {} @property - def responses(self) -> Optional[Dict[str, 'BatchResponseItem']]: + def responses(self) -> Optional[Dict[str, BatchResponseItem]]: """ Get the responses in the collection :return: A dictionary of response IDs and their BatchResponseItem objects @@ -32,7 +32,7 @@ def responses(self) -> Optional[Dict[str, 'BatchResponseItem']]: return self._responses @responses.setter - def responses(self, responses: Optional[Dict[str, 'BatchResponseItem']]) -> None: + def responses(self, responses: Optional[Dict[str, BatchResponseItem]]) -> None: """ Set the responses in the collection :param responses: The responses to set in the collection @@ -44,7 +44,7 @@ def get_response_by_id( self, request_id: str, response_type: Optional[Type[T]] = None, - ) -> Optional['BatchResponseItem']: + ) -> Optional[Union[T, BatchResponseItem]]: """ Get a response by its request ID from the collection :param request_id: The request ID of the response to get @@ -55,7 +55,7 @@ def get_response_by_id( if self._responses is None: return None if response_type is not None: - return response_type.create_from_discriminator_value(self._responses.get(request_id)) + return self.response_body(request_id, response_type) return self._responses.get(request_id) def get_response_stream_by_id(self, request_id: str) -> Optional[BytesIO]: @@ -91,7 +91,7 @@ def get_response_status_codes(self) -> Dict[str, int]: return status_codes def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: - """ + """ Get the body of a response by its request ID from the collection :param request_id: The request ID of the response to get :type request_id: str @@ -129,27 +129,27 @@ def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: content_type, base64_decoded_body ) response.body = base64_decoded_body - return parse_node.get_object_value(type.create_from_discriminator_value) + return parse_node.get_object_value(type) except Exception: raise ValueError( f"Unable to deserialize batch response for request Id: {request_id} to {type}" ) def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: - """ + """ Gets the deserialization information for this object. :return: The deserialization information for this object :rtype: Dict[str, Callable[[ParseNode], None]] """ - return { - 'responses': - lambda n: setattr( - self, '_responses', - {item.id: item - for item in n.get_collection_of_object_values(BatchResponseItem)} - ) - } + def set_responses(n: ParseNode): + values = n.get_collection_of_object_values(BatchResponseItem) + if values: + setattr(self, '_responses', {item.id: item for item in values}) + else: + setattr(self, '_responses', {}) + + return {'responses': lambda n: set_responses(n)} def serialize(self, writer: SerializationWriter) -> None: """ @@ -159,7 +159,7 @@ def serialize(self, writer: SerializationWriter) -> None: if self._responses is not None: writer.write_collection_of_object_values('responses', list(self._responses.values())) else: - writer.write_collection_of_object_values('responses', []) # type: ignore + writer.write_collection_of_object_values('responses', []) @staticmethod def create_from_discriminator_value( diff --git a/src/msgraph_core/requests/batch_response_content_collection.py b/src/msgraph_core/requests/batch_response_content_collection.py index e30ff30d..2e70707b 100644 --- a/src/msgraph_core/requests/batch_response_content_collection.py +++ b/src/msgraph_core/requests/batch_response_content_collection.py @@ -23,7 +23,7 @@ def __init__(self) -> None: self._responses: List[BatchResponseContent] = [] def add_response(self, response: BatchResponseContent) -> None: - """ + """ Adds a response to the collection. Args: keys: The keys of the response to add. @@ -32,7 +32,7 @@ def add_response(self, response: BatchResponseContent) -> None: self._responses.append(response) def get_responses(self): - """ + """ Gets the responses in the collection. Returns: List[Tuple[str, BatchResponseContent]]: The responses in the collection. @@ -50,7 +50,7 @@ async def responses_status_codes(self) -> Dict[str, int]: for response in self._responses: if isinstance(response, BatchResponseItem): if response.id is not None: - status_codes[response.id] = response.status_code + status_codes[response.id] = response.status else: raise ValueError("Response ID cannot be None") else: @@ -58,7 +58,7 @@ async def responses_status_codes(self) -> Dict[str, int]: return status_codes def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: - """ + """ Gets the deserialization information for this object. :return: The deserialization information for this object where each entry is a property key with its deserialization callback. @@ -66,11 +66,8 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: """ return { 'responses': - lambda n: setattr( - self, "_responses", - n. - get_collection_of_object_values(BatchResponseItem.create_from_discriminator_value) - ) + lambda n: + setattr(self, "_responses", n.get_collection_of_object_values(BatchResponseItem)) } def serialize(self, writer: SerializationWriter) -> None: diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py index 91c39881..e4e207f2 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -1,11 +1,13 @@ -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, Callable from io import BytesIO +from deprecated import deprecated from kiota_abstractions.serialization import Parsable, ParsableFactory from kiota_abstractions.serialization import ParseNode from kiota_abstractions.serialization import SerializationWriter +@deprecated("Use BytesIO type instead") class StreamInterface(BytesIO): pass @@ -24,7 +26,7 @@ def __init__(self) -> None: @property def id(self) -> Optional[str]: - """ + """ Get the ID of the response :return: The ID of the response :rtype: Optional[str] @@ -69,7 +71,7 @@ def status(self) -> Optional[int]: @status.setter def status(self, status_code: Optional[int]) -> None: - """ + """ Set the status code of the response :param status_code: The status code of the response :type status_code: Optional[int] @@ -104,11 +106,11 @@ def body(self) -> Optional[BytesIO]: return self._body @body.setter - def body(self, body: Optional[StreamInterface]) -> None: + def body(self, body: Optional[BytesIO]) -> None: """ Set the body of the response :param body: The body of the response - :type body: Optional[StreamInterface] + :type body: Optional[BytesIO] """ self._body = body @@ -138,7 +140,7 @@ def create_from_discriminator_value( raise TypeError("parse_node cannot be null") return BatchResponseItem() - def get_field_deserializers(self) -> Dict[str, Any]: + def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: """ Gets the deserialization information for this object. @@ -146,7 +148,11 @@ def get_field_deserializers(self) -> Dict[str, Any]: return { "id": lambda x: setattr(self, "id", x.get_str_value()), "status": lambda x: setattr(self, "status", x.get_int_value()), - "headers": lambda x: setattr(self, "headers", x.try_get_anything(x._json_node)), + "headers": lambda x: setattr( + self, + "headers", + x.try_get_anything(x._json_node) # type: ignore + ), # need interface to return a dictionary "body": lambda x: setattr(self, "body", x.get_bytes_value()), } @@ -157,5 +163,11 @@ def serialize(self, writer: SerializationWriter) -> None: writer.write_str_value('id', self._id) writer.write_str_value('atomicity_group', self._atomicity_group) writer.write_int_value('status', self._status) - writer.write_collection_of_primitive_values('headers', self._headers) - writer.write_bytes_value('body', self._body) + writer.write_collection_of_primitive_values( + 'headers', + self._headers # type: ignore + ) # need method to serialize dicts + if self._body: + writer.write_bytes_value('body', self._body.getvalue()) + else: + writer.write_bytes_value('body', None) diff --git a/src/msgraph_core/tasks/large_file_upload.py b/src/msgraph_core/tasks/large_file_upload.py index 5450d540..61ca4d71 100644 --- a/src/msgraph_core/tasks/large_file_upload.py +++ b/src/msgraph_core/tasks/large_file_upload.py @@ -1,21 +1,21 @@ import os -from typing import Callable, Optional, List, Tuple, Any, Dict +from typing import Callable, Optional, List, Tuple, Any, Dict, TypeVar, Union, Type from io import BytesIO from asyncio import Future from datetime import datetime, timedelta, timezone import logging -from kiota_abstractions.serialization.parsable import Parsable from kiota_abstractions.method import Method from kiota_abstractions.headers_collection import HeadersCollection from kiota_abstractions.request_information import RequestInformation -from kiota_abstractions.serialization.additional_data_holder import AdditionalDataHolder -from kiota_abstractions.serialization.parsable_factory import ParsableFactory +from kiota_abstractions.serialization import Parsable, ParsableFactory, AdditionalDataHolder from kiota_abstractions.request_adapter import RequestAdapter from msgraph_core.models import LargeFileUploadSession, UploadResult # check imports +T = TypeVar('T', bound=Parsable) + # pylint: disable=too-many-instance-attributes class LargeFileUploadTask: @@ -25,7 +25,7 @@ def __init__( upload_session: Parsable, request_adapter: RequestAdapter, stream: BytesIO, - parsable_factory: Optional[ParsableFactory] = None, + parsable_factory: Optional[ParsableFactory[T]] = None, max_chunk_size: int = 5 * 1024 * 1024 ): self._upload_session = upload_session @@ -69,7 +69,7 @@ def upload_session_expired(self, upload_session: Optional[Parsable] = None) -> b upload_session = upload_session or self.upload_session if not hasattr(upload_session, "expiration_date_time"): raise ValueError("Upload session does not have an expiration date time") - expiry = upload_session.expiration_date_time + expiry = getattr(upload_session, 'expiration_date_time') if expiry is None: raise ValueError("Expiry is None") if isinstance(expiry, str): @@ -92,7 +92,7 @@ async def upload(self, after_chunk_upload: Optional[Callable] = None): raise RuntimeError('The upload session is expired.') self.on_chunk_upload_complete = after_chunk_upload or self.on_chunk_upload_complete - session = await self.next_chunk( + session: LargeFileUploadSession = await self.next_chunk( self.stream, 0, max(0, min(self.max_chunk_size - 1, self.file_size - 1)) ) process_next = session @@ -110,12 +110,15 @@ async def upload(self, after_chunk_upload: Optional[Callable] = None): response = await self.last_chunk(self.stream) try: - lfu_session: LargeFileUploadSession = session # type: ignore + lfu_session = session if lfu_session is None: continue - next_range = lfu_session.next_expected_ranges + next_range = None + if hasattr(lfu_session, 'next_expected_ranges'): + next_range = lfu_session.next_expected_ranges old_url = self.get_validated_upload_url(self.upload_session) - lfu_session.upload_url = old_url + if hasattr(lfu_session, 'upload_url'): + lfu_session.upload_url = old_url if self.on_chunk_upload_complete is not None: self.on_chunk_upload_complete(uploaded_range) if not next_range: @@ -132,7 +135,8 @@ async def upload(self, after_chunk_upload: Optional[Callable] = None): self.chunks -= 1 upload_result: UploadResult[Any] = UploadResult() upload_result.item_response = response - upload_result.location = self.upload_session.upload_url + if hasattr(self.upload_session, 'upload_url'): + upload_result.location = self.upload_session.upload_url return upload_result @property @@ -143,7 +147,9 @@ def next_range(self): def next_range(self, value: Optional[str]) -> None: self._next_range = value - async def next_chunk(self, file: BytesIO, range_start: int = 0, range_end: int = 0) -> Future: + async def next_chunk( + self, file: BytesIO, range_start: int = 0, range_end: int = 0 + ) -> LargeFileUploadSession: upload_url = self.get_validated_upload_url(self.upload_session) if not upload_url: raise ValueError('The upload session URL must not be empty.') @@ -175,16 +181,15 @@ async def next_chunk(self, file: BytesIO, range_start: int = 0, range_end: int = info.headers.try_add("Content-Type", "application/octet-stream") info.set_stream_content(bytes(chunk_data)) error_map: Dict[str, int] = {} - parsable_factory = LargeFileUploadSession - return await self.request_adapter.send_async(info, parsable_factory, error_map) + return await self.request_adapter.send_async(info, LargeFileUploadSession, error_map) async def last_chunk( self, file: BytesIO, range_start: int = 0, range_end: int = 0, - parsable_factory: Optional[ParsableFactory] = None - ) -> Future: + parsable_factory: Optional[ParsableFactory[T]] = None + ) -> Optional[Union[T, bytes]]: upload_url = self.get_validated_upload_url(self.upload_session) if not upload_url: raise ValueError('The upload session URL must not be empty.') @@ -216,13 +221,15 @@ async def last_chunk( info.headers.try_add("Content-Type", "application/octet-stream") info.set_stream_content(bytes(chunk_data)) error_map: Dict[str, int] = {} - parsable_factory = self.factory or parsable_factory - return await self.request_adapter.send_async(info, parsable_factory, error_map) + factory = self.factory or parsable_factory + if factory: + return await self.request_adapter.send_async(info, factory, error_map) + return await self.request_adapter.send_primitive_async(info, "bytes", error_map) def get_file(self) -> BytesIO: return self.stream - async def cancel(self) -> Optional[Future]: + async def cancel(self) -> Parsable: upload_url = self.get_validated_upload_url(self.upload_session) request_information = RequestInformation(method=Method.DELETE, url_template=upload_url) @@ -242,10 +249,12 @@ def additional_data_contains(self, parsable: Parsable, property_candidates: List[str]) -> Tuple[bool, Any]: if not issubclass(type(parsable), AdditionalDataHolder): raise ValueError( - f'The object passed does not contain property/properties ' + 'The object passed does not contain property/properties ' f'{",".join(property_candidates)} and does not implement ' - f'AdditionalDataHolder' + 'AdditionalDataHolder' ) + if not hasattr(parsable, 'additional_data'): + raise ValueError('The object passed does not contain an additional_data property') additional_data = parsable.additional_data for property_candidate in property_candidates: if property_candidate in additional_data: diff --git a/src/msgraph_core/tasks/page_iterator.py b/src/msgraph_core/tasks/page_iterator.py index f3b5a314..5f32855f 100644 --- a/src/msgraph_core/tasks/page_iterator.py +++ b/src/msgraph_core/tasks/page_iterator.py @@ -17,7 +17,7 @@ and models modules. """ -from typing import Callable, Optional, Union, Dict, List +from typing import Callable, Optional, Union, Dict, Type from typing import TypeVar from requests.exceptions import InvalidURL @@ -26,7 +26,7 @@ from kiota_abstractions.method import Method from kiota_abstractions.headers_collection import HeadersCollection from kiota_abstractions.request_information import RequestInformation -from kiota_abstractions.serialization.parsable import Parsable +from kiota_abstractions.serialization import Parsable, ParsableFactory from msgraph_core.models.page_result import PageResult # pylint: disable=no-name-in-module, import-error @@ -59,12 +59,13 @@ def __init__( self, response: Union[T, list, object], request_adapter: RequestAdapter, - constructor_callable: Optional[Callable] = None + constructor_callable: Optional[Callable] = None, + error_mapping: Optional[Dict[str, Type[ParsableFactory]]] = None, ): self.request_adapter = request_adapter if isinstance(response, Parsable) and not constructor_callable: - parsable_factory = type(response) + parsable_factory: Type[Parsable] = type(response) elif constructor_callable is None: parsable_factory = PageResult else: @@ -74,7 +75,7 @@ def __init__( self.parsable_factory = parsable_factory self.pause_index = 0 self.headers: HeadersCollection = HeadersCollection() - self.request_options = [] # type: ignore + self.request_options: list = [] self.current_page = self.convert_to_page(response) self.object_type = self.current_page.value[ 0].__class__.__name__ if self.current_page.value else None @@ -89,6 +90,7 @@ def __init__( if page is not None: self.current_page = page self.has_next = bool(page.odata_next_link) + self.error_mapping = error_mapping if error_mapping else {} def set_headers(self, headers: dict) -> HeadersCollection: """ @@ -100,6 +102,7 @@ def set_headers(self, headers: dict) -> HeadersCollection: header names and the values are the header values. """ self.headers.add_all(**headers) + return self.headers @property def delta_link(self): @@ -145,8 +148,11 @@ async def next(self) -> Optional[PageResult]: if self.current_page is not None and not self.current_page.odata_next_link: return None response = await self.fetch_next_page() - page: PageResult = PageResult(response.odata_next_link, response.value) # type: ignore - return page + next_link = response.odata_next_link if response and hasattr( + response, 'odata_next_link' + ) else None + value = response.value if response and hasattr(response, 'value') else None + return PageResult(next_link, value) @staticmethod def convert_to_page(response: Union[T, list, object]) -> PageResult: @@ -166,9 +172,9 @@ def convert_to_page(response: Union[T, list, object]) -> PageResult: raise ValueError('Response cannot be null.') value = None if isinstance(response, list): - value = response.value # type: ignore + value = response elif hasattr(response, 'value'): - value = getattr(response, 'value') + value = response.value elif isinstance(response, object): value = getattr(response, 'value', []) if value is None: @@ -178,10 +184,9 @@ def convert_to_page(response: Union[T, list, object]) -> PageResult: parsable_page, dict ) else getattr(parsable_page, 'odata_next_link', '') - page: PageResult = PageResult(next_link, value) - return page + return PageResult(next_link, value) - async def fetch_next_page(self) -> List[Parsable]: + async def fetch_next_page(self) -> Optional[Union[T, PageResult]]: """ Fetches the next page of items from the server. Returns: @@ -202,11 +207,11 @@ async def fetch_next_page(self) -> List[Parsable]: request_info.headers = self.headers if self.request_options: request_info.add_request_options(*self.request_options) - error_map: Dict[str, int] = {} - response = await self.request_adapter.send_async( - request_info, self.parsable_factory, error_map + return await self.request_adapter.send_async( + request_info, + self.parsable_factory, # type: ignore + self.error_mapping ) - return response def enumerate(self, callback: Optional[Callable] = None) -> bool: """ diff --git a/tests/requests/test_batch_request_content.py b/tests/requests/test_batch_request_content.py index 3ee241af..43d8e2fb 100644 --- a/tests/requests/test_batch_request_content.py +++ b/tests/requests/test_batch_request_content.py @@ -107,5 +107,5 @@ def test_serialize(batch_request_content): writer = Mock(spec=SerializationWriter) batch_request_content.serialize(writer) writer.write_collection_of_object_values.assert_called_once_with( - "requests", batch_request_content.requests + "requests", list(batch_request_content.requests.values()) ) diff --git a/tests/requests/test_batch_request_item.py b/tests/requests/test_batch_request_item.py index 2dd3d863..2aeeffb4 100644 --- a/tests/requests/test_batch_request_item.py +++ b/tests/requests/test_batch_request_item.py @@ -96,13 +96,13 @@ def test_id_property(batch_request_item): def test_headers_property(batch_request_item): new_headers = {"Authorization": "Bearer token"} batch_request_item.headers = new_headers - assert batch_request_item.headers["Authorization"] == "Bearer token" + assert batch_request_item.headers["authorization"] == "Bearer token" def test_body_property(batch_request_item): new_body = StreamInterface(b'{"new_key": "new_value"}') batch_request_item.body = new_body - assert batch_request_item.body.read() == b'{"new_key": "new_value"}' + assert batch_request_item.body == b'{"new_key": "new_value"}' def test_method_property(batch_request_item):