Skip to content

Commit

Permalink
Merge pull request #1632 from microsoft/feature/field-deserializers-s…
Browse files Browse the repository at this point in the history
…implification-python

Feature/field deserializers simplification python
  • Loading branch information
samwelkanda authored Jul 15, 2022
2 parents f30e874 + ed8bf47 commit 0388c1b
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}'})
64 changes: 48 additions & 16 deletions abstractions/python/kiota/abstractions/request_information.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dataclasses import fields
from io import BytesIO
from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Tuple, TypeVar

Expand All @@ -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]):
Expand All @@ -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
Expand Down Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions http/python/requests/http_requests/kiota_client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

import requests

from .middleware import MiddlewarePipeline, RetryHandler

from .middleware import MiddlewarePipeline, ParametersNameDecodingHandler, RetryHandler

class KiotaClientFactory:
DEFAULT_CONNECTION_TIMEOUT: int = 30
Expand Down Expand Up @@ -35,6 +34,7 @@ def _register_default_middleware(self, session: requests.Session) -> requests.Se
"""
middleware_pipeline = MiddlewarePipeline()
middlewares = [
ParametersNameDecodingHandler(),
RetryHandler(),
]

Expand Down
1 change: 1 addition & 0 deletions http/python/requests/http_requests/middleware/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .middleware import MiddlewarePipeline
from .parameters_name_decoding_handler import ParametersNameDecodingHandler
from .retry_handler import RetryHandler
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .parameters_name_decoding_options import ParametersNameDecodingHandlerOption
from .retry_handler_option import RetryHandlerOptions
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ def _assign_field_values(self, item: U) -> None:
snake_case_key = re.sub(r'(?<!^)(?=[A-Z])', '_', key).lower()
deserializer = fields.get(snake_case_key)
if deserializer:
deserializer(item, JsonParseNode(val))
deserializer(JsonParseNode(val))
else:
if item_additional_data:
item_additional_data[snake_case_key] = val
24 changes: 12 additions & 12 deletions serialization/python/json/tests/helpers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 0388c1b

Please sign in to comment.