From 2a2b2d2d79184d87a564e4cb297843e5812468f4 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Thu, 5 Dec 2024 11:53:12 +0300 Subject: [PATCH 01/25] chore: bump kiota dependencies --- requirements-dev.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 047e8b2e..130ed4ca 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -143,11 +143,11 @@ 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' From 7b0c51b9249644aab3d7a5c27d3fffbdf94111cc Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Tue, 10 Dec 2024 17:01:55 +0300 Subject: [PATCH 02/25] enable static checks for missing imports --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) 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" From 33bdf40c1054b82129e7a8a1bc5835ed1f6a31d5 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Wed, 11 Dec 2024 09:50:42 +0300 Subject: [PATCH 03/25] Expose error mappings in page iterator and ignore errors due to undefined behaviour --- src/msgraph_core/models/page_result.py | 2 +- src/msgraph_core/tasks/page_iterator.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/msgraph_core/models/page_result.py b/src/msgraph_core/models/page_result.py index 7effd54e..f1ea1928 100644 --- a/src/msgraph_core/models/page_result.py +++ b/src/msgraph_core/models/page_result.py @@ -47,7 +47,7 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: """ 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)) + "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/tasks/page_iterator.py b/src/msgraph_core/tasks/page_iterator.py index f3b5a314..dbd469a2 100644 --- a/src/msgraph_core/tasks/page_iterator.py +++ b/src/msgraph_core/tasks/page_iterator.py @@ -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(response) # type: ignore elif constructor_callable is None: parsable_factory = PageResult else: @@ -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 dict() 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): @@ -181,7 +184,7 @@ def convert_to_page(response: Union[T, list, object]) -> PageResult: page: PageResult = PageResult(next_link, value) return page - 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,9 +205,8 @@ 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 + request_info, self.parsable_factory, self.error_mapping # type: ignore ) return response From 062180414a7f150de28edcc849f867115a2fa92c Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Wed, 11 Dec 2024 23:07:47 +0300 Subject: [PATCH 04/25] Resolve large file upload type issues --- .../models/large_file_upload_session.py | 10 +++++----- src/msgraph_core/tasks/large_file_upload.py | 15 +++++++-------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/msgraph_core/models/large_file_upload_session.py b/src/msgraph_core/models/large_file_upload_session.py index 4d5ee4f1..eb574bce 100644 --- a/src/msgraph_core/models/large_file_upload_session.py +++ b/src/msgraph_core/models/large_file_upload_session.py @@ -1,15 +1,15 @@ from __future__ import annotations -from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING, Union +from typing import Any, Callable, Dict, List, Optional, Protocol, TypeVar, Generic import datetime from dataclasses import dataclass, field from kiota_abstractions.serialization import ( - AdditionalDataHolder, Parsable, ParseNode, SerializationWriter + ParseNode, SerializationWriter, ParsableFactory ) @dataclass -class LargeFileUploadSession(AdditionalDataHolder, Parsable): +class LargeFileUploadSession(Protocol, ParsableFactory): additional_data: Dict[str, Any] = field(default_factory=dict) expiration_date_time: Optional[datetime.datetime] = None @@ -25,13 +25,13 @@ 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 """ if not parse_node: raise TypeError("parse_node cannot be null.") - return LargeFileUploadSession() + return LargeFileUploadSession() # type: ignore # Breaking change to remove this method since a Protocol cannot instantiate itself def get_field_deserializers(self, ) -> Dict[str, Callable[[ParseNode], None]]: """ diff --git a/src/msgraph_core/tasks/large_file_upload.py b/src/msgraph_core/tasks/large_file_upload.py index 5450d540..e36193cc 100644 --- a/src/msgraph_core/tasks/large_file_upload.py +++ b/src/msgraph_core/tasks/large_file_upload.py @@ -5,7 +5,6 @@ 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 @@ -22,7 +21,7 @@ class LargeFileUploadTask: def __init__( self, - upload_session: Parsable, + upload_session: LargeFileUploadSession, request_adapter: RequestAdapter, stream: BytesIO, parsable_factory: Optional[ParsableFactory] = None, @@ -64,7 +63,7 @@ def chunks(self): def chunks(self, value): self._chunks = value - def upload_session_expired(self, upload_session: Optional[Parsable] = None) -> bool: + def upload_session_expired(self, upload_session: Optional[LargeFileUploadSession] = None) -> bool: now = datetime.now(timezone.utc) upload_session = upload_session or self.upload_session if not hasattr(upload_session, "expiration_date_time"): @@ -110,7 +109,7 @@ 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 @@ -143,7 +142,7 @@ 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) -> Optional[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.') @@ -238,7 +237,7 @@ async def cancel(self) -> Optional[Future]: return self.upload_session - def additional_data_contains(self, parsable: Parsable, + def additional_data_contains(self, parsable: LargeFileUploadSession, property_candidates: List[str]) -> Tuple[bool, Any]: if not issubclass(type(parsable), AdditionalDataHolder): raise ValueError( @@ -253,7 +252,7 @@ def additional_data_contains(self, parsable: Parsable, return False, None def check_value_exists( - self, parsable: Parsable, attribute_name: str, property_names_in_additional_data: List[str] + self, parsable: LargeFileUploadSession, attribute_name: str, property_names_in_additional_data: List[str] ) -> Tuple[bool, Any]: checked_additional_data = self.additional_data_contains( parsable, property_names_in_additional_data @@ -287,7 +286,7 @@ async def resume(self) -> Future: self.next_range = next_range return await self.upload() - def get_validated_upload_url(self, upload_session: Parsable) -> str: + def get_validated_upload_url(self, upload_session: LargeFileUploadSession) -> str: if not hasattr(upload_session, 'upload_url'): raise RuntimeError('The upload session does not contain a valid upload url') result = upload_session.upload_url From d6785549383286af80d56e9b14742f95fdcc36b2 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Wed, 11 Dec 2024 23:10:23 +0300 Subject: [PATCH 05/25] Fix typing issue in auth provider --- .../authentication/azure_identity_authentication_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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] From b9b704fd6f7ecb20d8b021aa45ee9cff89ba027c Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Wed, 11 Dec 2024 23:15:40 +0300 Subject: [PATCH 06/25] Resolve errors in graph client factory --- src/msgraph_core/graph_client_factory.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/msgraph_core/graph_client_factory.py b/src/msgraph_core/graph_client_factory.py index 39344715..9578dcb4 100644 --- a/src/msgraph_core/graph_client_factory.py +++ b/src/msgraph_core/graph_client_factory.py @@ -22,7 +22,7 @@ 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 +53,7 @@ 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 +91,7 @@ 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() From 9d48700452445851a35f31c67c822eac916f315a Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Fri, 13 Dec 2024 11:55:13 +0300 Subject: [PATCH 07/25] Fix batch request item type hint issues --- .../requests/batch_request_item.py | 67 ++++++++++++------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index 3bab3453..c076be00 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,34 @@ 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 +124,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 +141,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 +156,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 +182,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 +219,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 +228,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 +245,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 +254,15 @@ 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')) From 3c1d99804bd69f740f5d9ad4d40f1481f8666094 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Fri, 13 Dec 2024 12:43:15 +0300 Subject: [PATCH 08/25] Fix batch request content issues --- .../requests/batch_request_content.py | 64 ++++++++++++------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index 8e99d69c..bee18aae 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -5,6 +5,8 @@ from kiota_abstractions.serialization import Parsable, ParseNode from kiota_abstractions.serialization import SerializationWriter +from urllib.request import Request + from .batch_request_item import BatchRequestItem @@ -15,20 +17,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 +45,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,49 +56,54 @@ 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: - if request.id == request_id: - request_to_remove = request - if hasattr(request, 'depends_on') and request.depends_on: - if request_id in request.depends_on: - request.depends_on.remove(request_id) + request_to_remove = self._request_by_id(request_id) if request_to_remove: + if hasattr(request_to_remove, 'depends_on') and request_to_remove.depends_on: + for dependent_id in request_to_remove.depends_on: + if self._request_by_id(dependent_id): + del self._requests[dependent_id] del self._requests[request_to_remove.id] else: raise ValueError(f"Request ID {request_id} not found in requests.") @@ -108,12 +124,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 +153,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())) From 2e77ff79036df9b7d9f39097c48bfa6b35a92505 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Fri, 13 Dec 2024 12:55:10 +0300 Subject: [PATCH 09/25] fix batch response item issues --- .../requests/batch_response_item.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py index 91c39881..d9d21590 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, Callable from io import BytesIO from kiota_abstractions.serialization import Parsable, ParsableFactory @@ -6,10 +6,6 @@ from kiota_abstractions.serialization import SerializationWriter -class StreamInterface(BytesIO): - pass - - class BatchResponseItem(Parsable): def __init__(self) -> None: @@ -24,7 +20,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 +65,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,7 +100,7 @@ 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 @@ -138,7 +134,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 +142,7 @@ 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 +153,8 @@ 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) From ec2cc7eecf98b8d47d3d613819e650e3b0b06746 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Fri, 13 Dec 2024 20:59:24 +0300 Subject: [PATCH 10/25] Fix batch response content type issues --- .../requests/batch_response_content.py | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index c7e78b54..2ecd945a 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,26 +129,30 @@ 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]] """ + 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: setattr( - self, '_responses', - {item.id: item - for item in n.get_collection_of_object_values(BatchResponseItem)} - ) + lambda n: set_responses(n) } def serialize(self, writer: SerializationWriter) -> None: @@ -159,7 +163,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( From 5b0e0e7eb1f1a5ae62f834d6a4affb9be77e73c9 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Fri, 13 Dec 2024 21:02:26 +0300 Subject: [PATCH 11/25] fix batch response content collection --- .../requests/batch_response_content_collection.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/msgraph_core/requests/batch_response_content_collection.py b/src/msgraph_core/requests/batch_response_content_collection.py index e30ff30d..b071f068 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. @@ -68,8 +68,7 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: 'responses': lambda n: setattr( self, "_responses", - n. - get_collection_of_object_values(BatchResponseItem.create_from_discriminator_value) + n.get_collection_of_object_values(BatchResponseItem) ) } From 210c174c6f95709722b39f818b23ad65a1e17031 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Fri, 13 Dec 2024 21:25:00 +0300 Subject: [PATCH 12/25] undo changes to batch request content remove() --- src/msgraph_core/requests/batch_request_content.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index bee18aae..9c61f36d 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -98,12 +98,14 @@ def remove(self, request_id: str) -> None: Args: request_id (str): The request id to remove. """ - request_to_remove = self._request_by_id(request_id) + request_to_remove = None + for request in self.requests: + if request.id == request_id: + request_to_remove = request + if hasattr(request, 'depends_on') and request.depends_on: + if request_id in request.depends_on: + request.depends_on.remove(request_id) if request_to_remove: - if hasattr(request_to_remove, 'depends_on') and request_to_remove.depends_on: - for dependent_id in request_to_remove.depends_on: - if self._request_by_id(dependent_id): - del self._requests[dependent_id] del self._requests[request_to_remove.id] else: raise ValueError(f"Request ID {request_id} not found in requests.") From d83c1ddbe06dfb8e41fab1a2840efebf123f9bd7 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Fri, 13 Dec 2024 21:26:21 +0300 Subject: [PATCH 13/25] fix issues in batch request content collection --- .../batch_request_content_collection.py | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py index fa67540d..eb9b09ec 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,9 @@ 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 [200, 201, 202, 203, 204, 205, 206, 207, 208, 226]: # type: ignore # Method should be deprecated 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, request) # type: ignore # Bug. Method should be deprecated else: raise ValueError("batch_with_failed_responses is None") return batch_with_failed_responses @@ -68,9 +66,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 +75,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) From 85e8935971adef13d4f5d1e55970c570ad267dda Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Fri, 13 Dec 2024 21:46:34 +0300 Subject: [PATCH 14/25] fix: Fixes type hints and failing mypy checks --- .../requests/batch_request_builder.py | 38 ++++++++----------- .../requests/batch_request_content.py | 2 +- 2 files changed, 17 insertions(+), 23 deletions(-) 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 9c61f36d..e0f8d60d 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -99,7 +99,7 @@ def remove(self, request_id: str) -> None: 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: From e861019f82d0e931537bbcfa2b7455b673761992 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Fri, 13 Dec 2024 23:14:15 +0300 Subject: [PATCH 15/25] Undo breaking type changes in LFU and fix errors --- .../models/large_file_upload_session.py | 8 +-- src/msgraph_core/tasks/large_file_upload.py | 50 +++++++++++-------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/msgraph_core/models/large_file_upload_session.py b/src/msgraph_core/models/large_file_upload_session.py index eb574bce..5faf30cd 100644 --- a/src/msgraph_core/models/large_file_upload_session.py +++ b/src/msgraph_core/models/large_file_upload_session.py @@ -1,15 +1,15 @@ from __future__ import annotations -from typing import Any, Callable, Dict, List, Optional, Protocol, TypeVar, Generic +from typing import Any, Callable, Dict, List, Optional import datetime from dataclasses import dataclass, field from kiota_abstractions.serialization import ( - ParseNode, SerializationWriter, ParsableFactory + AdditionalDataHolder, Parsable, ParseNode, SerializationWriter ) @dataclass -class LargeFileUploadSession(Protocol, ParsableFactory): +class LargeFileUploadSession(AdditionalDataHolder, Parsable): additional_data: Dict[str, Any] = field(default_factory=dict) expiration_date_time: Optional[datetime.datetime] = None @@ -31,7 +31,7 @@ def create_from_discriminator_value( """ if not parse_node: raise TypeError("parse_node cannot be null.") - return LargeFileUploadSession() # type: ignore # Breaking change to remove this method since a Protocol cannot instantiate itself + return LargeFileUploadSession() def get_field_deserializers(self, ) -> Dict[str, Callable[[ParseNode], None]]: """ diff --git a/src/msgraph_core/tasks/large_file_upload.py b/src/msgraph_core/tasks/large_file_upload.py index e36193cc..482576c9 100644 --- a/src/msgraph_core/tasks/large_file_upload.py +++ b/src/msgraph_core/tasks/large_file_upload.py @@ -1,5 +1,5 @@ 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 @@ -8,23 +8,23 @@ 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: def __init__( self, - upload_session: LargeFileUploadSession, + 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 @@ -63,12 +63,12 @@ def chunks(self): def chunks(self, value): self._chunks = value - def upload_session_expired(self, upload_session: Optional[LargeFileUploadSession] = None) -> bool: + def upload_session_expired(self, upload_session: Optional[Parsable] = None) -> bool: now = datetime.now(timezone.utc) 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): @@ -91,7 +91,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 @@ -112,9 +112,11 @@ async def upload(self, after_chunk_upload: Optional[Callable] = None): lfu_session = session if lfu_session is None: continue - next_range = lfu_session.next_expected_ranges + 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: @@ -131,7 +133,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 @@ -142,7 +145,7 @@ 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) -> Optional[LargeFileUploadSession]: + 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.') @@ -174,16 +177,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.') @@ -215,13 +217,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) @@ -237,7 +241,7 @@ async def cancel(self) -> Optional[Future]: return self.upload_session - def additional_data_contains(self, parsable: LargeFileUploadSession, + def additional_data_contains(self, parsable: Parsable, property_candidates: List[str]) -> Tuple[bool, Any]: if not issubclass(type(parsable), AdditionalDataHolder): raise ValueError( @@ -245,6 +249,8 @@ def additional_data_contains(self, parsable: LargeFileUploadSession, f'{",".join(property_candidates)} and does not implement ' f'AdditionalDataHolder' ) + if not hasattr(parsable, 'additional_data'): + raise ValueError(f'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: @@ -252,7 +258,7 @@ def additional_data_contains(self, parsable: LargeFileUploadSession, return False, None def check_value_exists( - self, parsable: LargeFileUploadSession, attribute_name: str, property_names_in_additional_data: List[str] + self, parsable: Parsable, attribute_name: str, property_names_in_additional_data: List[str] ) -> Tuple[bool, Any]: checked_additional_data = self.additional_data_contains( parsable, property_names_in_additional_data @@ -286,7 +292,7 @@ async def resume(self) -> Future: self.next_range = next_range return await self.upload() - def get_validated_upload_url(self, upload_session: LargeFileUploadSession) -> str: + def get_validated_upload_url(self, upload_session: Parsable) -> str: if not hasattr(upload_session, 'upload_url'): raise RuntimeError('The upload session does not contain a valid upload url') result = upload_session.upload_url From f3632a95f5fea7f76c1c3fd441289195b287c55b Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Fri, 13 Dec 2024 23:56:57 +0300 Subject: [PATCH 16/25] clean up page iterator fixes --- src/msgraph_core/tasks/page_iterator.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/msgraph_core/tasks/page_iterator.py b/src/msgraph_core/tasks/page_iterator.py index dbd469a2..5e604c62 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 @@ -65,7 +65,7 @@ def __init__( self.request_adapter = request_adapter if isinstance(response, Parsable) and not constructor_callable: - parsable_factory = type(response) # type: ignore + parsable_factory: Type[Parsable] = type(response) elif constructor_callable is None: parsable_factory = PageResult else: @@ -75,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 @@ -148,8 +148,9 @@ 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: @@ -169,9 +170,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: @@ -181,8 +182,7 @@ 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) -> Optional[Union[T, PageResult]]: """ @@ -205,10 +205,9 @@ async def fetch_next_page(self) -> Optional[Union[T, PageResult]]: request_info.headers = self.headers if self.request_options: request_info.add_request_options(*self.request_options) - response = await self.request_adapter.send_async( + return await self.request_adapter.send_async( request_info, self.parsable_factory, self.error_mapping # type: ignore ) - return response def enumerate(self, callback: Optional[Callable] = None) -> bool: """ From 92efc9cfb040e08506e08ea0289e0f40e7eb7c9b Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Sat, 14 Dec 2024 00:04:42 +0300 Subject: [PATCH 17/25] fix formatting issues --- src/msgraph_core/graph_client_factory.py | 4 +++- src/msgraph_core/models/page_result.py | 12 ++++++++++-- src/msgraph_core/requests/batch_request_content.py | 8 ++++++-- .../requests/batch_request_content_collection.py | 9 +++++++-- src/msgraph_core/requests/batch_request_item.py | 13 ++++++++++--- src/msgraph_core/requests/batch_response_content.py | 6 +----- .../requests/batch_response_content_collection.py | 6 ++---- src/msgraph_core/requests/batch_response_item.py | 11 +++++++++-- src/msgraph_core/tasks/large_file_upload.py | 5 ++++- src/msgraph_core/tasks/page_iterator.py | 8 ++++++-- 10 files changed, 58 insertions(+), 24 deletions(-) diff --git a/src/msgraph_core/graph_client_factory.py b/src/msgraph_core/graph_client_factory.py index 9578dcb4..5f56971a 100644 --- a/src/msgraph_core/graph_client_factory.py +++ b/src/msgraph_core/graph_client_factory.py @@ -91,7 +91,9 @@ def _get_telemetry_handler( options""" if options: - graph_telemetry_options: GraphTelemetryHandlerOption = options.get(GraphTelemetryHandlerOption().get_key()) # type: ignore # Unable to down cast type + 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/page_result.py b/src/msgraph_core/models/page_result.py index f1ea1928..60f72d74 100644 --- a/src/msgraph_core/models/page_result.py +++ b/src/msgraph_core/models/page_result.py @@ -46,8 +46,16 @@ 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)) # type: ignore # Bug. Should get a collection of primitive dictionary objects + "@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_content.py b/src/msgraph_core/requests/batch_request_content.py index e0f8d60d..712d5abc 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -67,10 +67,14 @@ def add_request(self, request_id: Optional[str], request: BatchRequestItem) -> N if hasattr(request, 'depends_on') and request.depends_on: for dependent_id in request.depends_on: 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") + 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, request_id: Optional[str] = None) -> None: + def add_request_information( + self, request_information: RequestInformation, request_id: Optional[str] = None + ) -> None: """ Adds a request to the batch request content. Args: diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py index eb9b09ec..1393035a 100644 --- a/src/msgraph_core/requests/batch_request_content_collection.py +++ b/src/msgraph_core/requests/batch_request_content_collection.py @@ -53,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]: # type: ignore # Method should be deprecated + 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) # type: ignore # Bug. Method should be deprecated + 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 diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index c076be00..9a1d91b6 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -78,8 +78,12 @@ def create_with_urllib_request( 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 # type: ignore - return BatchRequestItem(request_info, id, depends_on) # type: ignore # union types not analysed correctly + 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: """ @@ -254,7 +258,10 @@ 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) - writer.write_collection_of_object_values('headers', self._headers) # type: ignore # need method to serialize dicts + 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) diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index 2ecd945a..1faf7d8a 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -149,11 +149,7 @@ def set_responses(n: ParseNode): else: setattr(self, '_responses', {}) - - return { - 'responses': - lambda n: set_responses(n) - } + return {'responses': lambda n: set_responses(n)} def serialize(self, writer: SerializationWriter) -> None: """ diff --git a/src/msgraph_core/requests/batch_response_content_collection.py b/src/msgraph_core/requests/batch_response_content_collection.py index b071f068..2e70707b 100644 --- a/src/msgraph_core/requests/batch_response_content_collection.py +++ b/src/msgraph_core/requests/batch_response_content_collection.py @@ -66,10 +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) - ) + 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 d9d21590..0fe16636 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -142,7 +142,11 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: 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)), # type: ignore # need interface to return a dictionary + "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()), } @@ -153,7 +157,10 @@ 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) # type: ignore # need method to serialize dicts + 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: diff --git a/src/msgraph_core/tasks/large_file_upload.py b/src/msgraph_core/tasks/large_file_upload.py index 482576c9..0a0b1d83 100644 --- a/src/msgraph_core/tasks/large_file_upload.py +++ b/src/msgraph_core/tasks/large_file_upload.py @@ -16,6 +16,7 @@ T = TypeVar('T', bound=Parsable) + # pylint: disable=too-many-instance-attributes class LargeFileUploadTask: @@ -145,7 +146,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) -> LargeFileUploadSession: + 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.') diff --git a/src/msgraph_core/tasks/page_iterator.py b/src/msgraph_core/tasks/page_iterator.py index 5e604c62..2aece81a 100644 --- a/src/msgraph_core/tasks/page_iterator.py +++ b/src/msgraph_core/tasks/page_iterator.py @@ -148,7 +148,9 @@ 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() - next_link = response.odata_next_link if response and hasattr(response, 'odata_next_link') else None + 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) @@ -206,7 +208,9 @@ async def fetch_next_page(self) -> Optional[Union[T, PageResult]]: if self.request_options: request_info.add_request_options(*self.request_options) return await self.request_adapter.send_async( - request_info, self.parsable_factory, self.error_mapping # type: ignore + request_info, + self.parsable_factory, # type: ignore + self.error_mapping ) def enumerate(self, callback: Optional[Callable] = None) -> bool: From e1c575f62ec6fcbc40e886c711023593de8fb429 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Sat, 14 Dec 2024 00:19:20 +0300 Subject: [PATCH 18/25] Fix page iterator type var --- src/msgraph_core/tasks/page_iterator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/msgraph_core/tasks/page_iterator.py b/src/msgraph_core/tasks/page_iterator.py index 2aece81a..5ef6d8bf 100644 --- a/src/msgraph_core/tasks/page_iterator.py +++ b/src/msgraph_core/tasks/page_iterator.py @@ -60,7 +60,7 @@ def __init__( response: Union[T, list, object], request_adapter: RequestAdapter, constructor_callable: Optional[Callable] = None, - error_mapping: Optional[Dict[str, type[ParsableFactory]]] = None, + error_mapping: Optional[Dict[str, Type[ParsableFactory]]] = None, ): self.request_adapter = request_adapter From 9800f11e6419d0e72b1fbb0f7103323fbb5702b7 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Sat, 14 Dec 2024 00:29:45 +0300 Subject: [PATCH 19/25] Fix pylint errors --- src/msgraph_core/graph_client_factory.py | 6 ++++-- src/msgraph_core/models/page_result.py | 3 ++- src/msgraph_core/requests/batch_request_content.py | 7 ++++--- src/msgraph_core/requests/batch_request_item.py | 3 ++- src/msgraph_core/tasks/large_file_upload.py | 1 + src/msgraph_core/tasks/page_iterator.py | 2 +- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/msgraph_core/graph_client_factory.py b/src/msgraph_core/graph_client_factory.py index 5f56971a..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( # type: ignore # Breaking change to remove KiotaClientFactory as base class + 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( # type: ignore # Breaking change to remove K return GraphClientFactory._load_middleware_to_client(client, middleware) @staticmethod - def create_with_custom_middleware( # type: ignore # Breaking change to remove Kiota client factory as base class + 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, diff --git a/src/msgraph_core/models/page_result.py b/src/msgraph_core/models/page_result.py index 60f72d74..4d430201 100644 --- a/src/msgraph_core/models/page_result.py +++ b/src/msgraph_core/models/page_result.py @@ -53,7 +53,8 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: self, "value", x.get_collection_of_object_values( - Parsable # type: ignore # Bug. Should get a collection of primitive dictionary objects + Parsable # type: ignore + # Bug. Should get a collection of primitive dictionary objects ) ) } diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index 712d5abc..67d1fea3 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -1,12 +1,11 @@ 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 from kiota_abstractions.serialization import SerializationWriter -from urllib.request import Request - from .batch_request_item import BatchRequestItem @@ -68,7 +67,9 @@ def add_request(self, request_id: Optional[str], request: BatchRequestItem) -> N for dependent_id in request.depends_on: 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" + 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 diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index 9a1d91b6..141f2cb5 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -267,7 +267,8 @@ def serialize(self, writer: SerializationWriter) -> None: is_json_string = json_object and isinstance(json_object, dict) # /$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 + writer.write_collection_of_object_values( # type: ignore + # need method to serialize dicts 'body', json_object ) diff --git a/src/msgraph_core/tasks/large_file_upload.py b/src/msgraph_core/tasks/large_file_upload.py index 0a0b1d83..47082add 100644 --- a/src/msgraph_core/tasks/large_file_upload.py +++ b/src/msgraph_core/tasks/large_file_upload.py @@ -113,6 +113,7 @@ async def upload(self, after_chunk_upload: Optional[Callable] = None): lfu_session = session if lfu_session is None: continue + 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) diff --git a/src/msgraph_core/tasks/page_iterator.py b/src/msgraph_core/tasks/page_iterator.py index 5ef6d8bf..5f32855f 100644 --- a/src/msgraph_core/tasks/page_iterator.py +++ b/src/msgraph_core/tasks/page_iterator.py @@ -90,7 +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 dict() + self.error_mapping = error_mapping if error_mapping else {} def set_headers(self, headers: dict) -> HeadersCollection: """ From defda7107cd4613afed6a6a98103c614ce618835 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Sat, 14 Dec 2024 00:33:26 +0300 Subject: [PATCH 20/25] Fix Sonarcloud issues --- src/msgraph_core/tasks/large_file_upload.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/msgraph_core/tasks/large_file_upload.py b/src/msgraph_core/tasks/large_file_upload.py index 47082add..61ca4d71 100644 --- a/src/msgraph_core/tasks/large_file_upload.py +++ b/src/msgraph_core/tasks/large_file_upload.py @@ -249,12 +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(f'The object passed does not contain an additional_data property') + 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: From 68e52632803afc6c66175840b61ac16122e85115 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Mon, 16 Dec 2024 10:27:38 +0300 Subject: [PATCH 21/25] Mark StreamInterface type as deprecated StreamInterface is a PHP type that abstracts around built-in byte streams. BytesIO serves this purpose in Python making this type unnecessary --- src/msgraph_core/requests/batch_response_item.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py index 0fe16636..38f6bf2a 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -1,10 +1,15 @@ 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 + class BatchResponseItem(Parsable): @@ -104,7 +109,7 @@ 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 From 3b46699cb03129bc1c0deed4e7f7f734c80afd65 Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Mon, 16 Dec 2024 11:13:03 +0300 Subject: [PATCH 22/25] Add missing pytest asyncio plugin causing pytest failure --- requirements-dev.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 130ed4ca..6e0d5129 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' From 1d0eacc474af36d92b7aca12e4aa6ecd77f691bc Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Mon, 16 Dec 2024 11:16:35 +0300 Subject: [PATCH 23/25] Fix formatting --- src/msgraph_core/requests/batch_response_item.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py index 38f6bf2a..e4e207f2 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -6,6 +6,7 @@ from kiota_abstractions.serialization import ParseNode from kiota_abstractions.serialization import SerializationWriter + @deprecated("Use BytesIO type instead") class StreamInterface(BytesIO): pass From 4cfef9dd1a7290a2a469bb0c63a3c0dc6f310f1a Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Mon, 16 Dec 2024 11:20:19 +0300 Subject: [PATCH 24/25] import deprecated package and its type stubs --- requirements-dev.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6e0d5129..e441818e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -156,3 +156,7 @@ 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 From 40b35dcb1f7ef00fb36fcc2d2d166cfdb3124ccf Mon Sep 17 00:00:00 2001 From: Philip Gichuhi Date: Mon, 16 Dec 2024 12:18:54 +0300 Subject: [PATCH 25/25] Fix wrong test assertions --- tests/requests/test_batch_request_content.py | 2 +- tests/requests/test_batch_request_item.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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):