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 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 48b277e6e8..94b4ed4c8b 100644 --- a/abstractions/python/kiota/abstractions/request_information.py +++ b/abstractions/python/kiota/abstractions/request_information.py @@ -1,3 +1,4 @@ +from dataclasses import fields from io import BytesIO from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Tuple, TypeVar @@ -12,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,28 +23,30 @@ class RequestInformation(Generic[QueryParams]): BINARY_CONTENT_TYPE = 'application/octet-stream' CONTENT_TYPE_HEADER = 'Content-Type' - # The uri of the request - __uri: Optional[Url] + def __init__(self) -> None: - __request_options: Dict[str, RequestOption] = {} + # The uri of the request + self.__uri: Optional[Url] = None - # The path parameters for the current request - path_parameters: Dict[str, Any] = {} + self.__request_options: Dict[str, RequestOption] = {} - # The URL template for the request - url_template: Optional[str] + # The path parameters for the current request + self.path_parameters: Dict[str, Any] = {} - # The HTTP Method for the request - http_method: Method + # The URL template for the request + self.url_template: Optional[str] - # The query parameters for the request - query_parameters: Dict[str, QueryParams] = {} + # The HTTP Method for the request + self.http_method: Method - # The Request Headers - headers: Dict[str, str] = {} + # The query parameters for the request + self.query_parameters: Dict[str, QueryParams] = {} - # The Request Body - content: BytesIO + # The Request Headers + self.headers: Dict[str, str] = {} + + # The Request Body + self.content: BytesIO def get_url(self) -> Url: """ Gets the URL of the request @@ -79,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: + del self.headers[key.lower()] + def get_request_options(self) -> List[Tuple[str, RequestOption]]: """Gets the request options for the request. """ @@ -133,3 +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[QueryParams]) -> None: + if q: + for field in fields(q): + key = field.name + if hasattr(q, 'get_query_parameter'): + serialization_key = q.get_query_parameter(key) #type: ignore + if serialization_key: + key = serialization_key + self.query_parameters[key] = getattr(q, field.name) diff --git a/abstractions/python/kiota/abstractions/serialization/parsable.py b/abstractions/python/kiota/abstractions/serialization/parsable.py index c01a03a2be..d5d749a0d8 100644 --- a/abstractions/python/kiota/abstractions/serialization/parsable.py +++ b/abstractions/python/kiota/abstractions/serialization/parsable.py @@ -14,11 +14,11 @@ class Parsable(ABC): """ @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/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 diff --git a/http/python/requests/http_requests/requests_request_adapter.py b/http/python/requests/http_requests/requests_request_adapter.py index 5e492b0a53..e134bc2507 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, ) diff --git a/serialization/python/json/serialization_json/json_parse_node.py b/serialization/python/json/serialization_json/json_parse_node.py index c1e43f9326..c926a31529 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'(? 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: