Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/github_actions' into github_actions
Browse files Browse the repository at this point in the history
  • Loading branch information
Станислав Михайлов committed Sep 20, 2024
2 parents 49e5d07 + 3fe0f74 commit 075dfb8
Show file tree
Hide file tree
Showing 71 changed files with 5,184 additions and 0 deletions.
161 changes: 161 additions & 0 deletions docs/QUICKSTART.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
## Быстрый старт

## HTTPX_Service

Если не нужно использовать сконфигурированный транспорт, используйте HTTPXService (для async -> AsyncHttpxService)

```python
from kontur.httptoolkitcore import HttpxService
from kontur.httptoolkitcore import Header

headers = (
Header(name="My-Header", value="my-value", is_sensitive=False),
)
httpbin = HttpxService("http://httpbin.org", headers=headers)
httpbin.get("/get")
httpbin.post("/post")
```

## Service

Если нужно использовать сконфигурированный transport, используйте Service. (Service -> HttpxTransport, AsyncService -> AsyncHttpxTransport)

```python
### Sync

from kontur.httptoolkitcore import Service, Header
from kontur.httptoolkitcore.transport import HttpxTransport


class DummyService(Service):
pass


DummyService(
headers=(Header(name="ServiceHeader", value="service-header", is_sensitive=False),),
transport=HttpxTransport(base_url="https://example.com:4321", proxies={"http://": "http://10.10.1.10:3128"}),
## base_url в таком случае передается в transport
)
```

```python
### Async

from kontur.httptoolkitcore import AsyncService, Header
from kontur.httptoolkitcore.transport import AsyncHttpxTransport


class DummyService(AsyncService):
pass


DummyService(
headers=(Header(name="ServiceHeader", value="service-header", is_sensitive=False),),
transport=AsyncHttpxTransport(base_url="https://example.com:4321", proxies={"http://": "http://10.10.1.10:3128"}),
## base_url в таком случае передается в transport
)
```

### Отправка запроса

```python
### Async

from kontur.httptoolkitcore import Service, Header, HttpMethod
from kontur.httptoolkitcore.transport import HttpxTransport
from kontur.httptoolkitcore.request import Request


class DummyService(Service):
pass


service = DummyService(
headers=(Header(name="ServiceHeader", value="service-header", is_sensitive=False),),
transport=HttpxTransport(base_url="https://example.com:4321", proxies={"http://": "http://10.10.1.10:3128"}),
## base_url в таком случае передается в transport
)

# По методу
service.post(
path="/somewhere",
headers=(Header(name="SuperSecret", value="big_secret", is_sensitive=True, create_mask=lambda value: value[-4:])),
params={"over": "the rainbow"},
body="Something",
)

# По request
service.request(Request(method=HttpMethod.POST, body="Request", params={}, path=""))
```

### Отправка особых типов

```python
from kontur.httptoolkitcore import Service, Header, HttpMethod
from kontur.httptoolkitcore.transport import HttpxTransport
from kontur.httptoolkitcore.request import Request


class DummyService(Service):
pass


service = DummyService(
headers=(Header(name="ServiceHeader", value="service-header", is_sensitive=False),),
transport=HttpxTransport(base_url="https://example.com:4321", proxies={"http://": "http://10.10.1.10:3128"}),
## base_url в таком случае передается в transport
)

# Отправить JSON (json_encoder задан по-умолчанию, но можно его поменять в transport)
# Не отправлять вместе с body и с files
service.post(
path="/somewhere",
headers=(Header(name="SuperSecret", value="big_secret", is_sensitive=True, create_mask=lambda value: value[-4:])),
params={"over": "the rainbow"},
json={
"param1": 1,
"param2": 2,
},
)

# Отправить multipart-files в формате Dict[str, Union[BinaryIO, Tuple[str, BinaryIO, str]]]
# Можно отправлять вместе с body, но нельзя с json
service.post(
path="/somewhere",
headers=(Header(name="SuperSecret", value="big_secret", is_sensitive=True, create_mask=lambda value: value[-4:]),),
params={"over": "the rainbow"},
files={"upload-file": open("report.xls", "rb")},
# другой формат files = {'upload-file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel')}
)
```

## Имя логгера библиотеки

kontur.httptoolkitcore

## Уровень логирования по-умолчанию

logging.INFO

## Пример настройки логирования

```python
import logging
import kontur.httptoolkitcore

logging.basicConfig(level="INFO")


class MyService(kontur.httptoolkit.HttpxService):
def test(self):
self.get("/")


service = MyService("https://kontur.ru")

service.test()
```
## Вывод
```python
INFO:kontur.httptoolkit.transport._sync:Sending GET https://kontur.ru/
```
27 changes: 27 additions & 0 deletions kontur/httptoolkitcore/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import logging

from kontur.httptoolkitcore.errors import ServiceError, suppress_http_error
from kontur.httptoolkitcore.header import AuthSidHeader, BasicAuthHeader, BearerAuthHeader, Header
from kontur.httptoolkitcore.service import AsyncService, Service
from kontur.httptoolkitcore.httpx_service import HttpxService, AsyncHttpxService
from kontur.httptoolkitcore.http_method import HttpMethod

__all__ = [
"Service",
"AsyncService",
"HttpxService",
"AsyncHttpxService",
"ServiceError",
"suppress_http_error",
"Header",
"AuthSidHeader",
"BasicAuthHeader",
"BearerAuthHeader",
"HttpMethod",
]

logger = logging.getLogger("kontur.httptoolkitcore")
logger.addHandler(logging.NullHandler())
logger.setLevel(logging.INFO)
httpx_log = logging.getLogger("httpx")
httpx_log.propagate = False
3 changes: 3 additions & 0 deletions kontur/httptoolkitcore/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from os import environ

__version__ = environ.get("CI_COMMIT_TAG") or "0.dev0"
14 changes: 14 additions & 0 deletions kontur/httptoolkitcore/encoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import json
import uuid
from typing import Any


class DefaultJSONEncoder(json.JSONEncoder):
def default(self, obj: Any) -> str:
if isinstance(obj, uuid.UUID):
return str(obj)
return super().default(obj)


def default_json_encoder(content: Any) -> str:
return json.dumps(content, cls=DefaultJSONEncoder)
116 changes: 116 additions & 0 deletions kontur/httptoolkitcore/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from contextlib import contextmanager
from typing import Optional


class Error(Exception):
def __str__(self):
context = self.__cause__ or self.__context__

if context:
return "{}: {}".format(type(context).__name__, context)
else:
return super().__str__()


class TransportError(Error):
def __init__(self, request) -> None:
self.request = request


class ServiceError(Error):
MESSAGE_DELIMITER = "\n"

def __init__(self, request, response=None):
self._request = request
self._response = response

def __str__(self):
description = self._description()
context = self.__cause__ or self.__context__

if context:
description = self._concatenate(str(context), description)

return description

@property
def response(self):
return self._response

def response_code(self) -> Optional[int]:
if self._response is not None:
return self._response.status_code
return None

def response_body(self) -> Optional[str]:
if self._response is not None:
return self._response.text
return None

def _description(self):
return self._concatenate(self._request_description(), self._response_description())

def _request_description(self):
return self._concatenate(
"Request: {} {}".format(self._request.method.upper(), self._request.url),
"Request headers: {}".format(self._request.filtered_headers),
"Proxies: {}".format(self._request.proxies),
)

def _response_description(self):
if self._response is not None:
return self._concatenate(
"Response: {} {}".format(self.response_code(), self._response.reason),
"Response headers: {}".format(self._response.headers),
"Response body: {}".format(self.response_body()),
)

def __getstate__(self):
contexts = [self.__cause__ or self.__context__]
while contexts[len(contexts) - 1]:
last_context = contexts[len(contexts) - 1]
contexts.append(last_context.__context__ or last_context.__cause__)
return {
"contexts": contexts,
}

def __setstate__(self, state):
contexts = state["contexts"]
self.__context__ = contexts[0]
context = self.__context__
for i in range(0, len(contexts) - 1):
context.__context__ = contexts[i]

def __reduce__(self):
return (self.__class__, (self._request, self._response), self.__getstate__())

def _concatenate(self, *strings):
return self.MESSAGE_DELIMITER.join(filter(None, strings))


class HttpError(ServiceError):
def __init__(self, request, response):
super().__init__(request, response)


class HttpErrorTypecast:
HTTP_BAD_REQUEST_CODE = 400

@classmethod
def is_bad_request_error(cls, http_error):
return isinstance(http_error, Error) and http_error.response_code() == cls.HTTP_BAD_REQUEST_CODE


@contextmanager
def suppress_http_error(*statuses):
"""
Suppress http error with specified status codes.
:param statuses: list of status codes
"""
try:
yield
except HttpError as e:
if e.response.status_code in statuses:
return None
raise
Loading

0 comments on commit 075dfb8

Please sign in to comment.