Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for no content responses #1630

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e1a4c8e
Add parsable factory type
samwelkanda Apr 7, 2022
bccd985
Update parse node object methods to take factory as parameter
samwelkanda Apr 7, 2022
ec672d9
Add method to get additional data to parsable interface
samwelkanda Apr 7, 2022
7c049bb
Update module imports
samwelkanda Apr 7, 2022
7bc9164
Update error mapping type to use parsablefactory
samwelkanda Apr 7, 2022
71655f9
Update json parse node to take factory as parameter
samwelkanda Apr 7, 2022
8b944c0
Add factory to request adapter implementation
samwelkanda Apr 14, 2022
722c865
Bump abstractions, http, and serialization package versions
samwelkanda Apr 14, 2022
efe695a
Fix linting and format issues
samwelkanda Apr 14, 2022
8fd37d4
Add an entty to changelog
samwelkanda Apr 14, 2022
0816afb
Remove unnecessary get additional data method from parsable inteface
samwelkanda May 24, 2022
02ff3e1
Add support for no content responses
samwelkanda May 24, 2022
05e6e8b
Add support for vendor specific content types
samwelkanda May 25, 2022
42750cf
Simplify field deserializers
samwelkanda May 25, 2022
a5a4d01
Update test fixtures
samwelkanda May 25, 2022
9da3dce
Add support for encoding and decoding special characters in query par…
samwelkanda Jun 21, 2022
ba65992
Request configuration revamp
samwelkanda Jun 21, 2022
ff5c67f
Merge branch 'feature/discriminator-support-python' into feature/no-c…
samwelkanda Jul 15, 2022
0a624d8
Remove unnecessary print statement
samwelkanda Jul 15, 2022
597c137
Add an entry into changelog
samwelkanda Jul 15, 2022
7002f45
Merge branch 'feature/no-content-response-python' into feature/vendor…
samwelkanda Jul 15, 2022
f30e874
Add an entry into changelog
samwelkanda Jul 15, 2022
2597f2e
Merge branch 'feature/vendor-specific-content-types-python' into feat…
samwelkanda Jul 15, 2022
ded67d9
Add changelog entry
samwelkanda Jul 15, 2022
6fa978f
Merge branch 'feature/field-deserializers-simplification-python' into…
samwelkanda Jul 15, 2022
a490962
Fix failing checks
samwelkanda Jul 15, 2022
ed8bf47
Merge pull request #1741 from microsoft/feature/request-config-revamp
samwelkanda Jul 15, 2022
0388c1b
Merge pull request #1632 from microsoft/feature/field-deserializers-s…
samwelkanda Jul 15, 2022
7677cc6
Merge pull request #1631 from microsoft/feature/vendor-specific-conte…
samwelkanda Jul 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ 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)
- 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}'})
6 changes: 3 additions & 3 deletions abstractions/python/kiota/abstractions/request_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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.

Expand Down
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import re
from io import BytesIO
from typing import Dict

Expand Down Expand Up @@ -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"
)
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from typing import Dict

from .serialization_writer import SerializationWriter
Expand Down Expand Up @@ -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"
)
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
Loading