pyxroad 1.5.8__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.
pyxroad-1.5.8/PKG-INFO ADDED
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyxroad
3
+ Version: 1.5.8
4
+ Summary: X-Road (Trembita) Python client library for SOAP-based service integration
5
+ Author-email: Andrii Shapovalov <mt.andrey@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/AndreyShapovalovVN/pyxroad
8
+ Project-URL: Repository, https://github.com/AndreyShapovalovVN/pyxroad.git
9
+ Project-URL: Documentation, https://trembita.gov.ua
10
+ Keywords: x-road,xroad,trembita,soap,python
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: System :: Networking
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: lxml>=5.2.1
23
+ Requires-Dist: Requests>=2.32.3
24
+ Requires-Dist: setuptools>=70.0.0
25
+ Requires-Dist: zeep>=4.0.0
26
+ Requires-Dist: redis
27
+ Provides-Extra: redis
28
+ Requires-Dist: redis>=4.5.0; extra == "redis"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
31
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
32
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
33
+ Requires-Dist: black>=23.0.0; extra == "dev"
34
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
35
+
36
+ # X-Road (Trembita) Python Client
37
+
38
+ - **Version:** 1.5.6
39
+ - **Web:** https://trembita.gov.ua
40
+ - **Repository:** https://github.com/AndreyShapovalovVN/pyxroad
41
+ - **Keywords:** x-road, xroad, trembita, python, soap
42
+
43
+ A powerful Python client library for interacting with X-Road (Trembita) security servers.
44
+ This library provides a convenient wrapper around the SOAP-based X-Road protocol, allowing
45
+ developers to easily integrate X-Road services into their Python applications.
46
+
47
+ ## Supported Python Versions
48
+
49
+ - Python 3.10+
50
+ - Python 3.11+
51
+ - Python 3.12+
52
+
53
+ ## Features
54
+
55
+ - Easy-to-use SOAP client for X-Road services
56
+ - Support for multiple cache backends (SQLite, Redis, In-Memory)
57
+ - Automatic SOAP header management
58
+ - Transaction ID tracking
59
+ - Configurable logging
60
+ - Type hints for better IDE support
61
+
62
+ ## Installation from GitHub
63
+
64
+ Using pip:
65
+
66
+ ```bash
67
+ pip install git+https://github.com/AndreyShapovalovVN/pyxroad.git#egg=pyxroad
68
+ ```
69
+
70
+ Or clone and install locally:
71
+
72
+ ```bash
73
+ git clone https://github.com/AndreyShapovalovVN/pyxroad.git
74
+ cd pyxroad
75
+ pip install -e .
76
+ ```
77
+
78
+ ## Requirements
79
+
80
+ - lxml >= 5.2.1
81
+ - Requests >= 2.32.3
82
+ - setuptools >= 68.1.2
83
+ - zeep >= 4.0.0
84
+ - redis (optional, for Redis cache support)
85
+
86
+ ## Code Quality Checks
87
+
88
+ Run the same checks locally as in CI:
89
+
90
+ ```bash
91
+ ruff check . --extend-exclude 'tests/~*.py'
92
+ mypy . --ignore-missing-imports --pretty --show-error-codes --exclude 'tests/~.*\.py$'
93
+ pytest --maxfail=2 --disable-warnings --ignore-glob='tests/~*.py'
94
+ ```
95
+
96
+ ## Quick Start
97
+
98
+ Basic usage example:
99
+
100
+ ```python
101
+ from XRoad import XClient, Transport, SqliteCache
102
+ import logging
103
+ import sys
104
+
105
+ # Configure logging
106
+ logging.basicConfig(
107
+ stream=sys.stdout,
108
+ level=logging.INFO,
109
+ format='%(asctime)s - %(levelname)s - %(name)s - %(message)s'
110
+ )
111
+ _logger = logging.getLogger('XRoad')
112
+
113
+ # Create a client instance
114
+ client = XClient(
115
+ ssu="http://security-server:8080",
116
+ client='SEVDEIR-TEST/GOV/00013480/100001',
117
+ service='SEVDEIR-TEST/GOV/00032684/MIA_prod/CheckPassportStatus/v0.1'
118
+ )
119
+
120
+ # Make a service request
121
+ try:
122
+ response = client.request(
123
+ xroad_id='ABCD123456', # Optional: set custom request ID
124
+ PasNumber='AA123456',
125
+ PasSerial='654321'
126
+ )
127
+ _logger.info(f"Response: {response}")
128
+ except Exception as err:
129
+ _logger.error(f"Error: {err}")
130
+ ```
131
+
132
+ ## Advanced Usage
133
+
134
+ Using custom caching backend:
135
+
136
+ ```python
137
+ from XRoad import XClient, Transport, RedisCache
138
+
139
+ # With Redis cache
140
+ redis_cache = RedisCache(path='redis://localhost:6379/0', timeout=3600)
141
+ transport = Transport(cache=redis_cache)
142
+
143
+ client = XClient(
144
+ ssu="http://security-server:8080",
145
+ client='SEVDEIR-TEST/GOV/00013480/100001',
146
+ service='SEVDEIR-TEST/GOV/00032684/MIA_prod/CheckPassportStatus/v0.1',
147
+ transport=transport
148
+ )
149
+ ```
150
+
151
+ Setting custom headers:
152
+
153
+ ```python
154
+ client.userId = '0123456789' # Custom user ID
155
+ client.id = 'ABCD123456' # Custom request ID
156
+ ```
157
+
158
+ ## Available Cache Types
159
+
160
+ - **InMemoryCache**: Default, stores cache in application memory
161
+ - **SqliteCache**: Persistent cache using SQLite database
162
+ - **RedisCache**: Distributed cache using Redis
163
+
164
+ ## License
165
+
166
+ This project is licensed under the MIT License - see the LICENSE file for details.
167
+
168
+ ## Support
169
+
170
+ For issues, questions, and contributions, please visit:
171
+ https://github.com/AndreyShapovalovVN/pyxroad
@@ -0,0 +1,136 @@
1
+ # X-Road (Trembita) Python Client
2
+
3
+ - **Version:** 1.5.6
4
+ - **Web:** https://trembita.gov.ua
5
+ - **Repository:** https://github.com/AndreyShapovalovVN/pyxroad
6
+ - **Keywords:** x-road, xroad, trembita, python, soap
7
+
8
+ A powerful Python client library for interacting with X-Road (Trembita) security servers.
9
+ This library provides a convenient wrapper around the SOAP-based X-Road protocol, allowing
10
+ developers to easily integrate X-Road services into their Python applications.
11
+
12
+ ## Supported Python Versions
13
+
14
+ - Python 3.10+
15
+ - Python 3.11+
16
+ - Python 3.12+
17
+
18
+ ## Features
19
+
20
+ - Easy-to-use SOAP client for X-Road services
21
+ - Support for multiple cache backends (SQLite, Redis, In-Memory)
22
+ - Automatic SOAP header management
23
+ - Transaction ID tracking
24
+ - Configurable logging
25
+ - Type hints for better IDE support
26
+
27
+ ## Installation from GitHub
28
+
29
+ Using pip:
30
+
31
+ ```bash
32
+ pip install git+https://github.com/AndreyShapovalovVN/pyxroad.git#egg=pyxroad
33
+ ```
34
+
35
+ Or clone and install locally:
36
+
37
+ ```bash
38
+ git clone https://github.com/AndreyShapovalovVN/pyxroad.git
39
+ cd pyxroad
40
+ pip install -e .
41
+ ```
42
+
43
+ ## Requirements
44
+
45
+ - lxml >= 5.2.1
46
+ - Requests >= 2.32.3
47
+ - setuptools >= 68.1.2
48
+ - zeep >= 4.0.0
49
+ - redis (optional, for Redis cache support)
50
+
51
+ ## Code Quality Checks
52
+
53
+ Run the same checks locally as in CI:
54
+
55
+ ```bash
56
+ ruff check . --extend-exclude 'tests/~*.py'
57
+ mypy . --ignore-missing-imports --pretty --show-error-codes --exclude 'tests/~.*\.py$'
58
+ pytest --maxfail=2 --disable-warnings --ignore-glob='tests/~*.py'
59
+ ```
60
+
61
+ ## Quick Start
62
+
63
+ Basic usage example:
64
+
65
+ ```python
66
+ from XRoad import XClient, Transport, SqliteCache
67
+ import logging
68
+ import sys
69
+
70
+ # Configure logging
71
+ logging.basicConfig(
72
+ stream=sys.stdout,
73
+ level=logging.INFO,
74
+ format='%(asctime)s - %(levelname)s - %(name)s - %(message)s'
75
+ )
76
+ _logger = logging.getLogger('XRoad')
77
+
78
+ # Create a client instance
79
+ client = XClient(
80
+ ssu="http://security-server:8080",
81
+ client='SEVDEIR-TEST/GOV/00013480/100001',
82
+ service='SEVDEIR-TEST/GOV/00032684/MIA_prod/CheckPassportStatus/v0.1'
83
+ )
84
+
85
+ # Make a service request
86
+ try:
87
+ response = client.request(
88
+ xroad_id='ABCD123456', # Optional: set custom request ID
89
+ PasNumber='AA123456',
90
+ PasSerial='654321'
91
+ )
92
+ _logger.info(f"Response: {response}")
93
+ except Exception as err:
94
+ _logger.error(f"Error: {err}")
95
+ ```
96
+
97
+ ## Advanced Usage
98
+
99
+ Using custom caching backend:
100
+
101
+ ```python
102
+ from XRoad import XClient, Transport, RedisCache
103
+
104
+ # With Redis cache
105
+ redis_cache = RedisCache(path='redis://localhost:6379/0', timeout=3600)
106
+ transport = Transport(cache=redis_cache)
107
+
108
+ client = XClient(
109
+ ssu="http://security-server:8080",
110
+ client='SEVDEIR-TEST/GOV/00013480/100001',
111
+ service='SEVDEIR-TEST/GOV/00032684/MIA_prod/CheckPassportStatus/v0.1',
112
+ transport=transport
113
+ )
114
+ ```
115
+
116
+ Setting custom headers:
117
+
118
+ ```python
119
+ client.userId = '0123456789' # Custom user ID
120
+ client.id = 'ABCD123456' # Custom request ID
121
+ ```
122
+
123
+ ## Available Cache Types
124
+
125
+ - **InMemoryCache**: Default, stores cache in application memory
126
+ - **SqliteCache**: Persistent cache using SQLite database
127
+ - **RedisCache**: Distributed cache using Redis
128
+
129
+ ## License
130
+
131
+ This project is licensed under the MIT License - see the LICENSE file for details.
132
+
133
+ ## Support
134
+
135
+ For issues, questions, and contributions, please visit:
136
+ https://github.com/AndreyShapovalovVN/pyxroad
@@ -0,0 +1,104 @@
1
+ import logging
2
+ from dataclasses import dataclass
3
+
4
+ _logger = logging.getLogger(__name__)
5
+
6
+
7
+ @dataclass
8
+ class Members:
9
+ """
10
+ This class is used to parse the memberPath and create a dictionary with the member details.
11
+ """
12
+
13
+ objectType: str
14
+ memberPath: str
15
+ xRoadInstance: str | None = None
16
+ memberClass: str | None = None
17
+ memberCode: str | None = None
18
+ subsystemCode: str | None = None
19
+ serviceCode: str | None = None
20
+ serviceVersion: str | None = None
21
+
22
+ def __post_init__(self):
23
+ """
24
+ This method is used to parse the memberPath and assign the values to the class variables.
25
+ :return:
26
+ """
27
+ _logger.debug(f"Members init: {self.memberPath}")
28
+ if self.memberPath:
29
+ for iteration, val in enumerate(self.memberPath.split("/")):
30
+ match iteration:
31
+ case 0:
32
+ self.xRoadInstance = val
33
+ case 1:
34
+ self.memberClass = val
35
+ case 2:
36
+ self.memberCode = val
37
+ case 3:
38
+ self.subsystemCode = val
39
+ case 4:
40
+ self.serviceCode = val
41
+ case 5:
42
+ self.serviceVersion = val
43
+
44
+ @property
45
+ def wsdl_path(self) -> str:
46
+ """
47
+ This method returns the wsdl path for the service.
48
+ :return: String
49
+ """
50
+ if "SERVICE" in self.objectType:
51
+ _path = []
52
+ for key, value in self.member_dict.items():
53
+ if not value:
54
+ _logger.debug(f"Skipping {key} as value is None")
55
+ continue
56
+ if key in ("objectType", "memberPath"):
57
+ _logger.debug(f"Skipping {key}")
58
+ continue
59
+ if value is None:
60
+ _logger.debug(f"Skipping {key}")
61
+ continue
62
+ if "serviceVersion" in key:
63
+ _logger.debug(f"Adding version={value}")
64
+ _path.append(f"version={value}")
65
+ continue
66
+ _logger.debug(f"Adding {key}={value}")
67
+ _path.append(f"{key}={value}")
68
+ _logger.info(f"wsdl path: {_path[:-1]}")
69
+ return "&".join(_path)
70
+ raise ValueError("wsdl_path is only available for SERVICE objectType")
71
+
72
+ def wsdl_url(self, ssu: str) -> str:
73
+ """
74
+ This method returns the wsdl url for the service.
75
+ :param: Ssu - url to securyt server
76
+ :return: String
77
+ """
78
+ _logger.info(f"Generating wsdl url for {ssu}")
79
+ return f"{ssu}/wsdl?{self.wsdl_path}"
80
+
81
+ @property
82
+ def member_dict(self) -> dict[str, str]:
83
+ """
84
+ This method returns the member details in a dictionary format.
85
+ :return: Dict
86
+ """
87
+ _logger.info("Returning member details as dictionary")
88
+ r = {}
89
+ if self.xRoadInstance:
90
+ r["xRoadInstance"] = self.xRoadInstance
91
+ if self.memberClass:
92
+ r["memberClass"] = self.memberClass
93
+ if self.memberCode:
94
+ r["memberCode"] = self.memberCode
95
+ if self.subsystemCode:
96
+ r["subsystemCode"] = self.subsystemCode
97
+ if self.serviceCode:
98
+ r["serviceCode"] = self.serviceCode
99
+ if self.serviceVersion:
100
+ r["serviceVersion"] = self.serviceVersion
101
+ if self.objectType:
102
+ r["objectType"] = self.objectType
103
+
104
+ return r
@@ -0,0 +1,17 @@
1
+ from .client import XClient
2
+ from .transport import DRACTransport
3
+ from zeep.cache import SqliteCache, InMemoryCache
4
+ from zeep.transports import Transport
5
+ from .cache import RedisCache
6
+
7
+ __all__ = [
8
+ "XClient",
9
+ "Transport",
10
+ "DRACTransport",
11
+ "RedisCache",
12
+ "SqliteCache",
13
+ "InMemoryCache",
14
+ ]
15
+
16
+ __author__ = "Andrii Shapovalov"
17
+ __email__ = "mt.andrey@gmail.com"
@@ -0,0 +1,90 @@
1
+ import hashlib
2
+ import logging
3
+ from typing import cast
4
+
5
+ from redis import Redis
6
+ from zeep.cache import Base
7
+
8
+ _logger = logging.getLogger(__name__)
9
+
10
+
11
+ class RedisCache(Base):
12
+ """
13
+ Provides a caching mechanism backed by Redis.
14
+
15
+ This class is designed for caching purposes, used Redis as the underlying
16
+ storage. It supports caching content with a specified time-to-live (TTL) and
17
+ identifying cached data through unique keys derived from URLs. The cache can
18
+ accommodate both storing and retrieving cached data, making it suitable for
19
+ applications requiring temporary storage of frequently accessed resources.
20
+
21
+ :ivar redis_client: A Redis client instance is used to interact with Redis.
22
+ :type redis_client: Redis
23
+ :ivar ttl: The time-to-live (in seconds) for cached content.
24
+ :type ttl: Int
25
+ :ivar prefix: The prefix used to generate cache keys.
26
+ :type prefix: String
27
+ """
28
+
29
+ _version = "1"
30
+
31
+ def __init__(self, path: str | Redis | None = None, timeout: int = 3600):
32
+ """
33
+ Initializes a cache object for managing data with Redis as a backend.
34
+
35
+ :param path: A Redis connection URL as a string or an instance of the Redis client
36
+ object. If none is provided, a TypeError is raised.
37
+ :param timeout: The time-to-live (TTL) for the cached data, specified in seconds.
38
+ Defaults to 3600 seconds (1 hour).
39
+
40
+ :raises TypeError: If `path` is not a string or an instance of Redis.
41
+ """
42
+ if isinstance(path, str):
43
+ self.redis_client = Redis.from_url(path)
44
+ elif isinstance(path, Redis):
45
+ self.redis_client = path
46
+ else:
47
+ raise TypeError("path must be str or Redis")
48
+ self.ttl = timeout
49
+ self.prefix = "zeep:cache:"
50
+
51
+ def _key(self, url: str) -> str:
52
+ digest = hashlib.sha256(url.encode("utf-8")).hexdigest()
53
+ return f"{self.prefix}{digest}"
54
+
55
+ def add(self, url: str, content: bytes):
56
+ """
57
+ Adds content to the cache associated with a specific URL.
58
+
59
+ This function caches the provided content using the URL as a key. The content is
60
+ stored in the cache for the configured time-to-live (TTL) period. The key used for
61
+ caching is generated from the URL, which ensures that the content is identifiable
62
+ and retrievable.
63
+
64
+ :param url: The URL to serve as the key for caching. It must be a string.
65
+ :param content: The content to cache, provided as a sequence of bytes.
66
+ :return: None
67
+ """
68
+ _logger.debug("Caching contents of %s", url)
69
+ key = self._key(url)
70
+ self.redis_client.set(key, content, ex=self.ttl)
71
+
72
+ def get(self, url: str) -> bytes | None:
73
+ """
74
+ Retrieve content from a cache based on the provided URL. If the content is
75
+ found in the cache (cache hit), it is returned. Otherwise, it logs a cache
76
+ miss and returns None.
77
+
78
+ :param url: The URL used to retrieve the cached content.
79
+ :type url: String
80
+ :return: The cached content as bytes if the key exists in the cache; None
81
+ otherwise.
82
+ :rtype: Bytes or None
83
+ """
84
+ key = self._key(url)
85
+ content = cast(bytes | None, self.redis_client.get(key))
86
+ if content:
87
+ _logger.debug("Cache HIT for %s", url)
88
+ return content
89
+ _logger.debug("Cache MISS for %s", url)
90
+ return None
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import uuid
5
+
6
+ from zeep import Client
7
+ from zeep.cache import InMemoryCache
8
+ from zeep.exceptions import Fault
9
+ from zeep.helpers import serialize_object
10
+ from zeep.transports import Transport
11
+
12
+ from .Members import Members
13
+
14
+ _logger = logging.getLogger("XRoad")
15
+
16
+
17
+ class XClient(Client):
18
+ """
19
+ A specialized client class for interacting with X-Road services.
20
+
21
+ This class extends the base Client class and provides a mechanism to interact
22
+ with X-Road systems by implementing SOAP-based service requests. It also manages
23
+ the construction of appropriate SOAP headers and using default namespaces
24
+ required by the X-Road environment. The class is capable of handling session
25
+ setup, ensuring proper initialization of member and service objects, and
26
+ handling request serialization.
27
+
28
+ :ivar response: Holds the response of the latest SOAP request.
29
+ :type response: Any
30
+ """
31
+
32
+ _version = 4.0
33
+
34
+ def __init__(
35
+ self, ssu: str, client: str, service: str, transport: object | None = None, *args, **kwargs
36
+ ):
37
+ """
38
+ Initializes an instance of the class, configuring service and client details along with
39
+ SOAP header and transport setup. Validates the presence of required parameters and sets
40
+ default SOAP headers used for communication.
41
+
42
+ :param ssu: The subsystem uniform resource identifier or endpoint to connect.
43
+ :type ssu: String
44
+ :param client: The client subsystem identifier in the X-Road ecosystem.
45
+ :type client: String
46
+ :param service: The service identifier within the X-Road ecosystem to be accessed.
47
+ :type service: String
48
+ :param transport: Optional transport object for handling HTTP requests. Defaults
49
+ to an instance of `Transport` with in-memory caching if not provided.
50
+ :type transport: Object | None
51
+ :param args: Additional positional arguments to be passed to the parent class
52
+ initializer.
53
+ :param kwargs: Additional keyword arguments to be passed to the parent class
54
+ initializer.
55
+
56
+ :raises ValueError: If the `service` parameter is not provided.
57
+ :raises ValueError: If the `client` parameter is not provided.
58
+ """
59
+
60
+ self.response = None
61
+
62
+ if not service:
63
+ raise ValueError("service - required")
64
+ if not client:
65
+ raise ValueError("client - required")
66
+
67
+ client_member = Members(objectType="SUBSYSTEM", memberPath=client)
68
+ service_member = Members(objectType="SERVICE", memberPath=service)
69
+
70
+ if "transport" not in kwargs:
71
+ kwargs["transport"] = transport if transport else Transport(InMemoryCache(timeout=60))
72
+
73
+ super().__init__(
74
+ service_member.wsdl_url(ssu),
75
+ *args,
76
+ **kwargs,
77
+ )
78
+
79
+ self.transport.session.proxies.update({"http": ssu, })
80
+
81
+ self.set_ns_prefix("xro", "https://x-road.eu/xsd/xroad.xsd")
82
+ self.set_ns_prefix("iden", "https://x-road.eu/xsd/identifiers")
83
+
84
+ self.set_default_soapheaders(
85
+ {
86
+ "client": client_member.member_dict,
87
+ "service": service_member.member_dict,
88
+ "userId": client_member.subsystemCode,
89
+ "id": uuid.uuid4().hex,
90
+ "protocolVersion": self._version,
91
+ }
92
+ )
93
+ _logger.debug("Default header (%s)", self._default_soapheaders)
94
+
95
+ def request(self, **kwargs):
96
+ """
97
+ Handles SOAP service requests with the ability to specify custom arguments and
98
+ default configurations. The method attempts to process the request with the
99
+ specified arguments, perform serialization of the response object, and handle
100
+ any potential SOAP Fault exceptions that may occur.
101
+
102
+ :param kwargs: Arbitrary keyword arguments to be passed to the SOAP service
103
+ request. These may include service-specific parameters or optional settings.
104
+ If an argument with the key 'xroad_id' is supplied, it will set the 'id'
105
+ attribute of the instance.
106
+ :return: Serialized response object from the SOAP service.
107
+ :rtype: Any
108
+ :raises Fault: If a SOAP Fault exception occurs during the service call, it is
109
+ raised after logging the error details.
110
+ """
111
+
112
+ service = self._default_soapheaders["service"].get("serviceCode")
113
+ if "xroad_id" in kwargs:
114
+ self.id = kwargs.pop("xroad_id")
115
+
116
+ try:
117
+ response = self.service[service](**kwargs)
118
+ except Fault as error:
119
+ _logger.error("service error (%s: %s)", error.code, error.message)
120
+ raise Fault(error)
121
+ else:
122
+ s_object = serialize_object(response)
123
+ _logger.debug("Response (%s)", s_object)
124
+ return s_object
125
+
126
+ @property
127
+ def id(self):
128
+ """
129
+ Provides access to the `id` property representing a specific identifier contained within the
130
+ default SOAP header. This property fetches and returns the associated identifier value from
131
+ a predefined key called "id" in the default SOAP headers.
132
+
133
+ The property is read-only and allows retrieval of the value, but no direct modifications
134
+ to the object’s underlying internal data structures.
135
+
136
+ :rtype: Any
137
+ :return: The value corresponding to the "id" key in the default SOAP headers.
138
+ """
139
+ return self._default_soapheaders.get("id")
140
+
141
+ @id.setter
142
+ def id(self, value):
143
+ """
144
+ Sets the value of the `id` attribute and updates the corresponding SOAP
145
+ headers. This setter ensures that the `id` value is synchronized with
146
+ the default SOAP headers and logs the operation for debugging purposes.
147
+
148
+ :param value: The new value to set for the `id` attribute. This value
149
+ is used to update the `_default_soapheaders`.
150
+ :type value: Any
151
+ """
152
+
153
+ self._default_soapheaders["id"] = value
154
+ self.set_default_soapheaders(self._default_soapheaders)
155
+ _logger.debug("Set (id: %s)", value)
156
+
157
+ @property
158
+ def userId(self) -> str | None:
159
+ """
160
+ Provides access to the userId obtained from default SOAP headers.
161
+
162
+ This property is a getter that retrieves the value of the "userId" field
163
+ from the default SOAP headers set in the object. It is meant to provide a
164
+ convenient way to access this specific information if it exists in the
165
+ SOAP headers.
166
+
167
+ :rtype: String
168
+ :return: The value of "userId" from the default SOAP headers if it exists,
169
+ otherwise returns None.
170
+ """
171
+ return self._default_soapheaders.get("userId") # type: ignore[no-any-return]
172
+
173
+ @userId.setter
174
+ def userId(self, value: str):
175
+ """
176
+ Sets the userId value into the default SOAP headers and updates the headers accordingly.
177
+ Logs the userId value upon setting for debugging purposes.
178
+
179
+ :param value: The userId value to be set in the default SOAP headers.
180
+ :type value: String
181
+ """
182
+
183
+ self._default_soapheaders["userId"] = value
184
+ self.set_default_soapheaders(self._default_soapheaders)
185
+ _logger.debug("Set (userId: %s)", value)
@@ -0,0 +1,55 @@
1
+ import datetime
2
+ import logging
3
+ from email.utils import parsedate_to_datetime
4
+
5
+ from zeep.plugins import HistoryPlugin
6
+
7
+ _logger = logging.getLogger(__name__)
8
+
9
+
10
+ class UXPHistoryPlugin(HistoryPlugin):
11
+ """
12
+ Represents a plugin for handling history-related functionality specific to UXP.
13
+
14
+ This class is a specialized extension of the generic HistoryPlugin, designed to
15
+ handle UXP-specific headers, such as transaction ID and transaction date. The
16
+ primary purpose of this plugin is to extract and provide easy access to these
17
+ pieces of information from HTTP headers.
18
+
19
+ :ivar last_received: Stores the last received HTTP headers and potentially other
20
+ related data.
21
+ :type last_received: dict
22
+ """
23
+ @property
24
+ def transaction_id(self) -> str:
25
+ """
26
+ Retrieves the transaction ID from the HTTP headers of the last received
27
+ request. This ID is used to track a specific transaction across
28
+ the system.
29
+
30
+ :rtype: String
31
+ :return: The transaction ID as a string. If the "uxp-transaction-id" header
32
+ is not present, an empty string is returned.
33
+ """
34
+ transaction: str = self.last_received["http_headers"].get( # type: ignore
35
+ "uxp-transaction-id", ""
36
+ )
37
+ return transaction
38
+
39
+ @property
40
+ def transaction_date(self) -> datetime.datetime | None:
41
+ """
42
+ Retrieves the transaction date from the "Date" field in the HTTP headers. If the "Date"
43
+ field is not a valid datetime format, the current datetime will be returned instead.
44
+
45
+ :return: The transaction date parsed as a `datetime.datetime` object or the current
46
+ datetime if parsing fails.
47
+ :rtype: datetime.datetime | None
48
+ """
49
+ try:
50
+ date = parsedate_to_datetime(
51
+ self.last_received["http_headers"].get("Date", "") # type: ignore
52
+ )
53
+ except ValueError:
54
+ date = datetime.datetime.now()
55
+ return date
@@ -0,0 +1,31 @@
1
+ import logging
2
+
3
+ from lxml import etree
4
+ from zeep.transports import Transport
5
+
6
+ _logger = logging.getLogger("DRACTransport")
7
+
8
+
9
+ class DRACTransport(Transport):
10
+ """
11
+ Handles communication with the DRAC (Державни Реестр Актових Записів) service.
12
+
13
+ This class is responsible for sending messages to the DRAC service by formatting
14
+ XML envelopes appropriately. It used the Transport parent class for underlying
15
+ communication mechanisms.
16
+ """
17
+
18
+ def post_xml(self, address, envelope, headers):
19
+ message = etree.tostring(
20
+ envelope, pretty_print=True, xml_declaration=True, encoding="utf-8"
21
+ ).decode("utf-8")
22
+
23
+ _logger.debug(f"Source: \n {message}")
24
+ drac_message = ""
25
+ for line in message.splitlines():
26
+ drac_message += line
27
+ if "iden:" not in line:
28
+ drac_message += "\n"
29
+ _logger.debug(f"Modified: \n {drac_message}")
30
+
31
+ return self.post(address, drac_message.encode("utf-8"), headers)
@@ -0,0 +1,66 @@
1
+ [build-system]
2
+ requires = ["setuptools>=70.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyxroad"
7
+ version = "1.5.8"
8
+ description = "X-Road (Trembita) Python client library for SOAP-based service integration"
9
+ authors = [{ name = "Andrii Shapovalov", email = "mt.andrey@gmail.com" }]
10
+ readme = "README.md"
11
+ license = { text = "MIT" }
12
+ requires-python = ">=3.10"
13
+ keywords = ["x-road", "xroad", "trembita", "soap", "python"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ "Topic :: System :: Networking",
24
+ ]
25
+ dependencies = [
26
+ "lxml>=5.2.1",
27
+ "Requests>=2.32.3",
28
+ "setuptools>=70.0.0",
29
+ "zeep>=4.0.0",
30
+ "redis",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ redis = ["redis>=4.5.0"]
35
+ dev = [
36
+ "pytest>=8.0.0",
37
+ "pytest-cov>=4.0.0",
38
+ "mypy>=1.0.0",
39
+ "black>=23.0.0",
40
+ "flake8>=6.0.0",
41
+ ]
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/AndreyShapovalovVN/pyxroad"
45
+ Repository = "https://github.com/AndreyShapovalovVN/pyxroad.git"
46
+ Documentation = "https://trembita.gov.ua"
47
+
48
+ [tool.setuptools]
49
+ packages = ["XRoad"]
50
+
51
+ [tool.setuptools.package-data]
52
+ XRoad = ["py.typed"]
53
+
54
+ [tool.mypy]
55
+ python_version = "3.10"
56
+ warn_return_any = true
57
+ warn_unused_configs = true
58
+ disallow_untyped_defs = false
59
+ disallow_incomplete_defs = false
60
+
61
+ [tool.ruff]
62
+ target-version = "py310"
63
+
64
+ [tool.black]
65
+ line-length = 100
66
+ target-version = ["py310", "py311", "py312"]
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyxroad
3
+ Version: 1.5.8
4
+ Summary: X-Road (Trembita) Python client library for SOAP-based service integration
5
+ Author-email: Andrii Shapovalov <mt.andrey@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/AndreyShapovalovVN/pyxroad
8
+ Project-URL: Repository, https://github.com/AndreyShapovalovVN/pyxroad.git
9
+ Project-URL: Documentation, https://trembita.gov.ua
10
+ Keywords: x-road,xroad,trembita,soap,python
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: System :: Networking
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: lxml>=5.2.1
23
+ Requires-Dist: Requests>=2.32.3
24
+ Requires-Dist: setuptools>=70.0.0
25
+ Requires-Dist: zeep>=4.0.0
26
+ Requires-Dist: redis
27
+ Provides-Extra: redis
28
+ Requires-Dist: redis>=4.5.0; extra == "redis"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
31
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
32
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
33
+ Requires-Dist: black>=23.0.0; extra == "dev"
34
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
35
+
36
+ # X-Road (Trembita) Python Client
37
+
38
+ - **Version:** 1.5.6
39
+ - **Web:** https://trembita.gov.ua
40
+ - **Repository:** https://github.com/AndreyShapovalovVN/pyxroad
41
+ - **Keywords:** x-road, xroad, trembita, python, soap
42
+
43
+ A powerful Python client library for interacting with X-Road (Trembita) security servers.
44
+ This library provides a convenient wrapper around the SOAP-based X-Road protocol, allowing
45
+ developers to easily integrate X-Road services into their Python applications.
46
+
47
+ ## Supported Python Versions
48
+
49
+ - Python 3.10+
50
+ - Python 3.11+
51
+ - Python 3.12+
52
+
53
+ ## Features
54
+
55
+ - Easy-to-use SOAP client for X-Road services
56
+ - Support for multiple cache backends (SQLite, Redis, In-Memory)
57
+ - Automatic SOAP header management
58
+ - Transaction ID tracking
59
+ - Configurable logging
60
+ - Type hints for better IDE support
61
+
62
+ ## Installation from GitHub
63
+
64
+ Using pip:
65
+
66
+ ```bash
67
+ pip install git+https://github.com/AndreyShapovalovVN/pyxroad.git#egg=pyxroad
68
+ ```
69
+
70
+ Or clone and install locally:
71
+
72
+ ```bash
73
+ git clone https://github.com/AndreyShapovalovVN/pyxroad.git
74
+ cd pyxroad
75
+ pip install -e .
76
+ ```
77
+
78
+ ## Requirements
79
+
80
+ - lxml >= 5.2.1
81
+ - Requests >= 2.32.3
82
+ - setuptools >= 68.1.2
83
+ - zeep >= 4.0.0
84
+ - redis (optional, for Redis cache support)
85
+
86
+ ## Code Quality Checks
87
+
88
+ Run the same checks locally as in CI:
89
+
90
+ ```bash
91
+ ruff check . --extend-exclude 'tests/~*.py'
92
+ mypy . --ignore-missing-imports --pretty --show-error-codes --exclude 'tests/~.*\.py$'
93
+ pytest --maxfail=2 --disable-warnings --ignore-glob='tests/~*.py'
94
+ ```
95
+
96
+ ## Quick Start
97
+
98
+ Basic usage example:
99
+
100
+ ```python
101
+ from XRoad import XClient, Transport, SqliteCache
102
+ import logging
103
+ import sys
104
+
105
+ # Configure logging
106
+ logging.basicConfig(
107
+ stream=sys.stdout,
108
+ level=logging.INFO,
109
+ format='%(asctime)s - %(levelname)s - %(name)s - %(message)s'
110
+ )
111
+ _logger = logging.getLogger('XRoad')
112
+
113
+ # Create a client instance
114
+ client = XClient(
115
+ ssu="http://security-server:8080",
116
+ client='SEVDEIR-TEST/GOV/00013480/100001',
117
+ service='SEVDEIR-TEST/GOV/00032684/MIA_prod/CheckPassportStatus/v0.1'
118
+ )
119
+
120
+ # Make a service request
121
+ try:
122
+ response = client.request(
123
+ xroad_id='ABCD123456', # Optional: set custom request ID
124
+ PasNumber='AA123456',
125
+ PasSerial='654321'
126
+ )
127
+ _logger.info(f"Response: {response}")
128
+ except Exception as err:
129
+ _logger.error(f"Error: {err}")
130
+ ```
131
+
132
+ ## Advanced Usage
133
+
134
+ Using custom caching backend:
135
+
136
+ ```python
137
+ from XRoad import XClient, Transport, RedisCache
138
+
139
+ # With Redis cache
140
+ redis_cache = RedisCache(path='redis://localhost:6379/0', timeout=3600)
141
+ transport = Transport(cache=redis_cache)
142
+
143
+ client = XClient(
144
+ ssu="http://security-server:8080",
145
+ client='SEVDEIR-TEST/GOV/00013480/100001',
146
+ service='SEVDEIR-TEST/GOV/00032684/MIA_prod/CheckPassportStatus/v0.1',
147
+ transport=transport
148
+ )
149
+ ```
150
+
151
+ Setting custom headers:
152
+
153
+ ```python
154
+ client.userId = '0123456789' # Custom user ID
155
+ client.id = 'ABCD123456' # Custom request ID
156
+ ```
157
+
158
+ ## Available Cache Types
159
+
160
+ - **InMemoryCache**: Default, stores cache in application memory
161
+ - **SqliteCache**: Persistent cache using SQLite database
162
+ - **RedisCache**: Distributed cache using Redis
163
+
164
+ ## License
165
+
166
+ This project is licensed under the MIT License - see the LICENSE file for details.
167
+
168
+ ## Support
169
+
170
+ For issues, questions, and contributions, please visit:
171
+ https://github.com/AndreyShapovalovVN/pyxroad
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ XRoad/Members.py
4
+ XRoad/__init__.py
5
+ XRoad/cache.py
6
+ XRoad/client.py
7
+ XRoad/plugins.py
8
+ XRoad/transport.py
9
+ pyxroad.egg-info/PKG-INFO
10
+ pyxroad.egg-info/SOURCES.txt
11
+ pyxroad.egg-info/dependency_links.txt
12
+ pyxroad.egg-info/requires.txt
13
+ pyxroad.egg-info/top_level.txt
14
+ tests/test_members.py
@@ -0,0 +1,15 @@
1
+ lxml>=5.2.1
2
+ Requests>=2.32.3
3
+ setuptools>=70.0.0
4
+ zeep>=4.0.0
5
+ redis
6
+
7
+ [dev]
8
+ pytest>=8.0.0
9
+ pytest-cov>=4.0.0
10
+ mypy>=1.0.0
11
+ black>=23.0.0
12
+ flake8>=6.0.0
13
+
14
+ [redis]
15
+ redis>=4.5.0
@@ -0,0 +1 @@
1
+ XRoad
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,80 @@
1
+ import unittest
2
+
3
+ from XRoad.Members import Members
4
+
5
+
6
+ class TestMembers(unittest.TestCase):
7
+ def test_initialization_with_full_path(self):
8
+ member = Members(
9
+ objectType="SERVICE",
10
+ memberPath="instance/class/code/subsystem/service/serviceVersion",
11
+ )
12
+
13
+ self.assertEqual(member.xRoadInstance, "instance")
14
+ self.assertEqual(member.memberClass, "class")
15
+ self.assertEqual(member.memberCode, "code")
16
+ self.assertEqual(member.subsystemCode, "subsystem")
17
+ self.assertEqual(member.serviceCode, "service")
18
+ self.assertEqual(member.serviceVersion, "serviceVersion")
19
+
20
+ def test_initialization_with_partial_path(self):
21
+ member = Members(objectType="SERVICE", memberPath="instance/class")
22
+
23
+ self.assertEqual(member.xRoadInstance, "instance")
24
+ self.assertEqual(member.memberClass, "class")
25
+ self.assertIsNone(member.memberCode)
26
+ self.assertIsNone(member.subsystemCode)
27
+ self.assertIsNone(member.serviceCode)
28
+ self.assertIsNone(member.serviceVersion)
29
+
30
+ def test_wsdl_path_generation(self):
31
+ member = Members(
32
+ objectType="SERVICE",
33
+ memberPath="instance/class/code/subsystem/service/serviceVersion",
34
+ )
35
+ expected_wsdl_path = "xRoadInstance=instance&memberClass=class&memberCode=code&subsystemCode=subsystem&serviceCode=service&version=serviceVersion"
36
+
37
+ self.assertEqual(member.wsdl_path, expected_wsdl_path)
38
+
39
+ def test_wsdl_url(self):
40
+ member = Members(
41
+ objectType="SERVICE",
42
+ memberPath="instance/class/code/subsystem/service/serviceVersion",
43
+ )
44
+ ssu = "https://example.com"
45
+ expected_wsdl_path = "xRoadInstance=instance&memberClass=class&memberCode=code&subsystemCode=subsystem&serviceCode=service&version=serviceVersion"
46
+ expected_url = f"{ssu}/wsdl?{expected_wsdl_path}"
47
+
48
+ self.assertEqual(member.wsdl_url(ssu), expected_url)
49
+
50
+ def test_member_dict(self):
51
+ member = Members(
52
+ objectType="SERVICE",
53
+ memberPath="instance/class/code/subsystem/service/serviceVersion",
54
+ )
55
+ expected_dict = {
56
+ "xRoadInstance": "instance",
57
+ "memberClass": "class",
58
+ "memberCode": "code",
59
+ "subsystemCode": "subsystem",
60
+ "serviceCode": "service",
61
+ "serviceVersion": "serviceVersion",
62
+ "objectType": "SERVICE",
63
+ }
64
+
65
+ self.assertDictEqual(member.member_dict, expected_dict)
66
+
67
+ def test_wsdl_path_none_for_non_service(self):
68
+ member = Members(
69
+ objectType="OTHER_TYPE",
70
+ memberPath="instance/class/code/subsystem/service/serviceVersion",
71
+ )
72
+ with self.assertRaises(ValueError) as ctx:
73
+ _ = member.wsdl_path
74
+ self.assertEqual(
75
+ str(ctx.exception), "wsdl_path is only available for SERVICE objectType"
76
+ )
77
+
78
+
79
+ if __name__ == "__main__":
80
+ unittest.main()