soap-client 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- soap_client/__init__.py +14 -0
- soap_client/exceptions.py +24 -0
- soap_client/manager.py +127 -0
- soap_client/py.typed +0 -0
- soap_client/registry.py +84 -0
- soap_client/settings.py +57 -0
- soap_client-0.1.0.dist-info/METADATA +28 -0
- soap_client-0.1.0.dist-info/RECORD +9 -0
- soap_client-0.1.0.dist-info/WHEEL +4 -0
soap_client/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""SOAP client library."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from soap_client.exceptions import SoapError
|
|
6
|
+
from soap_client.manager import SoapClientManager
|
|
7
|
+
from soap_client.registry import SoapRegistry
|
|
8
|
+
from soap_client.settings import BaseSoapClientSettings
|
|
9
|
+
|
|
10
|
+
__all__ = ["BaseSoapClientSettings", "SoapClientManager", "SoapError", "SoapRegistry"]
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logging.getLogger("soap_client").addHandler(logging.NullHandler())
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Файл хранит классы-исключения."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SoapError(ConnectionError):
|
|
7
|
+
"""Ошибка при обращении к SOAP-методу.
|
|
8
|
+
|
|
9
|
+
Используется в исключениях связанных с ``SoapClientManager``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, action: str, detail: str, params: dict[Any, Any] | None = None) -> None:
|
|
13
|
+
"""Инициализатор класса ``SoapError``.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
action (``str``): Действие, которое было вызвано клиентом.
|
|
17
|
+
detail (``str``): Дополнительные подробности об операции.
|
|
18
|
+
params (``dict | None``, ``optional``): Дополнительные параметры,
|
|
19
|
+
указанные для action. Defaults to None.
|
|
20
|
+
"""
|
|
21
|
+
super().__init__(f"SOAP-метод {action!r} завершился ошибкой: {detail}")
|
|
22
|
+
self.action = action
|
|
23
|
+
self.detail = detail
|
|
24
|
+
self.params = params
|
soap_client/manager.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""File contains implementation of the SoapClientManager."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from asyncio import Semaphore
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
from aiolimiter import AsyncLimiter
|
|
8
|
+
from zeep import AsyncClient
|
|
9
|
+
from zeep.exceptions import Error
|
|
10
|
+
from zeep.transports import AsyncTransport
|
|
11
|
+
|
|
12
|
+
from soap_client.exceptions import SoapError
|
|
13
|
+
from soap_client.settings import BaseSoapClientSettings
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SoapClientManager:
|
|
19
|
+
"""Класс-обёртка над клиентом zeep для работы с 1с.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
config (``SoapClientConfig``): Конфигурация SOAP-клиента.
|
|
23
|
+
timeout (``int``): Максимальное время ожидания ответа.
|
|
24
|
+
client (``zeep.AsyncClient | None``): SOAP-клиент.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, settings: BaseSoapClientSettings, *, timeout: int = 120) -> None:
|
|
28
|
+
"""Инициализатор класса ``SoapClientManager``.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
settings (``SoapClientConfig``): Конфигурация SOAP-клиента.
|
|
32
|
+
timeout (``int``, ``optional``): Максимальное время ожидания. Defaults to 120.
|
|
33
|
+
"""
|
|
34
|
+
self.settings = settings
|
|
35
|
+
self.timeout = timeout
|
|
36
|
+
self.client: AsyncClient | None = None
|
|
37
|
+
self.__semaphore = Semaphore(settings.max_concurrent)
|
|
38
|
+
self.__limiter = AsyncLimiter(settings.rate_limit, 1)
|
|
39
|
+
logger.debug("Manager created for wsdl=%r", settings.wsdl)
|
|
40
|
+
|
|
41
|
+
def init(self) -> None:
|
|
42
|
+
"""Создаёт zeep-клиент и настраивает подключение.
|
|
43
|
+
|
|
44
|
+
Note:
|
|
45
|
+
Необходимо единоразово вызвать (к примеру: в ``lifespan`` FastAPI).
|
|
46
|
+
"""
|
|
47
|
+
logger.debug("Initializing client for wsdl=%r", self.settings.wsdl)
|
|
48
|
+
if self.is_initialized:
|
|
49
|
+
logger.warning("Trying to init client when that inited already.")
|
|
50
|
+
return
|
|
51
|
+
try:
|
|
52
|
+
transport = AsyncTransport(
|
|
53
|
+
timeout=self.timeout,
|
|
54
|
+
operation_timeout=self.timeout,
|
|
55
|
+
)
|
|
56
|
+
if self.settings.login and self.settings.password:
|
|
57
|
+
transport.wsdl_client.auth = (self.settings.login, self.settings.password)
|
|
58
|
+
transport.client.auth = (self.settings.login, self.settings.password)
|
|
59
|
+
else:
|
|
60
|
+
logger.debug("No auth credentials provided; skipping Basic auth")
|
|
61
|
+
logger.debug("zeep AsyncTransport created")
|
|
62
|
+
|
|
63
|
+
wsdl = self.settings.wsdl
|
|
64
|
+
self.client = AsyncClient(wsdl=wsdl, transport=transport)
|
|
65
|
+
logger.info("zeep client created for wsdl=%r", self.settings.wsdl)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.debug("Incorrect init data", exc_info=True)
|
|
68
|
+
raise SoapError(action="<init>", detail=str(e)) from e
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def is_initialized(self) -> bool:
|
|
72
|
+
"""Свойство - инициализирован ли SOAP-клиент из zeep.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
``bool``: Если инициализирован - ``true``, иначе ``false``.
|
|
76
|
+
"""
|
|
77
|
+
return self.client is not None
|
|
78
|
+
|
|
79
|
+
async def load(self, action: str, params: dict[str, Any] | None = None) -> object:
|
|
80
|
+
"""Вызывает SOAP-метод по имени.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
action (``str``): Имя метода, как объявлено в WSDL.
|
|
84
|
+
params (``dict[str, Any] | None``): Параметры, которые
|
|
85
|
+
необходимо отправить вместе с действием.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
``object``: Результат, который вернул ``zeep`` клиент.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
``RuntimeError``: Если клиент не инициализирован.
|
|
92
|
+
``AttributeError``: Если требуемого динамического атрибута нет.
|
|
93
|
+
``SoapError``: Если произошла ошибка при работе zeep клиента.
|
|
94
|
+
|
|
95
|
+
See Also:
|
|
96
|
+
- :class:`SoapError`: Класс ошибок SOAP-подключений.
|
|
97
|
+
- :class:`BaseSoapClientSettings`: Базовый класс настроек.
|
|
98
|
+
"""
|
|
99
|
+
if self.client is None:
|
|
100
|
+
logger.error("Trying to load without inited client.")
|
|
101
|
+
raise RuntimeError("Client didn't init.")
|
|
102
|
+
logger.debug("load(%r) params=%r", action, params)
|
|
103
|
+
try:
|
|
104
|
+
method = getattr(self.client.service, action)
|
|
105
|
+
except AttributeError:
|
|
106
|
+
logger.exception("Unknown action %r", action)
|
|
107
|
+
raise
|
|
108
|
+
async with self.__limiter, self.__semaphore:
|
|
109
|
+
try:
|
|
110
|
+
result = await method(**(params or {}))
|
|
111
|
+
except Error as e:
|
|
112
|
+
logger.debug("load(%r) failed params=%r.", action, params, exc_info=True)
|
|
113
|
+
raise SoapError(action=action, detail=str(e)) from e
|
|
114
|
+
else:
|
|
115
|
+
logger.debug("load(%r) ok.", action)
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
async def aclose(self) -> None:
|
|
119
|
+
"""Функция для закрытия клиента."""
|
|
120
|
+
logger.debug("Closing client for wsdl=%r.", self.settings.wsdl)
|
|
121
|
+
if self.client is not None:
|
|
122
|
+
transport = cast("AsyncTransport", self.client.transport)
|
|
123
|
+
await transport.aclose()
|
|
124
|
+
self.client = None
|
|
125
|
+
logger.info("Client closed for wsdl=%r.", self.settings.wsdl)
|
|
126
|
+
else:
|
|
127
|
+
logger.warning("Trying to close client when that didn't init.")
|
soap_client/py.typed
ADDED
|
File without changes
|
soap_client/registry.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""File contains implementation of the SoapRegistry."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
|
|
6
|
+
from soap_client.manager import SoapClientManager
|
|
7
|
+
from soap_client.settings import BaseSoapClientSettings
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SoapRegistry:
|
|
13
|
+
"""Класс для запуска, настройки, проверки и закрытия SOAP-клиентов.
|
|
14
|
+
|
|
15
|
+
See Also:
|
|
16
|
+
- :class:`SoapClientManager`: Менеджер SOAP-клиента.
|
|
17
|
+
- :class:`BaseSoapClientSettings`: Базовый класс настроек.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, settings: Mapping[str, BaseSoapClientSettings]) -> None:
|
|
21
|
+
"""Инициализатор класса ``SoapRegistry``.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
settings (``SoapClientSettings``): Настройки для клиентов.
|
|
25
|
+
"""
|
|
26
|
+
self._settings = dict(settings)
|
|
27
|
+
self._clients: dict[str, SoapClientManager] = {}
|
|
28
|
+
logger.debug("Registry created for %d clients: %r", len(settings), list(settings))
|
|
29
|
+
|
|
30
|
+
def init_all(self) -> None:
|
|
31
|
+
"""Инициализация всех клиентов.
|
|
32
|
+
|
|
33
|
+
Note:
|
|
34
|
+
Блокирует event loop на время загрузки WSDL. Вызывайте из lifespan.
|
|
35
|
+
"""
|
|
36
|
+
logger.debug("Initializing all clients")
|
|
37
|
+
for name in self._settings:
|
|
38
|
+
self.get(name)
|
|
39
|
+
logger.info("All clients initialized")
|
|
40
|
+
|
|
41
|
+
def get(self, soap_client_name: str) -> SoapClientManager:
|
|
42
|
+
"""Получить или создать SOAP-клиента.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
soap_client_name (``str``): Имя SOAP-клиента.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
``SoapClientManager``: Запрашиваемый менеджер SOAP-клиента.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
``KeyError``: Возникает если передан незаданный тип клиента.
|
|
52
|
+
``SoapError``: Если не удалось инициализировать клиента.
|
|
53
|
+
|
|
54
|
+
Note:
|
|
55
|
+
При создании выполняет синхронную загрузку WSDL и блокирует
|
|
56
|
+
event loop. Вызывайте из lifespan FastAPI или похожего по роли контекста.
|
|
57
|
+
"""
|
|
58
|
+
if soap_client_name not in self._settings:
|
|
59
|
+
logger.error(
|
|
60
|
+
"Unknown client %r; available: %r",
|
|
61
|
+
soap_client_name,
|
|
62
|
+
list(self._settings),
|
|
63
|
+
)
|
|
64
|
+
raise KeyError(
|
|
65
|
+
f"Unknown soap client: {soap_client_name!r}. Available: {list(self._settings)!r}"
|
|
66
|
+
)
|
|
67
|
+
if soap_client_name not in self._clients:
|
|
68
|
+
logger.debug("Creating client %r", soap_client_name)
|
|
69
|
+
config = self._settings[soap_client_name]
|
|
70
|
+
client = SoapClientManager(config)
|
|
71
|
+
client.init()
|
|
72
|
+
self._clients[soap_client_name] = client
|
|
73
|
+
return self._clients[soap_client_name]
|
|
74
|
+
|
|
75
|
+
async def aclose_all(self) -> None:
|
|
76
|
+
"""Закрыть все соединения."""
|
|
77
|
+
logger.debug("Closing %d clients", len(self._clients))
|
|
78
|
+
for name, client in self._clients.items():
|
|
79
|
+
try:
|
|
80
|
+
await client.aclose()
|
|
81
|
+
except Exception:
|
|
82
|
+
logger.exception("Failed to close client %r", name)
|
|
83
|
+
self._clients.clear()
|
|
84
|
+
logger.info("All clients closed")
|
soap_client/settings.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""File contains implementation of the BaseSoapClientSettings and helpful function."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseSoapClientSettings(BaseSettings):
|
|
10
|
+
"""Базовые настройки SOAP-клиента для взаимодействия с 1с.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
wsdl (``str``): WSDL - контракт интерфейса по взаимодействию с 1С.
|
|
14
|
+
login (``str``): Логин, по которому происходит вход.
|
|
15
|
+
password (``str``): Пароль, по которому происходит вход.
|
|
16
|
+
max_concurrent (``int``): Максимально число одновременных вызовов.
|
|
17
|
+
rate_limit (``int``): Максимальное число запросов в секунду.
|
|
18
|
+
|
|
19
|
+
Note:
|
|
20
|
+
- Используется для создания пользовательских типов конфигураций.
|
|
21
|
+
- Вызовите ``get_model_config`` для создания Pydantic настроек.
|
|
22
|
+
Без этого подкласс унаследует дефолтный конфиг Pydantic, и настройки
|
|
23
|
+
не будут читаться из ``.env``.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Main settings of connection by wsdl
|
|
27
|
+
wsdl: str = Field(min_length=1)
|
|
28
|
+
login: str
|
|
29
|
+
password: str
|
|
30
|
+
# Settings of the connection load
|
|
31
|
+
max_concurrent: int = Field(gt=0)
|
|
32
|
+
rate_limit: int = Field(gt=0)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_model_config(
|
|
36
|
+
env_prefix: str, env_file: str | Path = ".env", env_file_encoding: str = "UTF-8"
|
|
37
|
+
) -> SettingsConfigDict:
|
|
38
|
+
"""Вспомогательная функция для генерации model_config.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
env_prefix (``str``): Префикс в ``.env`` файле,
|
|
42
|
+
который будет использован для настроек.
|
|
43
|
+
env_file (``str | Path``, ``optional``): Путь до ``.env`` файла. Defaults to ".env".
|
|
44
|
+
env_file_encoding (``str``, ``optional``):
|
|
45
|
+
Кодировка ``.env`` файла. Defaults to "UTF-8".
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
SettingsConfigDict: model_config для настроек.
|
|
49
|
+
"""
|
|
50
|
+
return SettingsConfigDict(
|
|
51
|
+
env_file=env_file,
|
|
52
|
+
env_file_encoding=env_file_encoding,
|
|
53
|
+
env_prefix=env_prefix,
|
|
54
|
+
extra="ignore",
|
|
55
|
+
frozen=True,
|
|
56
|
+
case_sensitive=False,
|
|
57
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: soap-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SOAP client for working with services using SOAP and WSDL.
|
|
5
|
+
Author: Freontt
|
|
6
|
+
Author-email: Freontt <freontt.public@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: aiolimiter>=1.3.0
|
|
9
|
+
Requires-Dist: lxml>=6.1.3
|
|
10
|
+
Requires-Dist: pydantic>=2.14.2
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.16.2
|
|
12
|
+
Requires-Dist: zeep[async]>=4.3.3
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Project-URL: Homepage, https://freontt.github.io/soap-client/
|
|
15
|
+
Project-URL: Repository, https://github.com/Freontt/soap-client
|
|
16
|
+
Project-URL: Issues, https://github.com/Freontt/soap-client/issues
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# SOAP-клиент для общения с сервисами по WSDL контракту
|
|
20
|
+
|
|
21
|
+
Данная библиотека предоставляет удобную обёртку для использования SOAP-клиента из библиотеки zeep. Оболочка предназначена в первую очередь для асинхронных приложений.
|
|
22
|
+
|
|
23
|
+
Библиотека состоит из следующих компонентов:
|
|
24
|
+
- [exceptions.py](src/soap_client/exceptions.py): Исключения библиотеки.
|
|
25
|
+
- [manager.py](src/soap_client/manager.py): Manager для управления клиентом.
|
|
26
|
+
- [registry.py](src/soap_client/registry.py): Класс-реестр всех SOAP-клиентов.
|
|
27
|
+
- [settings.py](src/soap_client/settings.py): Настройки для управления клиентом.
|
|
28
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
soap_client/__init__.py,sha256=3ePbw28S_7jeFv-gA3zJlLCZdaOstveU6hjFfQ2p5XE,420
|
|
2
|
+
soap_client/exceptions.py,sha256=h4Yx-dNypD9weiVaEWiT7kDbuuZ67cTaSTdDOwZB2zo,1058
|
|
3
|
+
soap_client/manager.py,sha256=qbrsi3xvcguuZ4ffrfK0qOoNGMwQKtEWhNsRFtYol9c,5586
|
|
4
|
+
soap_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
soap_client/registry.py,sha256=wkspbjwr8TDwuQIGaB_j9S3RKX-udlKiXY3Yhonq9wg,3405
|
|
6
|
+
soap_client/settings.py,sha256=xU0bWCZ296n1Yw1OWIPUrEPCMG6Tr6USBKz9TeaQZiE,2458
|
|
7
|
+
soap_client-0.1.0.dist-info/WHEEL,sha256=MWx7psoF7KNST9G72pGtiBYuaXRXQXBk9pw5R6FzYko,80
|
|
8
|
+
soap_client-0.1.0.dist-info/METADATA,sha256=r0CzA9xq-HTMRPa0ajOOY7q5wLkkVTqmUzkeOXfYsRw,1525
|
|
9
|
+
soap_client-0.1.0.dist-info/RECORD,,
|