soap-client 0.1.0__tar.gz

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.
@@ -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,10 @@
1
+ # SOAP-клиент для общения с сервисами по WSDL контракту
2
+
3
+ Данная библиотека предоставляет удобную обёртку для использования SOAP-клиента из библиотеки zeep. Оболочка предназначена в первую очередь для асинхронных приложений.
4
+
5
+ Библиотека состоит из следующих компонентов:
6
+ - [exceptions.py](src/soap_client/exceptions.py): Исключения библиотеки.
7
+ - [manager.py](src/soap_client/manager.py): Manager для управления клиентом.
8
+ - [registry.py](src/soap_client/registry.py): Класс-реестр всех SOAP-клиентов.
9
+ - [settings.py](src/soap_client/settings.py): Настройки для управления клиентом.
10
+
@@ -0,0 +1,132 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.13.6,<0.14.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "soap-client"
7
+ version = "0.1.0"
8
+ description = "SOAP client for working with services using SOAP and WSDL."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ dependencies = [
13
+ "aiolimiter>=1.3.0",
14
+ "lxml>=6.1.3",
15
+ "pydantic>=2.14.2",
16
+ "pydantic-settings>=2.16.2",
17
+ "zeep[async]>=4.3.3",
18
+ ]
19
+
20
+ [[project.authors]]
21
+ name = "Freontt"
22
+ email = "freontt.public@gmail.com"
23
+
24
+ [project.urls]
25
+ Homepage = "https://freontt.github.io/soap-client/"
26
+ Repository = "https://github.com/Freontt/soap-client"
27
+ Issues = "https://github.com/Freontt/soap-client/issues"
28
+
29
+ [dependency-groups]
30
+ test = [
31
+ "pytest>=9.2.0",
32
+ "pytest-asyncio>=1.4.1.post1",
33
+ "pytest-cov>=7.1.0",
34
+ "pytest-httpserver>=1.2.0",
35
+ "pytest-mock>=3.17.0",
36
+ ]
37
+ docs = [
38
+ "mike>=2.2.0",
39
+ "mkdocs>=1.6.1",
40
+ "mkdocs-material>=9.8.0",
41
+ "mkdocstrings[python]>=1.0.8",
42
+ ]
43
+ lint = [
44
+ "lxml-stubs>=0.5.1",
45
+ "mypy>=2.4.1",
46
+ "ruff>=0.17.3",
47
+ ]
48
+
49
+ [[dependency-groups.dev]]
50
+ include-group = "test"
51
+
52
+ [[dependency-groups.dev]]
53
+ include-group = "docs"
54
+
55
+ [[dependency-groups.dev]]
56
+ include-group = "lint"
57
+
58
+ [tool.ruff]
59
+ target-version = "py313"
60
+ line-length = 100
61
+ exclude = ["tests"]
62
+
63
+ [tool.ruff.lint]
64
+ select = [
65
+ "E",
66
+ "W",
67
+ "F",
68
+ "I",
69
+ "B",
70
+ "S",
71
+ "PERF",
72
+ "RUF",
73
+ "UP",
74
+ "D",
75
+ "ANN",
76
+ "ASYNC",
77
+ "T20",
78
+ "PTH",
79
+ "RET",
80
+ "SIM",
81
+ "ARG",
82
+ "LOG",
83
+ "G",
84
+ "TC",
85
+ "N",
86
+ "C4",
87
+ "RSE",
88
+ "ISC",
89
+ "ICN",
90
+ "TRY",
91
+ "FURB",
92
+ "PL",
93
+ "PT",
94
+ "A",
95
+ ]
96
+ ignore = [
97
+ "RUF002",
98
+ "RUF003",
99
+ "D105",
100
+ "TRY003",
101
+ ]
102
+
103
+ [tool.ruff.lint.pydocstyle]
104
+ convention = "google"
105
+
106
+ [tool.mypy]
107
+ python_version = "3.13"
108
+ files = ["src"]
109
+ strict = true
110
+ show_error_codes = true
111
+ pretty = true
112
+ exclude = [
113
+ "build/",
114
+ "dist/",
115
+ ]
116
+ disable_error_code = ["no-untyped-call"]
117
+ enable_error_code = [
118
+ "redundant-expr",
119
+ "truthy-bool",
120
+ "truthy-iterable",
121
+ "ignore-without-code",
122
+ "possibly-undefined",
123
+ "unused-awaitable",
124
+ "redundant-self",
125
+ "explicit-override",
126
+ "mutable-override",
127
+ ]
128
+
129
+ [tool.pytest]
130
+ asyncio_default_fixture_loop_scope = "function"
131
+ asyncio_default_test_loop_scope = "function"
132
+ markers = ["performance: speed metering"]
@@ -0,0 +1,132 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.13.6,<0.14.0"]
3
+ build-backend = "uv_build"
4
+
5
+
6
+ [project]
7
+ name = "soap-client"
8
+ version = "0.1.0"
9
+ description = "SOAP client for working with services using SOAP and WSDL."
10
+ readme = "README.md"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Freontt", email = "freontt.public@gmail.com" }
14
+ ]
15
+ requires-python = ">=3.11"
16
+ dependencies = [
17
+ "aiolimiter>=1.3.0",
18
+ "lxml>=6.1.3",
19
+ "pydantic>=2.14.2",
20
+ "pydantic-settings>=2.16.2",
21
+ "zeep[async]>=4.3.3",
22
+ ]
23
+
24
+
25
+ [project.urls]
26
+ Homepage = "https://freontt.github.io/soap-client/"
27
+ Repository = "https://github.com/Freontt/soap-client"
28
+ Issues = "https://github.com/Freontt/soap-client/issues"
29
+
30
+
31
+ [dependency-groups]
32
+ test = [
33
+ "pytest>=9.2.0",
34
+ "pytest-asyncio>=1.4.1.post1",
35
+ "pytest-cov>=7.1.0",
36
+ "pytest-httpserver>=1.2.0",
37
+ "pytest-mock>=3.17.0",
38
+ ]
39
+
40
+ docs = [
41
+ "mike>=2.2.0",
42
+ "mkdocs>=1.6.1",
43
+ "mkdocs-material>=9.8.0",
44
+ "mkdocstrings[python]>=1.0.8",
45
+ ]
46
+
47
+ lint = [
48
+ "lxml-stubs>=0.5.1",
49
+ "mypy>=2.4.1",
50
+ "ruff>=0.17.3",
51
+ ]
52
+
53
+ dev = [
54
+ { include-group = "test" },
55
+ { include-group = "docs" },
56
+ { include-group = "lint" },
57
+ ]
58
+
59
+
60
+ [tool.ruff]
61
+ # Set the maximum line length to 100.
62
+ target-version = "py313"
63
+ line-length = 100
64
+ exclude = ["tests"]
65
+
66
+
67
+ [tool.ruff.lint]
68
+ select = [
69
+ "E", # pycodestyle errors
70
+ "W", # pycodestyle warnings
71
+ "F", # pyflakes
72
+ "I", # isort
73
+ "B", # flake8-bugbear
74
+ "S", # flake8-bandit
75
+ "PERF", # perflint
76
+ "RUF", # ruff-specific
77
+ "UP", # pyupgrade
78
+ "D", # pydocstyle
79
+ "ANN", # flake8-annotations
80
+ "ASYNC", # flake8-async
81
+ "T20", # flake8-print
82
+ "PTH", # flake8-use-pathlib
83
+ "RET", # flake8-return
84
+ "SIM", # flake8-simplify
85
+ "ARG", # flake8-unused-arguments
86
+ "LOG", # flake8-logging
87
+ "G", # flake8-logging-format
88
+ "TC", # flake8-type-checking
89
+ "N", # Naming
90
+ "C4", # flake8-comprehensions
91
+ "RSE", # flake8-raise
92
+ "ISC", # flake8-implicit-str-concat
93
+ "ICN", # flake8-import-conventions
94
+ "TRY", # Correct exception's handling
95
+ "FURB", # Idioms
96
+ "PL",
97
+ "PT",
98
+ "A",
99
+ ]
100
+ ignore = ["RUF002", "RUF003", "D105", "TRY003"]
101
+
102
+ [tool.ruff.lint.pydocstyle]
103
+ convention = "google"
104
+
105
+
106
+ [tool.mypy]
107
+ python_version = "3.13"
108
+ files = ["src"]
109
+ strict = true
110
+ show_error_codes = true
111
+ pretty = true
112
+ exclude = ["build/", "dist/"]
113
+ disable_error_code = ["no-untyped-call"]
114
+ enable_error_code = [
115
+ "redundant-expr", # meaningless expressions (x or x)
116
+ "truthy-bool", # objects with __bool__ always True
117
+ "truthy-iterable", # literals, always truthy
118
+ "ignore-without-code", # # type: ignore without the code
119
+ "possibly-undefined", # the variable may not be defined
120
+ "unused-awaitable", # forgotten await
121
+ "redundant-self", # superfluous Self in arguments
122
+ "explicit-override", # require @override
123
+ "mutable-override", # redefinition with mutable type
124
+ ]
125
+
126
+
127
+ [tool.pytest]
128
+ asyncio_default_fixture_loop_scope = "function"
129
+ asyncio_default_test_loop_scope = "function"
130
+ markers = [
131
+ "performance: speed metering",
132
+ ]
@@ -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
@@ -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.")
File without changes
@@ -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")
@@ -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
+ )