From e1a4c8e329207e320528142622a49890603c4969 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 7 Apr 2022 16:26:19 +0300 Subject: [PATCH 01/22] Add parsable factory type --- .../serialization/parsable_factory.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 abstractions/python/kiota/abstractions/serialization/parsable_factory.py diff --git a/abstractions/python/kiota/abstractions/serialization/parsable_factory.py b/abstractions/python/kiota/abstractions/serialization/parsable_factory.py new file mode 100644 index 0000000000..af26b91758 --- /dev/null +++ b/abstractions/python/kiota/abstractions/serialization/parsable_factory.py @@ -0,0 +1,21 @@ +from typing import Optional + +from .parsable import Parsable +from .parse_node import ParseNode, U + + +class ParsableFactory(Parsable): + """Defines the factory for creating parsable objects. + """ + @staticmethod + def create(parse_node: Optional[ParseNode]) -> U: + """Create a new parsable object from the given serialized data. + + Args: + parse_node (Optional[ParseNode]): The node to parse to get the discriminator value + from the payload. + + Returns: + U: The parsable object. + """ + pass From bccd985b43ef43a799892a89e19ff0440cb9a6dc Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 7 Apr 2022 16:27:52 +0300 Subject: [PATCH 02/22] Update parse node object methods to take factory as parameter --- .../abstractions/serialization/parse_node.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/abstractions/python/kiota/abstractions/serialization/parse_node.py b/abstractions/python/kiota/abstractions/serialization/parse_node.py index 3080c8a427..11768711c3 100644 --- a/abstractions/python/kiota/abstractions/serialization/parse_node.py +++ b/abstractions/python/kiota/abstractions/serialization/parse_node.py @@ -4,17 +4,20 @@ from datetime import date, datetime, time, timedelta from enum import Enum from io import BytesIO -from typing import Callable, List, Optional, TypeVar +from typing import TYPE_CHECKING, Callable, List, Optional, TypeVar from uuid import UUID from .parsable import Parsable + T = TypeVar("T") U = TypeVar("U", bound=Parsable) K = TypeVar("K", bound=Enum) +if TYPE_CHECKING: + from .parsable_factory import ParsableFactory class ParseNode(ABC): """ @@ -124,9 +127,10 @@ def get_collection_of_primitive_values(self) -> List[T]: pass @abstractmethod - def get_collection_of_object_values(self) -> List[U]: + def get_collection_of_object_values(self, factory: ParsableFactory) -> List[U]: """Gets the collection of model object values of the node - + Args: + factory (ParsableFactory): The factory to use to create the model object. Returns: List[U]: The collection of model object values of the node """ @@ -151,9 +155,10 @@ def get_enum_value(self) -> Enum: pass @abstractmethod - def get_object_value(self, class_type: Callable[[], U]) -> U: + def get_object_value(self, factory: ParsableFactory) -> U: """Gets the model object value of the node - + Args: + factory (ParsableFactory): The factory to use to create the model object. Returns: Parsable: The model object value of the node """ From ec672d9f79964f247a446c64bf3495376319c3f9 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 7 Apr 2022 16:29:49 +0300 Subject: [PATCH 03/22] Add method to get additional data to parsable interface --- .../kiota/abstractions/serialization/parsable.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/abstractions/python/kiota/abstractions/serialization/parsable.py b/abstractions/python/kiota/abstractions/serialization/parsable.py index 7769b44166..a80e3483c7 100644 --- a/abstractions/python/kiota/abstractions/serialization/parsable.py +++ b/abstractions/python/kiota/abstractions/serialization/parsable.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Callable, Dict, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Dict, TypeVar -T = TypeVar('T') +T = TypeVar("T") if TYPE_CHECKING: from .parse_node import ParseNode @@ -12,6 +12,15 @@ class Parsable(ABC): """ Defines a serializable model object. """ + @abstractmethod + def get_additional_data(self) -> Dict[str, Any]: + """Gets the additional data for this object that did not belong to the properties + + Returns: + Dict[str, Any]: The additional data for this object + """ + pass + @abstractmethod def get_field_deserializers(self) -> Dict[str, Callable[[T, 'ParseNode'], None]]: """Gets the deserialization information for this object. From 7c049bbf67ebe423e4415fcdf3f2d1d9e00e0fbe Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 7 Apr 2022 16:30:17 +0300 Subject: [PATCH 04/22] Update module imports --- .../python/kiota/abstractions/serialization/__init__.py | 2 +- abstractions/python/kiota/abstractions/store/__init__.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/abstractions/python/kiota/abstractions/serialization/__init__.py b/abstractions/python/kiota/abstractions/serialization/__init__.py index 017f4476bd..0f6759b89f 100644 --- a/abstractions/python/kiota/abstractions/serialization/__init__.py +++ b/abstractions/python/kiota/abstractions/serialization/__init__.py @@ -1,5 +1,5 @@ -from .additional_data_holder import AdditionalDataHolder from .parsable import Parsable +from .parsable_factory import ParsableFactory from .parse_node import ParseNode from .parse_node_factory import ParseNodeFactory from .parse_node_factory_registry import ParseNodeFactoryRegistry diff --git a/abstractions/python/kiota/abstractions/store/__init__.py b/abstractions/python/kiota/abstractions/store/__init__.py index a82c7c15ed..2d4602719a 100644 --- a/abstractions/python/kiota/abstractions/store/__init__.py +++ b/abstractions/python/kiota/abstractions/store/__init__.py @@ -1,5 +1,3 @@ -import imp - from .backed_model import BackingStore from .backing_store import BackingStore from .backing_store_factory import BackingStoreFactory From 7bc9164290a730b211120051d5528afad0258517 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 7 Apr 2022 16:30:59 +0300 Subject: [PATCH 05/22] Update error mapping type to use parsablefactory --- .../abstractions/native_response_handler.py | 6 +-- .../kiota/abstractions/request_adapter.py | 40 ++++++++++--------- .../kiota/abstractions/response_handler.py | 6 +-- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/abstractions/python/kiota/abstractions/native_response_handler.py b/abstractions/python/kiota/abstractions/native_response_handler.py index 1492796aeb..e00d4ff2a9 100644 --- a/abstractions/python/kiota/abstractions/native_response_handler.py +++ b/abstractions/python/kiota/abstractions/native_response_handler.py @@ -1,7 +1,7 @@ from typing import Any, Callable, Dict, Optional, TypeVar, cast from .response_handler import ResponseHandler -from .serialization import Parsable +from .serialization import Parsable, ParsableFactory NativeResponseType = TypeVar("NativeResponseType") ModelType = TypeVar("ModelType") @@ -16,10 +16,10 @@ class NativeResponseHandler(ResponseHandler): # The error mappings for the response to use when deserializing failed responses bodies. # Where an error code like 401 applies specifically to that status code, a class code like # 4XX applies to all status codes within the range if a specific error code is not present. - error_map: Dict[str, Optional[Callable[[], Parsable]]] + error_map: Dict[str, Optional[ParsableFactory]] async def handle_response_async( - self, response: NativeResponseType, error_map: Dict[str, Optional[Callable[[], Parsable]]] + self, response: NativeResponseType, error_map: Dict[str, Optional[ParsableFactory]] ) -> ModelType: self.value = response self.error_map = error_map diff --git a/abstractions/python/kiota/abstractions/request_adapter.py b/abstractions/python/kiota/abstractions/request_adapter.py index ac0b5d161a..fcffe8d19c 100644 --- a/abstractions/python/kiota/abstractions/request_adapter.py +++ b/abstractions/python/kiota/abstractions/request_adapter.py @@ -5,7 +5,7 @@ from .request_information import RequestInformation from .response_handler import ResponseHandler -from .serialization import Parsable, SerializationWriterFactory +from .serialization import Parsable, ParsableFactory, SerializationWriterFactory from .store import BackingStoreFactory ResponseType = TypeVar("ResponseType", str, int, float, bool, datetime, BytesIO) @@ -30,19 +30,18 @@ def get_serialization_writer_factory(self) -> SerializationWriterFactory: @abstractmethod async def send_async( - self, request_info: RequestInformation, type: ModelType, - response_handler: Optional[ResponseHandler], - error_map: Dict[str, Optional[Callable[[], Parsable]]] + self, request_info: RequestInformation, type: ParsableFactory, + response_handler: Optional[ResponseHandler], error_map: Dict[str, Optional[ParsableFactory]] ) -> ModelType: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model. Args: request_info (RequestInformation): the request info to execute. - type (ModelType): the class of the response model to deserialize the response into. + type (ParsableFactory): the class of response model to deserialize the response into. response_handler (Optional[ResponseHandler]): The response handler to use for the HTTP request instead of the default handler. - error_map (Dict[str, Optional[Callable[[], Parsable]]]): the error dict to use in case + error_map (Dict[str, Optional[ParsableFactory]]): the error dict to use in case of a failed request. Returns: @@ -52,19 +51,21 @@ async def send_async( @abstractmethod async def send_collection_async( - self, request_info: RequestInformation, type: ModelType, + self, + request_info: RequestInformation, + type: ParsableFactory, response_handler: Optional[ResponseHandler], - error_map: Dict[str, Optional[Callable[[], Parsable]]] + error_map: Dict[str, Optional[ParsableFactory]], ) -> List[ModelType]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. Args: request_info (RequestInformation): the request info to execute. - type (ModelType): the class of the response model to deserialize the response into. + type (ParsableFactory): the class of response model to deserialize the response into. response_handler (Optional[ResponseHandler]): The response handler to use for the HTTP request instead of the default handler. - error_map (Dict[str, Optional[Callable[[], Parsable]]]): the error dict to use in + error_map (Dict[str, Optional[ParsableFactory]]): the error dict to use in case of a failed request. Returns: @@ -74,9 +75,11 @@ async def send_collection_async( @abstractmethod async def send_collection_of_primitive_async( - self, request_info: RequestInformation, response_type: ResponseType, + self, + request_info: RequestInformation, + response_type: ResponseType, response_handler: Optional[ResponseHandler], - error_map: Dict[str, Optional[Callable[[], Parsable]]] + error_map: Dict[str, Optional[ParsableFactory]], ) -> Optional[List[ResponseType]]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. @@ -87,19 +90,18 @@ async def send_collection_of_primitive_async( response into. response_handler (Optional[ResponseType]): The response handler to use for the HTTP request instead of the default handler. - error_map (Dict[str, Optional[Callable[[], Parsable]]]): the error dict to use in + error_map (Dict[str, Optional[ParsableFactory]]): the error dict to use in case of a failed request. Returns: - Optional[List[ModelType]]: he deserialized response model collection. + Optional[List[ModelType]]: The deserialized response model collection. """ pass @abstractmethod async def send_primitive_async( self, request_info: RequestInformation, response_type: ResponseType, - response_handler: Optional[ResponseHandler], - error_map: Dict[str, Optional[Callable[[], Parsable]]] + response_handler: Optional[ResponseHandler], error_map: Dict[str, Optional[ParsableFactory]] ) -> ResponseType: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. @@ -110,7 +112,7 @@ async def send_primitive_async( response into. response_handler (Optional[ResponseHandler]): The response handler to use for the HTTP request instead of the default handler. - error_map (Dict[str, Optional[Callable[[], Parsable]]]): the error dict to use in + error_map (Dict[str, Optional[ParsableFactory]]): the error dict to use in case of a failed request. Returns: @@ -121,7 +123,7 @@ async def send_primitive_async( @abstractmethod async def send_no_response_content_async( self, request_info: RequestInformation, response_handler: Optional[ResponseHandler], - error_map: Dict[str, Optional[Callable[[], Parsable]]] + error_map: Dict[str, Optional[ParsableFactory]] ) -> None: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. @@ -130,7 +132,7 @@ async def send_no_response_content_async( request_info (RequestInformation):the request info to execute. response_handler (Optional[ResponseHandler]): The response handler to use for the HTTP request instead of the default handler. - error_map (Dict[str, Optional[Callable[[], Parsable]]]): the error dict to use in + error_map (Dict[str, Optional[Optional[ParsableFactory]]): the error dict to use in case of a failed request. """ pass diff --git a/abstractions/python/kiota/abstractions/response_handler.py b/abstractions/python/kiota/abstractions/response_handler.py index 4933f71be2..cbcfd6e0fb 100644 --- a/abstractions/python/kiota/abstractions/response_handler.py +++ b/abstractions/python/kiota/abstractions/response_handler.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from typing import Callable, Dict, Optional, TypeVar -from .serialization import Parsable +from .serialization import Parsable, ParsableFactory NativeResponseType = TypeVar("NativeResponseType") ModelType = TypeVar("ModelType") @@ -12,13 +12,13 @@ class ResponseHandler(ABC): """ @abstractmethod async def handle_response_async( - self, response: NativeResponseType, error_map: Dict[str, Optional[Callable[[], Parsable]]] + self, response: NativeResponseType, error_map: Dict[str, Optional[ParsableFactory]] ) -> ModelType: """Callback method that is invoked when a response is received. Args: response (NativeResponseType): The type of the native response object. - error_map (Dict[str, Optional[Callable[[], Parsable]]]): the error dict to use + error_map (Dict[str, Optional[ParsableFactory]]): the error dict to use in case of a failed request. Returns: From 71655f9b6693aef47fbd0c6cb5da404fb1cea50b Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 7 Apr 2022 17:55:10 +0300 Subject: [PATCH 06/22] Update json parse node to take factory as parameter --- .../python/json/serialization_json/json_parse_node.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/serialization/python/json/serialization_json/json_parse_node.py b/serialization/python/json/serialization_json/json_parse_node.py index 4ffe698263..632a8551e1 100644 --- a/serialization/python/json/serialization_json/json_parse_node.py +++ b/serialization/python/json/serialization_json/json_parse_node.py @@ -9,7 +9,8 @@ from uuid import UUID from dateutil import parser -from kiota.abstractions.serialization import AdditionalDataHolder, Parsable, ParseNode + +from kiota.abstractions.serialization import AdditionalDataHolder, Parsable, ParsableFactory, ParseNode T = TypeVar("T") @@ -165,14 +166,14 @@ def func(item): return list(map(func, json.loads(self._json_node))) return list(map(func, list(self._json_node))) - def get_collection_of_object_values(self, class_type: Type[U]) -> List[U]: + def get_collection_of_object_values(self, factory: ParsableFactory) -> List[U]: """Gets the collection of type U values from the json node Returns: List[U]: The collection of model object values of the node """ return list( map( - lambda x: JsonParseNode(json.dumps(x)).get_object_value(class_type), # type: ignore + lambda x: JsonParseNode(json.dumps(x)).get_object_value(factory), # type: ignore json.loads(self._json_node) ) ) @@ -203,12 +204,12 @@ def get_enum_value(self, enum_class: K) -> Optional[K]: raise Exception(f'Invalid key: {raw_key} for enum {enum_class._name_}.') return None - def get_object_value(self, class_type: Callable[[], U]) -> U: + def get_object_value(self, factory: ParsableFactory) -> U: """Gets the model object value of the node Returns: Parsable: The model object value of the node """ - result = class_type() + result = factory.create(self) if self.on_before_assign_field_values: self.on_before_assign_field_values(result) self._assign_field_values(result) From 8b944c0d199f5bade1d223706608a192ad9f1d1d Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 14 Apr 2022 15:32:25 +0300 Subject: [PATCH 07/22] Add factory to request adapter implementation --- .../http_requests/requests_request_adapter.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/http/python/requests/http_requests/requests_request_adapter.py b/http/python/requests/http_requests/requests_request_adapter.py index cba527fb1f..d17ef7c04f 100644 --- a/http/python/requests/http_requests/requests_request_adapter.py +++ b/http/python/requests/http_requests/requests_request_adapter.py @@ -12,6 +12,7 @@ from kiota.abstractions.response_handler import ResponseHandler from kiota.abstractions.serialization import ( Parsable, + ParsableFactory, ParseNode, ParseNodeFactory, ParseNodeFactoryRegistry, @@ -91,18 +92,18 @@ def get_response_content_type(self, response: requests.Response) -> Optional[str return segments[0] async def send_async( - self, request_info: RequestInformation, model_type: ModelType, + self, request_info: RequestInformation, model_type: ParsableFactory, response_handler: Optional[ResponseHandler], - error_map: Dict[str, Optional[Callable[[], Parsable]]] + error_map: Dict[str, ParsableFactory] ) -> ModelType: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model. Args: request_info (RequestInformation): the request info to execute. - type (ModelType): the class of the response model to deserialize the response into. + type (ParsableFactory): the class of the response model to deserialize the response into. response_handler (Optional[ResponseHandler]): The response handler to use for the HTTP request instead of the default handler. - error_map (Dict[str, Optional[Callable[[], Parsable]]]): the error dict to use in + error_map (Dict[str, ParsableFactory]): the error dict to use in case of a failed request. Returns: @@ -122,18 +123,18 @@ async def send_async( return result async def send_collection_async( - self, request_info: RequestInformation, model_type: ModelType, + self, request_info: RequestInformation, model_type: ParsableFactory, response_handler: Optional[ResponseHandler], - error_map: Dict[str, Optional[Callable[[], Parsable]]] + error_map: Dict[str, ParsableFactory] ) -> List[ModelType]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. Args: request_info (RequestInformation): the request info to execute. - type (ModelType): the class of the response model to deserialize the response into. + type (ParsableFactory): the class of the response model to deserialize the response into. response_handler (Optional[ResponseHandler]): The response handler to use for the HTTP request instead of the default handler. - error_map (Dict[str, Optional[Callable[[], Parsable]]]): the error dict to use in + error_map (Dict[str, ParsableFactory]): the error dict to use in case of a failed request. Returns: @@ -155,7 +156,7 @@ async def send_collection_async( async def send_collection_of_primitive_async( self, request_info: RequestInformation, response_type: ResponseType, response_handler: Optional[ResponseHandler], - error_map: Dict[str, Optional[Callable[[], Parsable]]] + error_map: Dict[str, ParsableFactory] ) -> Optional[List[ResponseType]]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. @@ -165,7 +166,7 @@ async def send_collection_of_primitive_async( response into. response_handler (Optional[ResponseType]): The response handler to use for the HTTP request instead of the default handler. - error_map (Dict[str, Optional[Callable[[], Parsable]]]): the error dict to use in + error_map (Dict[str, ParsableFactory]): the error dict to use in case of a failed request. Returns: @@ -186,7 +187,7 @@ async def send_collection_of_primitive_async( async def send_primitive_async( self, request_info: RequestInformation, response_type: ResponseType, response_handler: Optional[ResponseHandler], - error_map: Dict[str, Optional[Callable[[], Parsable]]] + error_map: Dict[str, ParsableFactory] ) -> ResponseType: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. @@ -196,7 +197,7 @@ async def send_primitive_async( response into. response_handler (Optional[ResponseHandler]): The response handler to use for the HTTP request instead of the default handler. - error_map (Dict[str, Optional[Callable[[], Parsable]]]): the error dict to use in case + error_map (Dict[str, ParsableFactory]): the error dict to use in case of a failed request. Returns: @@ -228,7 +229,7 @@ async def send_primitive_async( async def send_no_response_content_async( self, request_info: RequestInformation, response_handler: Optional[ResponseHandler], - error_map: Dict[str, Optional[Callable[[], Parsable]]] + error_map: Dict[str, ParsableFactory] ) -> None: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. @@ -236,7 +237,7 @@ async def send_no_response_content_async( request_info (RequestInformation):the request info to execute. response_handler (Optional[ResponseHandler]): The response handler to use for the HTTP request instead of the default handler. - error_map (Dict[str, Optional[Callable[[], Parsable]]]): the error dict to use in case + error_map (Dict[str, ParsableFactory]): the error dict to use in case of a failed request. """ if not request_info: @@ -274,7 +275,7 @@ async def get_root_parse_node(self, response: requests.Response) -> ParseNode: return self._parse_node_factory.get_root_parse_node(response_content_type, payload) async def throw_failed_responses( - self, response: requests.Response, error_map: Dict[str, Optional[Callable[[], Parsable]]] + self, response: requests.Response, error_map: Dict[str, ParsableFactory] ) -> None: if response.ok: return From 722c865820022c8e1896aa6194730d0f8d04ac64 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 14 Apr 2022 15:34:15 +0300 Subject: [PATCH 08/22] Bump abstractions, http, and serialization package versions --- abstractions/python/kiota/abstractions/_version.py | 2 +- http/python/requests/http_requests/_version.py | 2 +- serialization/python/json/serialization_json/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/abstractions/python/kiota/abstractions/_version.py b/abstractions/python/kiota/abstractions/_version.py index 6bf6b2c362..8dd7053bf0 100644 --- a/abstractions/python/kiota/abstractions/_version.py +++ b/abstractions/python/kiota/abstractions/_version.py @@ -1 +1 @@ -VERSION: str = '0.3.0' +VERSION: str = '0.4.0' diff --git a/http/python/requests/http_requests/_version.py b/http/python/requests/http_requests/_version.py index 6b47c142e5..8e3d668e72 100644 --- a/http/python/requests/http_requests/_version.py +++ b/http/python/requests/http_requests/_version.py @@ -1 +1 @@ -VERSION: str = '0.1.0' +VERSION: str = '0.2.0' diff --git a/serialization/python/json/serialization_json/_version.py b/serialization/python/json/serialization_json/_version.py index 6bf6b2c362..8dd7053bf0 100644 --- a/serialization/python/json/serialization_json/_version.py +++ b/serialization/python/json/serialization_json/_version.py @@ -1 +1 @@ -VERSION: str = '0.3.0' +VERSION: str = '0.4.0' From efe695afcd4290727566c1a79d703e3d335307c4 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 14 Apr 2022 15:54:12 +0300 Subject: [PATCH 09/22] Fix linting and format issues --- .../kiota/abstractions/serialization/__init__.py | 1 + .../abstractions/serialization/parse_node.py | 2 +- .../http_requests/requests_request_adapter.py | 16 ++++++---------- .../json/serialization_json/json_parse_node.py | 9 ++++++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/abstractions/python/kiota/abstractions/serialization/__init__.py b/abstractions/python/kiota/abstractions/serialization/__init__.py index 0f6759b89f..9dfb2175d5 100644 --- a/abstractions/python/kiota/abstractions/serialization/__init__.py +++ b/abstractions/python/kiota/abstractions/serialization/__init__.py @@ -1,3 +1,4 @@ +from .additional_data_holder import AdditionalDataHolder from .parsable import Parsable from .parsable_factory import ParsableFactory from .parse_node import ParseNode diff --git a/abstractions/python/kiota/abstractions/serialization/parse_node.py b/abstractions/python/kiota/abstractions/serialization/parse_node.py index 11768711c3..f5d7eef4d1 100644 --- a/abstractions/python/kiota/abstractions/serialization/parse_node.py +++ b/abstractions/python/kiota/abstractions/serialization/parse_node.py @@ -9,7 +9,6 @@ from .parsable import Parsable - T = TypeVar("T") U = TypeVar("U", bound=Parsable) @@ -19,6 +18,7 @@ if TYPE_CHECKING: from .parsable_factory import ParsableFactory + class ParseNode(ABC): """ Interface for a deserialization node in a parse tree. This interace provides an abstraction diff --git a/http/python/requests/http_requests/requests_request_adapter.py b/http/python/requests/http_requests/requests_request_adapter.py index d17ef7c04f..eff4e388e9 100644 --- a/http/python/requests/http_requests/requests_request_adapter.py +++ b/http/python/requests/http_requests/requests_request_adapter.py @@ -93,14 +93,13 @@ def get_response_content_type(self, response: requests.Response) -> Optional[str async def send_async( self, request_info: RequestInformation, model_type: ParsableFactory, - response_handler: Optional[ResponseHandler], - error_map: Dict[str, ParsableFactory] + response_handler: Optional[ResponseHandler], error_map: Dict[str, ParsableFactory] ) -> ModelType: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model. Args: request_info (RequestInformation): the request info to execute. - type (ParsableFactory): the class of the response model to deserialize the response into. + type (ParsableFactory): the class of the response model to deserialize the response into response_handler (Optional[ResponseHandler]): The response handler to use for the HTTP request instead of the default handler. error_map (Dict[str, ParsableFactory]): the error dict to use in @@ -124,14 +123,13 @@ async def send_async( async def send_collection_async( self, request_info: RequestInformation, model_type: ParsableFactory, - response_handler: Optional[ResponseHandler], - error_map: Dict[str, ParsableFactory] + response_handler: Optional[ResponseHandler], error_map: Dict[str, ParsableFactory] ) -> List[ModelType]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. Args: request_info (RequestInformation): the request info to execute. - type (ParsableFactory): the class of the response model to deserialize the response into. + type (ParsableFactory): the class of the response model to deserialize the response into response_handler (Optional[ResponseHandler]): The response handler to use for the HTTP request instead of the default handler. error_map (Dict[str, ParsableFactory]): the error dict to use in @@ -155,8 +153,7 @@ async def send_collection_async( async def send_collection_of_primitive_async( self, request_info: RequestInformation, response_type: ResponseType, - response_handler: Optional[ResponseHandler], - error_map: Dict[str, ParsableFactory] + response_handler: Optional[ResponseHandler], error_map: Dict[str, ParsableFactory] ) -> Optional[List[ResponseType]]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. @@ -186,8 +183,7 @@ async def send_collection_of_primitive_async( async def send_primitive_async( self, request_info: RequestInformation, response_type: ResponseType, - response_handler: Optional[ResponseHandler], - error_map: Dict[str, ParsableFactory] + response_handler: Optional[ResponseHandler], error_map: Dict[str, ParsableFactory] ) -> ResponseType: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. diff --git a/serialization/python/json/serialization_json/json_parse_node.py b/serialization/python/json/serialization_json/json_parse_node.py index 632a8551e1..b19d263d18 100644 --- a/serialization/python/json/serialization_json/json_parse_node.py +++ b/serialization/python/json/serialization_json/json_parse_node.py @@ -9,8 +9,12 @@ from uuid import UUID from dateutil import parser - -from kiota.abstractions.serialization import AdditionalDataHolder, Parsable, ParsableFactory, ParseNode +from kiota.abstractions.serialization import ( + AdditionalDataHolder, + Parsable, + ParsableFactory, + ParseNode, +) T = TypeVar("T") @@ -272,4 +276,3 @@ def _assign_field_values(self, item: U) -> None: else: if item_additional_data: item_additional_data[snake_case_key] = val - From 8fd37d410fe387155341719c994582920c4e1329 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 14 Apr 2022 17:23:05 +0300 Subject: [PATCH 10/22] Add an entty to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6a7250d7a..686fb892ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug with special characters in query parameters names. [#1445](https://github.com/microsoft/kiota/issues/1445) - Fixed a bug where complex types path parameters would fail to generate. - Fixed a bug where Go serialization/deserialization method would generate invalid accessor names. +- Added discriminator support in the python abstractions serialization and http packages. [#1500](https://github.com/microsoft/kiota/issues/1256) ## [0.0.22] - 2022-04-08 From 0816afbb9a9013277550884c650ed60ed40f1a7a Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Tue, 24 May 2022 10:27:45 +0300 Subject: [PATCH 11/22] Remove unnecessary get additional data method from parsable inteface --- .../python/kiota/abstractions/serialization/parsable.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/abstractions/python/kiota/abstractions/serialization/parsable.py b/abstractions/python/kiota/abstractions/serialization/parsable.py index a80e3483c7..bf06f9175f 100644 --- a/abstractions/python/kiota/abstractions/serialization/parsable.py +++ b/abstractions/python/kiota/abstractions/serialization/parsable.py @@ -12,15 +12,6 @@ class Parsable(ABC): """ Defines a serializable model object. """ - @abstractmethod - def get_additional_data(self) -> Dict[str, Any]: - """Gets the additional data for this object that did not belong to the properties - - Returns: - Dict[str, Any]: The additional data for this object - """ - pass - @abstractmethod def get_field_deserializers(self) -> Dict[str, Callable[[T, 'ParseNode'], None]]: """Gets the deserialization information for this object. From 02ff3e1ba6a1434c59f2e48b9cfe462ff32e9d84 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Tue, 24 May 2022 17:44:38 +0300 Subject: [PATCH 12/22] Add support for no content responses --- .../kiota/abstractions/request_adapter.py | 6 ++-- .../http_requests/requests_request_adapter.py | 17 ++++++++-- .../tests/test_requests_request_adapter.py | 33 +++++++++++++++++++ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/abstractions/python/kiota/abstractions/request_adapter.py b/abstractions/python/kiota/abstractions/request_adapter.py index fcffe8d19c..1c661a955e 100644 --- a/abstractions/python/kiota/abstractions/request_adapter.py +++ b/abstractions/python/kiota/abstractions/request_adapter.py @@ -32,7 +32,7 @@ def get_serialization_writer_factory(self) -> SerializationWriterFactory: async def send_async( self, request_info: RequestInformation, type: ParsableFactory, response_handler: Optional[ResponseHandler], error_map: Dict[str, Optional[ParsableFactory]] - ) -> ModelType: + ) -> Optional[ModelType]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model. @@ -56,7 +56,7 @@ async def send_collection_async( type: ParsableFactory, response_handler: Optional[ResponseHandler], error_map: Dict[str, Optional[ParsableFactory]], - ) -> List[ModelType]: + ) -> Optional[List[ModelType]]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. @@ -102,7 +102,7 @@ async def send_collection_of_primitive_async( async def send_primitive_async( self, request_info: RequestInformation, response_type: ResponseType, response_handler: Optional[ResponseHandler], error_map: Dict[str, Optional[ParsableFactory]] - ) -> ResponseType: + ) -> Optional[ResponseType]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. diff --git a/http/python/requests/http_requests/requests_request_adapter.py b/http/python/requests/http_requests/requests_request_adapter.py index eff4e388e9..f2b543c299 100644 --- a/http/python/requests/http_requests/requests_request_adapter.py +++ b/http/python/requests/http_requests/requests_request_adapter.py @@ -94,7 +94,7 @@ def get_response_content_type(self, response: requests.Response) -> Optional[str async def send_async( self, request_info: RequestInformation, model_type: ParsableFactory, response_handler: Optional[ResponseHandler], error_map: Dict[str, ParsableFactory] - ) -> ModelType: + ) -> Optional[ModelType]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model. Args: @@ -117,6 +117,8 @@ async def send_async( return await response_handler.handle_response_async(response, error_map) await self.throw_failed_responses(response, error_map) + if self._should_return_none(response): + return None root_node = await self.get_root_parse_node(response) result = root_node.get_object_value(model_type) return result @@ -124,7 +126,7 @@ async def send_async( async def send_collection_async( self, request_info: RequestInformation, model_type: ParsableFactory, response_handler: Optional[ResponseHandler], error_map: Dict[str, ParsableFactory] - ) -> List[ModelType]: + ) -> Optional[List[ModelType]]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. Args: @@ -147,6 +149,8 @@ async def send_collection_async( return await response_handler.handle_response_async(response, error_map) await self.throw_failed_responses(response, error_map) + if self._should_return_none(response): + return None root_node = await self.get_root_parse_node(response) result = root_node.get_collection_of_object_values(model_type) return result @@ -178,13 +182,15 @@ async def send_collection_of_primitive_async( return await response_handler.handle_response_async(response, error_map) await self.throw_failed_responses(response, error_map) + if self._should_return_none(response): + return None root_node = await self.get_root_parse_node(response) return root_node.get_collection_of_primitive_values() async def send_primitive_async( self, request_info: RequestInformation, response_type: ResponseType, response_handler: Optional[ResponseHandler], error_map: Dict[str, ParsableFactory] - ) -> ResponseType: + ) -> Optional[ResponseType]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. Args: @@ -208,6 +214,8 @@ async def send_primitive_async( return await response_handler.handle_response_async(response, error_map) await self.throw_failed_responses(response, error_map) + if self._should_return_none(response): + return None root_node = await self.get_root_parse_node(response) if response_type == str: return root_node.get_string_value() @@ -270,6 +278,9 @@ async def get_root_parse_node(self, response: requests.Response) -> ParseNode: return self._parse_node_factory.get_root_parse_node(response_content_type, payload) + def _should_return_none(self, response: requests.Response) -> bool: + return response.status_code == 204 + async def throw_failed_responses( self, response: requests.Response, error_map: Dict[str, ParsableFactory] ) -> None: diff --git a/http/python/requests/tests/test_requests_request_adapter.py b/http/python/requests/tests/test_requests_request_adapter.py index bcb438438c..e3bc2ead8a 100644 --- a/http/python/requests/tests/test_requests_request_adapter.py +++ b/http/python/requests/tests/test_requests_request_adapter.py @@ -197,6 +197,30 @@ def mock_primitive_response(mocker): resp = session.send(prepped) return resp +@pytest.fixture +@responses.activate +def mock_no_content_response(mocker): + responses.add( + responses.GET, + url=BASE_URL, + status=204, + match=[ + matchers.header_matcher({"Content-Type": "application/json"}, strict_match=True) + ] + ) + + session = requests.Session() + prepped = session.prepare_request( + requests.Request( + method="GET", + url=BASE_URL, + ) + ) + prepped.headers = {"Content-Type": "application/json"} + + resp = session.send(prepped) + return resp + def test_create_requests_request_adapter(auth_provider, parse_node_factory, serialization_writer_factory): request_adapter = RequestsRequestAdapter(auth_provider, parse_node_factory, serialization_writer_factory) assert request_adapter._authentication_provider is auth_provider @@ -293,3 +317,12 @@ async def test_send_primitive_async(request_adapter, request_info, mock_primitiv assert resp.headers.get("content-type") == 'application/json' final_result = await request_adapter.send_primitive_async(request_info, float, None, {}) assert final_result == 22.3 + +@pytest.mark.asyncio +@responses.activate +async def test_send_primitive_async(request_adapter, request_info, mock_no_content_response): + request_adapter.get_http_response_message = AsyncMock(return_value = mock_no_content_response) + resp = await request_adapter.get_http_response_message(request_info) + assert resp.headers.get("content-type") == 'application/json' + final_result = await request_adapter.send_primitive_async(request_info, float, None, {}) + assert final_result is None From 05e6e8bb7633359b4951245431a29d22892f2d27 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Wed, 25 May 2022 15:40:08 +0300 Subject: [PATCH 13/22] Add support for vendor specific content types --- .../serialization/parse_node_factory_registry.py | 14 +++++++++++--- .../serialization_writer_factory_registry.py | 14 +++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/abstractions/python/kiota/abstractions/serialization/parse_node_factory_registry.py b/abstractions/python/kiota/abstractions/serialization/parse_node_factory_registry.py index 1bebb0a7df..f626dbe28e 100644 --- a/abstractions/python/kiota/abstractions/serialization/parse_node_factory_registry.py +++ b/abstractions/python/kiota/abstractions/serialization/parse_node_factory_registry.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re from io import BytesIO from typing import Dict @@ -36,9 +37,16 @@ def get_root_parse_node(self, content_type: str, content: BytesIO) -> ParseNode: if not content: raise Exception("Content cannot be null") - factory = self.CONTENT_TYPE_ASSOCIATED_FACTORIES.get(content_type) + vendor_specific_content_type = content_type.split(';')[0] + factory = self.CONTENT_TYPE_ASSOCIATED_FACTORIES.get(vendor_specific_content_type) if factory: - return factory.get_root_parse_node(content_type, content) + return factory.get_root_parse_node(vendor_specific_content_type, content) + + cleaned_content_type = re.sub(r'[^/]+\+', '', vendor_specific_content_type) + factory = self.CONTENT_TYPE_ASSOCIATED_FACTORIES.get(cleaned_content_type) + if factory: + return factory.get_root_parse_node(cleaned_content_type, content) + raise Exception( - f"Content type {content_type} does not have a factory registered to be parsed" + f"Content type {cleaned_content_type} does not have a factory registered to be parsed" ) diff --git a/abstractions/python/kiota/abstractions/serialization/serialization_writer_factory_registry.py b/abstractions/python/kiota/abstractions/serialization/serialization_writer_factory_registry.py index 2fd5c2e848..f2fa4e4a12 100644 --- a/abstractions/python/kiota/abstractions/serialization/serialization_writer_factory_registry.py +++ b/abstractions/python/kiota/abstractions/serialization/serialization_writer_factory_registry.py @@ -1,3 +1,4 @@ +import re from typing import Dict from .serialization_writer import SerializationWriter @@ -32,9 +33,16 @@ def get_serialization_writer(self, content_type: str) -> SerializationWriter: if not content_type: raise Exception("Content type cannot be null") - factory = self.CONTENT_TYPE_ASSOCIATED_FACTORIES.get(content_type) + vendor_specific_content_type = content_type.split(';')[0] + factory = self.CONTENT_TYPE_ASSOCIATED_FACTORIES.get(vendor_specific_content_type) if factory: - return factory.get_serialization_writer(content_type) + return factory.get_serialization_writer(vendor_specific_content_type) + cleaned_content_type = re.sub(r'[^/]+\+', '', vendor_specific_content_type) + + factory = self.CONTENT_TYPE_ASSOCIATED_FACTORIES.get(cleaned_content_type) + if factory: + return factory.get_serialization_writer(cleaned_content_type) raise Exception( - f"Content type {content_type} does not have a factory registered to be serialized" + f"Content type {cleaned_content_type} does not have a factory registered" + "to be serialized" ) From 42750cf8865ae13f84c406f2b888340da1ac5c79 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Wed, 25 May 2022 18:40:24 +0300 Subject: [PATCH 14/22] Simplify field deserializers --- .../python/kiota/abstractions/serialization/parsable.py | 4 ++-- .../python/json/serialization_json/json_parse_node.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/abstractions/python/kiota/abstractions/serialization/parsable.py b/abstractions/python/kiota/abstractions/serialization/parsable.py index bf06f9175f..cc3a01a14a 100644 --- a/abstractions/python/kiota/abstractions/serialization/parsable.py +++ b/abstractions/python/kiota/abstractions/serialization/parsable.py @@ -13,11 +13,11 @@ class Parsable(ABC): Defines a serializable model object. """ @abstractmethod - def get_field_deserializers(self) -> Dict[str, Callable[[T, 'ParseNode'], None]]: + def get_field_deserializers(self) -> Dict[str, Callable[['ParseNode'], None]]: """Gets the deserialization information for this object. Returns: - Dict[str, Callable[[T, ParseNode], None]]: The deserialization information for this + Dict[str, Callable[[ParseNode], None]]: The deserialization information for this object where each entry is a property key with its deserialization callback. """ pass diff --git a/serialization/python/json/serialization_json/json_parse_node.py b/serialization/python/json/serialization_json/json_parse_node.py index b19d263d18..1516de078a 100644 --- a/serialization/python/json/serialization_json/json_parse_node.py +++ b/serialization/python/json/serialization_json/json_parse_node.py @@ -272,7 +272,7 @@ def _assign_field_values(self, item: U) -> None: snake_case_key = re.sub(r'(? Date: Wed, 25 May 2022 18:40:52 +0300 Subject: [PATCH 15/22] Update test fixtures --- .../python/json/tests/helpers/user.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/serialization/python/json/tests/helpers/user.py b/serialization/python/json/tests/helpers/user.py index 0fc8d4b22a..3f48cb0a67 100644 --- a/serialization/python/json/tests/helpers/user.py +++ b/serialization/python/json/tests/helpers/user.py @@ -87,34 +87,34 @@ def get_additional_data(self) -> Dict[str, Any]: def set_additional_data(self, data: Dict[str, Any]) -> None: self._additional_data = data - def get_field_deserializers(self) -> Dict[str, Callable[[T, ParseNode], None]]: + def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: """Gets the deserialization information for this object. Returns: - Dict[str, Callable[[T, ParseNode], None]]: The deserialization information for this + Dict[str, Callable[[ParseNode], None]]: The deserialization information for this object where each entry is a property key with its deserialization callback. """ return { "id": - lambda o, n: o.set_id(n.get_uuid_value()), + lambda n: self.set_id(n.get_uuid_value()), "display_name": - lambda o, n: o.set_display_name(n.get_string_value()), + lambda n: self.set_display_name(n.get_string_value()), "office_location": - lambda o, n: o.set_office_location(n.get_enum_value(OfficeLocation)), + lambda n: self.set_office_location(n.get_enum_value(OfficeLocation)), "updated_at": - lambda o, n: o.set_updated_at(n.get_datetime_offset_value()), + lambda n: self.set_updated_at(n.get_datetime_offset_value()), "birthday": - lambda o, n: o.set_birthday(n.get_date_value()), + lambda n: self.set_birthday(n.get_date_value()), "business_phones": - lambda o, n: o.set_business_phones(n.get_collection_of_primitive_values()), + lambda n: self.set_business_phones(n.get_collection_of_primitive_values()), "mobile_phone": - lambda o, n: o.set_mobile_phone(n.get_string_value()), + lambda n: self.set_mobile_phone(n.get_string_value()), "is_active": - lambda o, n: o.set_is_active(n.get_boolean_value()), + lambda n: self.set_is_active(n.get_boolean_value()), "age": - lambda o, n: o.set_age(n.get_int_value()), + lambda n: self.set_age(n.get_int_value()), "gpa": - lambda o, n: o.set_gpa(n.get_float_value()) + lambda n: self.set_gpa(n.get_float_value()) } def serialize(self, writer: SerializationWriter) -> None: From 9da3dce66e2c7cc9a02e97e01bb9c4cd2edbc1f0 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Tue, 21 Jun 2022 11:06:28 +0300 Subject: [PATCH 16/22] Add support for encoding and decoding special characters in query parameters --- .../kiota/abstractions/request_information.py | 11 +++++ .../http_requests/kiota_client_factory.py | 4 +- .../http_requests/middleware/__init__.py | 1 + .../middleware/options/__init__.py | 1 + .../parameters_name_decoding_options.py | 22 +++++++++ .../parameters_name_decoding_handler.py | 45 +++++++++++++++++++ 6 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 http/python/requests/http_requests/middleware/options/parameters_name_decoding_options.py create mode 100644 http/python/requests/http_requests/middleware/parameters_name_decoding_handler.py diff --git a/abstractions/python/kiota/abstractions/request_information.py b/abstractions/python/kiota/abstractions/request_information.py index 48b277e6e8..1a14bf5ef5 100644 --- a/abstractions/python/kiota/abstractions/request_information.py +++ b/abstractions/python/kiota/abstractions/request_information.py @@ -1,4 +1,5 @@ from io import BytesIO +from dataclasses import fields from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Tuple, TypeVar from uritemplate import URITemplate @@ -133,3 +134,13 @@ def set_stream_content(self, value: BytesIO) -> None: """ self.headers[self.CONTENT_TYPE_HEADER] = self.BINARY_CONTENT_TYPE self.content = value + + def set_query_string_parameters_from_raw_object(self, q: object) -> None: + for field in fields(q): + key = field + if hasattr(q, 'get_query_parameter'): + serialization_key = q.get_query_parameter(key) + if serialization_key: + key = serialization_key + self.query_parameters[key] = getattr(q, field.name) + \ No newline at end of file diff --git a/http/python/requests/http_requests/kiota_client_factory.py b/http/python/requests/http_requests/kiota_client_factory.py index 6ae3f520b7..67a2a4c853 100644 --- a/http/python/requests/http_requests/kiota_client_factory.py +++ b/http/python/requests/http_requests/kiota_client_factory.py @@ -2,8 +2,7 @@ import requests -from .middleware import MiddlewarePipeline, RetryHandler - +from .middleware import MiddlewarePipeline, ParametersNameDecodingHandler, RetryHandler class KiotaClientFactory: DEFAULT_CONNECTION_TIMEOUT: int = 30 @@ -35,6 +34,7 @@ def _register_default_middleware(self, session: requests.Session) -> requests.Se """ middleware_pipeline = MiddlewarePipeline() middlewares = [ + ParametersNameDecodingHandler(), RetryHandler(), ] diff --git a/http/python/requests/http_requests/middleware/__init__.py b/http/python/requests/http_requests/middleware/__init__.py index 2ca89dee50..536b81f283 100644 --- a/http/python/requests/http_requests/middleware/__init__.py +++ b/http/python/requests/http_requests/middleware/__init__.py @@ -1,2 +1,3 @@ from .middleware import MiddlewarePipeline +from .parameters_name_decoding_handler import ParametersNameDecodingHandler from .retry_handler import RetryHandler diff --git a/http/python/requests/http_requests/middleware/options/__init__.py b/http/python/requests/http_requests/middleware/options/__init__.py index d71b2d1d27..16b887d9d5 100644 --- a/http/python/requests/http_requests/middleware/options/__init__.py +++ b/http/python/requests/http_requests/middleware/options/__init__.py @@ -1 +1,2 @@ +from .parameters_name_decoding_options import ParametersNameDecodingHandlerOption from .retry_handler_option import RetryHandlerOptions diff --git a/http/python/requests/http_requests/middleware/options/parameters_name_decoding_options.py b/http/python/requests/http_requests/middleware/options/parameters_name_decoding_options.py new file mode 100644 index 0000000000..db2d17325d --- /dev/null +++ b/http/python/requests/http_requests/middleware/options/parameters_name_decoding_options.py @@ -0,0 +1,22 @@ +from typing import List +from kiota.abstractions.request_option import RequestOption + +class ParametersNameDecodingHandlerOption(RequestOption): + """The ParametersNameDecodingOptions request class + """ + + parameters_name_decoding_handler_options_key = "ParametersNameDecodingOptionKey" + + def __init__(self, enable: bool = True, characters_to_decode: List[str] = [".", "-", "~", "$"]) -> None: + """To create an instance of ParametersNameDecodingHandlerOptions + + Args: + enable (bool, optional): - Whether to decode the specified characters in the request query parameters names. + Defaults to True. + characters_to_decode (List[str], optional):- The characters to decode. Defaults to [".", "-", "~", "$"]. + """ + self.enable = enable + self.characters_to_decode = characters_to_decode + + def get_key(self) -> str: + return self.parameters_name_decoding_handler_options_key \ No newline at end of file diff --git a/http/python/requests/http_requests/middleware/parameters_name_decoding_handler.py b/http/python/requests/http_requests/middleware/parameters_name_decoding_handler.py new file mode 100644 index 0000000000..858f324cae --- /dev/null +++ b/http/python/requests/http_requests/middleware/parameters_name_decoding_handler.py @@ -0,0 +1,45 @@ +from typing import Dict +from kiota.abstractions.request_option import RequestOption +from requests import PreparedRequest, Response + +from .middleware import BaseMiddleware +from .options import ParametersNameDecodingHandlerOption + +class ParametersNameDecodingHandler(BaseMiddleware): + + def __init__(self, options: ParametersNameDecodingHandlerOption = ParametersNameDecodingHandlerOption(), **kwargs): + """Create an instance of ParametersNameDecodingHandler + + Args: + options (ParametersNameDecodingHandlerOption, optional): The parameters name decoding handler options value. + Defaults to ParametersNameDecodingHandlerOption + """ + if not options: + raise Exception("The options parameter is required.") + + self.options = options + + def send(self, request: PreparedRequest, request_options: Dict[str, RequestOption], **kwargs) -> Response: + """To execute the current middleware + + Args: + request (PreparedRequest): The prepared request object + request_options (Dict[str, RequestOption]): The request options + + Returns: + Response: The response object. + """ + current_options = self.options + options_key = ParametersNameDecodingHandlerOption.parameters_name_decoding_handler_options_key + if request_options and options_key in request_options.keys(): + current_options = request_options[options_key] + + updated_url = request.url + if current_options and current_options.enable and '%' in updated_url and current_options.characters_to_decode: + for char in current_options.characters_to_decode: + encoding = f"{ord(f'{char}:X')}" + updated_url = updated_url.replace(f'%{encoding}', char) + + request.url = updated_url + response = super().send(request, **kwargs) + return response \ No newline at end of file From ba65992b569122cc2db4502e7a06f1f7adea2c1f Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Tue, 21 Jun 2022 14:10:59 +0300 Subject: [PATCH 17/22] Request configuration revamp --- ...se_bearer_token_authentication_provider.py | 4 +- .../kiota/abstractions/request_information.py | 69 ++++++++++++------- .../http_requests/requests_request_adapter.py | 2 +- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/abstractions/python/kiota/abstractions/authentication/base_bearer_token_authentication_provider.py b/abstractions/python/kiota/abstractions/authentication/base_bearer_token_authentication_provider.py index d0bbf22754..6525f81123 100644 --- a/abstractions/python/kiota/abstractions/authentication/base_bearer_token_authentication_provider.py +++ b/abstractions/python/kiota/abstractions/authentication/base_bearer_token_authentication_provider.py @@ -20,10 +20,10 @@ async def authenticate_request(self, request: RequestInformation) -> None: """ if not request: raise Exception("Request cannot be null") - if not request.headers: + if not request.get_request_headers(): request.headers = {} if not self.AUTHORIZATION_HEADER in request.headers: token = await self.access_token_provider.get_authorization_token(request.get_url()) if token: - request.headers.update({f'{self.AUTHORIZATION_HEADER}': f'Bearer {token}'}) + request.add_request_headers({f'{self.AUTHORIZATION_HEADER}': f'Bearer {token}'}) diff --git a/abstractions/python/kiota/abstractions/request_information.py b/abstractions/python/kiota/abstractions/request_information.py index 1a14bf5ef5..ee81cccb0f 100644 --- a/abstractions/python/kiota/abstractions/request_information.py +++ b/abstractions/python/kiota/abstractions/request_information.py @@ -22,29 +22,31 @@ class RequestInformation(Generic[QueryParams]): RAW_URL_KEY = 'request-raw-url' BINARY_CONTENT_TYPE = 'application/octet-stream' CONTENT_TYPE_HEADER = 'Content-Type' + + def __init__(self) -> None: - # The uri of the request - __uri: Optional[Url] + # The uri of the request + self.__uri: Optional[Url] = None - __request_options: Dict[str, RequestOption] = {} + self.__request_options: Dict[str, RequestOption] = {} - # The path parameters for the current request - path_parameters: Dict[str, Any] = {} + # The path parameters for the current request + self.path_parameters: Dict[str, Any] = {} - # The URL template for the request - url_template: Optional[str] + # The URL template for the request + self.url_template: Optional[str] - # The HTTP Method for the request - http_method: Method + # The HTTP Method for the request + self.http_method: Method - # The query parameters for the request - query_parameters: Dict[str, QueryParams] = {} + # The query parameters for the request + self.query_parameters: Dict[str, QueryParams] = {} - # The Request Headers - headers: Dict[str, str] = {} + # The Request Headers + self.headers: Dict[str, str] = {} - # The Request Body - content: BytesIO + # The Request Body + self.content: BytesIO def get_url(self) -> Url: """ Gets the URL of the request @@ -80,6 +82,25 @@ def set_url(self, url: Url) -> None: self.query_parameters.clear() self.path_parameters.clear() + def get_request_headers(self) -> Optional[Dict]: + return self.headers + + def add_request_headers(self, headers_to_add: Optional[Dict[str, str]]) -> None: + """Adds headers to the request + """ + if headers_to_add: + for key in headers_to_add: + self.headers[key.lower()] = headers_to_add[key] + + def remove_request_headers(self, key: str) -> None: + """Removes a request header from the current request + + Args: + key (str): The key of the header to remove + """ + if key and key.lower in self.headers.keys(): + del self.headers[key.lower()] + def get_request_options(self) -> List[Tuple[str, RequestOption]]: """Gets the request options for the request. """ @@ -135,12 +156,12 @@ def set_stream_content(self, value: BytesIO) -> None: self.headers[self.CONTENT_TYPE_HEADER] = self.BINARY_CONTENT_TYPE self.content = value - def set_query_string_parameters_from_raw_object(self, q: object) -> None: - for field in fields(q): - key = field - if hasattr(q, 'get_query_parameter'): - serialization_key = q.get_query_parameter(key) - if serialization_key: - key = serialization_key - self.query_parameters[key] = getattr(q, field.name) - \ No newline at end of file + def set_query_string_parameters_from_raw_object(self, q: Optional[object]) -> None: + if q: + for field in fields(q): + key = field + if hasattr(q, 'get_query_parameter'): + serialization_key = q.get_query_parameter(key) + if serialization_key: + key = serialization_key + self.query_parameters[key] = getattr(q, field.name) diff --git a/http/python/requests/http_requests/requests_request_adapter.py b/http/python/requests/http_requests/requests_request_adapter.py index f2b543c299..7699106b2c 100644 --- a/http/python/requests/http_requests/requests_request_adapter.py +++ b/http/python/requests/http_requests/requests_request_adapter.py @@ -333,7 +333,7 @@ def get_request_from_request_information( req = requests.Request( method=str(request_info.http_method), url=request_info.get_url(), - headers=request_info.headers, + headers=request_info.get_request_headers(), data=request_info.content, params=request_info.query_parameters, ) From 0a624d829a1b75c12d7a5c844fdbe8429c6fe9f5 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Fri, 15 Jul 2022 05:21:00 +0300 Subject: [PATCH 18/22] Remove unnecessary print statement --- http/python/requests/http_requests/requests_request_adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http/python/requests/http_requests/requests_request_adapter.py b/http/python/requests/http_requests/requests_request_adapter.py index f2b543c299..5e492b0a53 100644 --- a/http/python/requests/http_requests/requests_request_adapter.py +++ b/http/python/requests/http_requests/requests_request_adapter.py @@ -271,8 +271,8 @@ def enable_backing_store(self, backing_store_factory: Optional[BackingStoreFacto async def get_root_parse_node(self, response: requests.Response) -> ParseNode: payload = response.content - print(payload) response_content_type = self.get_response_content_type(response) + if not response_content_type: raise Exception("No response content type found for deserialization") From 597c137879d9995a165634a595bacc32d4c8a122 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Fri, 15 Jul 2022 05:29:49 +0300 Subject: [PATCH 19/22] Add an entry into changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64a6529c29..dfed787042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Added support for no-content responses in python abstractions and http packages. [#1630](https://github.com/microsoft/kiota/issues/1459) ### Changed From f30e874637511ba6f079473ced2a65cbfcaae755 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Fri, 15 Jul 2022 05:46:00 +0300 Subject: [PATCH 20/22] Add an entry into changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfed787042..582646fa19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support for no-content responses in python abstractions and http packages. [#1630](https://github.com/microsoft/kiota/issues/1459) +- Added support for vendor-specific content types in python. [#1631](https://github.com/microsoft/kiota/issues/1463) ### Changed From ded67d920eb9c740ad1c8fc2169b15ce269324be Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Fri, 15 Jul 2022 06:11:57 +0300 Subject: [PATCH 21/22] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 582646fa19..fe3b933635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support for no-content responses in python abstractions and http packages. [#1630](https://github.com/microsoft/kiota/issues/1459) - Added support for vendor-specific content types in python. [#1631](https://github.com/microsoft/kiota/issues/1463) +- Simplified field deserializers for json in Python. [#1632](https://github.com/microsoft/kiota/issues/1492) ### Changed From a490962228915c4640a46a731d4bb3ef8bdd0b1f Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Fri, 15 Jul 2022 07:38:51 +0300 Subject: [PATCH 22/22] Fix failing checks --- .../kiota/abstractions/request_information.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/abstractions/python/kiota/abstractions/request_information.py b/abstractions/python/kiota/abstractions/request_information.py index ee81cccb0f..94b4ed4c8b 100644 --- a/abstractions/python/kiota/abstractions/request_information.py +++ b/abstractions/python/kiota/abstractions/request_information.py @@ -1,5 +1,5 @@ -from io import BytesIO from dataclasses import fields +from io import BytesIO from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Tuple, TypeVar from uritemplate import URITemplate @@ -13,7 +13,7 @@ Url = str T = TypeVar("T", bound=Parsable) -QueryParams = TypeVar('QueryParams', int, float, str, bool, None) +QueryParams = TypeVar('QueryParams') class RequestInformation(Generic[QueryParams]): @@ -22,7 +22,7 @@ class RequestInformation(Generic[QueryParams]): RAW_URL_KEY = 'request-raw-url' BINARY_CONTENT_TYPE = 'application/octet-stream' CONTENT_TYPE_HEADER = 'Content-Type' - + def __init__(self) -> None: # The uri of the request @@ -84,23 +84,23 @@ def set_url(self, url: Url) -> None: def get_request_headers(self) -> Optional[Dict]: return self.headers - + def add_request_headers(self, headers_to_add: Optional[Dict[str, str]]) -> None: """Adds headers to the request """ if headers_to_add: for key in headers_to_add: self.headers[key.lower()] = headers_to_add[key] - + def remove_request_headers(self, key: str) -> None: """Removes a request header from the current request Args: key (str): The key of the header to remove """ - if key and key.lower in self.headers.keys(): + if key and key.lower() in self.headers: del self.headers[key.lower()] - + def get_request_options(self) -> List[Tuple[str, RequestOption]]: """Gets the request options for the request. """ @@ -155,13 +155,13 @@ def set_stream_content(self, value: BytesIO) -> None: """ self.headers[self.CONTENT_TYPE_HEADER] = self.BINARY_CONTENT_TYPE self.content = value - - def set_query_string_parameters_from_raw_object(self, q: Optional[object]) -> None: + + def set_query_string_parameters_from_raw_object(self, q: Optional[QueryParams]) -> None: if q: for field in fields(q): - key = field + key = field.name if hasattr(q, 'get_query_parameter'): - serialization_key = q.get_query_parameter(key) + serialization_key = q.get_query_parameter(key) #type: ignore if serialization_key: key = serialization_key self.query_parameters[key] = getattr(q, field.name)