Skip to content

Commit

Permalink
python: provide full typing for the REST client(s)
Browse files Browse the repository at this point in the history
  • Loading branch information
multani committed Oct 1, 2023
1 parent cec5b89 commit 122260f
Show file tree
Hide file tree
Showing 19 changed files with 895 additions and 272 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/samples-python-client-echo-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,15 @@ jobs:
- name: Test
working-directory: ${{ matrix.sample }}
run: python -m pytest

# These modules should pass mypy without errors
- name: mypy (WIP)
working-directory: ${{ matrix.sample }}
run: |
python -m mypy */rest.py
# This runs mypy on all the modules, but full typing is not yet fully implemented, this can fail.
- name: mypy
working-directory: ${{ matrix.sample }}
continue-on-error: true
run: python -m mypy
13 changes: 13 additions & 0 deletions .github/workflows/samples-python-petstore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,16 @@ jobs:
- name: Test
working-directory: ${{ matrix.sample }}
run: poetry run pytest -v

# These modules should pass mypy without errors
- name: mypy (WIP)
working-directory: ${{ matrix.sample }}
run: |
poetry run mypy */rest.py
# This runs mypy on all the modules, but full typing is not yet fully implemented, this can fail.
- name: mypy
working-directory: ${{ matrix.sample }}
continue-on-error: true
run: |
poetry run mypy
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,43 @@ import json
import logging
import re
import ssl
from typing import Any, Dict, List, Optional, Union, Tuple

import aiohttp
from aiohttp import ClientResponse
from urllib.parse import urlencode, quote_plus

from {{packageName}}.configuration import Configuration
from {{packageName}}.exceptions import ApiException, ApiValueError

logger = logging.getLogger(__name__)


class RESTResponse(io.IOBase):
class RESTResponse:
"""An HTTP response."""
# This provides a generic object to store HTTP responses.
# It proxies the original HTTP response from the underlying HTTP library
# (aiohttp, urllib3, etc.) so that clients of RESTClientObject can work
# without knowing too much about each library specifics.

def __init__(self, resp, data) -> None:
self.aiohttp_response = resp
def __init__(self, resp: ClientResponse, data: bytes) -> None:
self._aiohttp_response = resp
self.status = resp.status
self.reason = resp.reason
self.data = data

def getheaders(self):
"""Returns a CIMultiDictProxy of the response headers."""
return self.aiohttp_response.headers
def getheaders(self) -> Dict[str, str]:
"""Returns a dictionary of the response headers."""
# Note: this can lose the CIMultiDictProxy duplicated headers.
return dict(self._aiohttp_response.headers)

def getheader(self, name, default=None):
def getheader(self, name: str, default: Optional[str]=None) -> Optional[str]:
"""Returns a given response header."""
return self.aiohttp_response.headers.get(name, default)
return self._aiohttp_response.headers.get(name, default)


class RESTClientObject:

def __init__(self, configuration, pools_size=4, maxsize=None) -> None:

def __init__(self, configuration: Configuration, pools_size: int=4, maxsize: Optional[int]=None) -> None:
# maxsize is number of requests to host that are allowed in parallel
if maxsize is None:
maxsize = configuration.connection_pool_maxsize
Expand Down Expand Up @@ -65,12 +72,19 @@ class RESTClientObject:
trust_env=True
)

async def close(self):
async def close(self) -> None:
await self.pool_manager.close()

async def request(self, method, url, query_params=None, headers=None,
body=None, post_params=None, _preload_content=True,
_request_timeout=None):
async def request(self,
method: str,
url: str,
query_params: Optional[Dict[str, str]]=None,
headers: Optional[Dict[str, str]]=None,
body: Any=None,
post_params: Optional[List[Tuple[str, Union[str, Tuple[str, bytes, str]]]]]=None,
_preload_content: bool=True,
_request_timeout: Optional[int]=None,
) -> RESTResponse:
"""Execute request

:param method: http request method
Expand All @@ -97,7 +111,7 @@ class RESTClientObject:
"body parameter cannot be used with post_params parameter."
)

post_params = post_params or {}
post_params = post_params or []
headers = headers or {}
# url already contains the URL query string
# so reset query_params to empty dict
Expand All @@ -108,8 +122,6 @@ class RESTClientObject:
headers['Content-Type'] = 'application/json'

args = {
"method": method,
"url": url,
"timeout": timeout,
"headers": headers
}
Expand All @@ -120,7 +132,8 @@ class RESTClientObject:
args["proxy_headers"] = self.proxy_headers

if query_params:
args["url"] += '?' + urlencode(query_params)
url += '?' + urlencode(query_params)


# For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
Expand Down Expand Up @@ -158,12 +171,12 @@ class RESTClientObject:
declared content type."""
raise ApiException(status=0, reason=msg)

r = await self.pool_manager.request(**args)
if _preload_content:
_r = await self.pool_manager.request(method, url, **args)

data = await r.read()
r = RESTResponse(r, data)
response_data = await r.read()
r = RESTResponse(_r, response_data)

if _preload_content:
# log response body
logger.debug("response body: %s", r.data)

Expand All @@ -172,25 +185,44 @@ class RESTClientObject:

return r

async def get_request(self, url, headers=None, query_params=None,
_preload_content=True, _request_timeout=None):
async def get_request(
self,
url: str,
headers: Optional[Dict[str, str]]=None,
query_params: Optional[Dict[str, str]]=None,
_preload_content: bool=True,
_request_timeout: Optional[int]=None,
) -> RESTResponse:
return (await self.request("GET", url,
headers=headers,
_preload_content=_preload_content,
_request_timeout=_request_timeout,
query_params=query_params))

async def head_request(self, url, headers=None, query_params=None,
_preload_content=True, _request_timeout=None):
async def head_request(
self,
url: str,
headers: Optional[Dict[str, str]]=None,
query_params: Optional[Dict[str, str]]=None,
_preload_content: bool=True,
_request_timeout: Optional[int]=None,
) -> RESTResponse:
return (await self.request("HEAD", url,
headers=headers,
_preload_content=_preload_content,
_request_timeout=_request_timeout,
query_params=query_params))

async def options_request(self, url, headers=None, query_params=None,
post_params=None, body=None, _preload_content=True,
_request_timeout=None):
async def options_request(
self,
url: str,
headers: Optional[Dict[str, str]]=None,
query_params: Optional[Dict[str, str]]=None,
post_params: Optional[List[Tuple[str, Union[str, Tuple[str, bytes, str]]]]]=None,
body: Any=None,
_preload_content: bool=True,
_request_timeout: Optional[int]=None,
) -> RESTResponse:
return (await self.request("OPTIONS", url,
headers=headers,
query_params=query_params,
Expand All @@ -199,18 +231,32 @@ class RESTClientObject:
_request_timeout=_request_timeout,
body=body))

async def delete_request(self, url, headers=None, query_params=None, body=None,
_preload_content=True, _request_timeout=None):
async def delete_request(
self,
url: str,
headers: Optional[Dict[str, str]]=None,
query_params: Optional[Dict[str, str]]=None,
body: Any=None,
_preload_content: bool=True,
_request_timeout: Optional[int]=None,
) -> RESTResponse:
return (await self.request("DELETE", url,
headers=headers,
query_params=query_params,
_preload_content=_preload_content,
_request_timeout=_request_timeout,
body=body))

async def post_request(self, url, headers=None, query_params=None,
post_params=None, body=None, _preload_content=True,
_request_timeout=None):
async def post_request(
self,
url: str,
headers: Optional[Dict[str, str]]=None,
query_params: Optional[Dict[str, str]]=None,
post_params: Optional[List[Tuple[str, Union[str, Tuple[str, bytes, str]]]]]=None,
body: Any=None,
_preload_content: bool=True,
_request_timeout: Optional[int]=None,
) -> RESTResponse:
return (await self.request("POST", url,
headers=headers,
query_params=query_params,
Expand All @@ -219,8 +265,16 @@ class RESTClientObject:
_request_timeout=_request_timeout,
body=body))

async def put_request(self, url, headers=None, query_params=None, post_params=None,
body=None, _preload_content=True, _request_timeout=None):
async def put_request(
self,
url: str,
headers: Optional[Dict[str, str]]=None,
query_params: Optional[Dict[str, str]]=None,
post_params: Optional[List[Tuple[str, Union[str, Tuple[str, bytes, str]]]]]=None,
body: Any=None,
_preload_content: bool=True,
_request_timeout: Optional[int]=None,
) -> RESTResponse:
return (await self.request("PUT", url,
headers=headers,
query_params=query_params,
Expand All @@ -229,9 +283,16 @@ class RESTClientObject:
_request_timeout=_request_timeout,
body=body))

async def patch_request(self, url, headers=None, query_params=None,
post_params=None, body=None, _preload_content=True,
_request_timeout=None):
async def patch_request(
self,
url: str,
headers: Optional[Dict[str, str]]=None,
query_params: Optional[Dict[str, str]]=None,
post_params: Optional[List[Tuple[str, Union[str, Tuple[str, bytes, str]]]]]=None,
body: Any=None,
_preload_content: bool=True,
_request_timeout: Optional[int]=None,
) -> RESTResponse:
return (await self.request("PATCH", url,
headers=headers,
query_params=query_params,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,32 @@ typing-extensions = ">=4.7.1"
pytest = ">=7.2.1"
tox = ">=3.9.0"
flake8 = ">=4.0.0"
mypy = "<1.5.0" # 1.5.0 supports Python 3.8+
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[tool.pylint.'MESSAGES CONTROL']
extension-pkg-whitelist = "pydantic"
[tool.mypy]
packages = ["{{packageName}}"]
warn_unused_configs = true
warn_redundant_casts = true
[[tool.mypy.overrides]]
module = "{{packageName}}.rest"
#strict = true
warn_unused_ignores = true
strict_equality = true
strict_concatenate = true
check_untyped_defs = true
disallow_subclassing_any = true
disallow_untyped_decorators = true
disallow_any_generics = true
disallow_untyped_calls = true
disallow_incomplete_defs = true
disallow_untyped_defs = true
no_implicit_reexport = true
warn_return_any = true
Loading

0 comments on commit 122260f

Please sign in to comment.