onewayswp 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.
onewayswp/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ from .admin import Admin
2
+ from .client import Client
3
+ from .info import info
4
+ from .exceptions import (
5
+ OnewaySWPError,
6
+ InvalidTokenError,
7
+ ExpiredError,
8
+ ClockRollbackError,
9
+ SecurityError,
10
+ InvalidKeyError,
11
+ CorruptedFileError,
12
+ InsufficientCreditError,
13
+ FileLockedError,
14
+ PlatformNotSupportedError,
15
+ CoreLoadError,
16
+ NoActiveSessionError,
17
+ )
18
+
19
+ __version__ = "0.1.0"
20
+ __all__ = [
21
+ "Admin",
22
+ "Client",
23
+ "info",
24
+ "OnewaySWPError",
25
+ "InvalidTokenError",
26
+ "ExpiredError",
27
+ "ClockRollbackError",
28
+ "SecurityError",
29
+ "InvalidKeyError",
30
+ "CorruptedFileError",
31
+ "InsufficientCreditError",
32
+ "FileLockedError",
33
+ "PlatformNotSupportedError",
34
+ "CoreLoadError",
35
+ "NoActiveSessionError",
36
+ ]
onewayswp/_core.py ADDED
@@ -0,0 +1,220 @@
1
+ import ctypes
2
+ import json
3
+ import platform
4
+ import time
5
+ from pathlib import Path
6
+ from ctypes import c_char_p, c_void_p, c_int64
7
+
8
+ from .exceptions import (
9
+ PlatformNotSupportedError,
10
+ CoreLoadError,
11
+ FileLockedError,
12
+ )
13
+
14
+
15
+ SUPPORTED_PLATFORMS = {
16
+ ("windows", "x64"): "onewayswp_core.dll",
17
+ ("windows", "arm64"): "onewayswp_core.dll",
18
+ ("windows", "x86"): "onewayswp_core.dll",
19
+ ("linux", "x64"): "libonewayswp_core.so",
20
+ ("linux", "arm64"): "libonewayswp_core.so",
21
+ ("linux", "armv7"): "libonewayswp_core.so",
22
+ ("macos", "x64"): "libonewayswp_core.dylib",
23
+ ("macos", "arm64"): "libonewayswp_core.dylib",
24
+ ("android", "arm64"): "libonewayswp_core.so",
25
+ ("android", "armv7"): "libonewayswp_core.so",
26
+ ("android", "x64"): "libonewayswp_core.so",
27
+ ("android", "x86"): "libonewayswp_core.so",
28
+ }
29
+
30
+
31
+ def detect_platform() -> tuple:
32
+ system = platform.system().lower()
33
+ machine = platform.machine().lower()
34
+
35
+ if system == "windows":
36
+ os_name = "windows"
37
+ elif system == "darwin":
38
+ os_name = "macos"
39
+ elif system == "linux":
40
+ if "android" in platform.platform().lower():
41
+ os_name = "android"
42
+ else:
43
+ os_name = "linux"
44
+ else:
45
+ os_name = system
46
+
47
+ if machine in ("x86_64", "amd64"):
48
+ arch = "x64"
49
+ elif machine in ("aarch64", "arm64"):
50
+ arch = "arm64"
51
+ elif machine.startswith("armv7") or machine == "armv7l":
52
+ arch = "armv7"
53
+ elif machine in ("i386", "i686", "x86"):
54
+ arch = "x86"
55
+ else:
56
+ arch = machine
57
+
58
+ return os_name, arch
59
+
60
+
61
+ def find_library() -> str:
62
+ os_name, arch = detect_platform()
63
+ key = (os_name, arch)
64
+
65
+ if key not in SUPPORTED_PLATFORMS:
66
+ supported = ", ".join(f"{o}-{a}" for o, a in sorted(SUPPORTED_PLATFORMS))
67
+ raise PlatformNotSupportedError(
68
+ f"Platform '{os_name}-{arch}' is not supported. Supported: {supported}"
69
+ )
70
+
71
+ lib_name = SUPPORTED_PLATFORMS[key]
72
+ base = Path(__file__).parent / "lib"
73
+ specific = base / f"{os_name}-{arch}" / lib_name
74
+
75
+ if not specific.exists():
76
+ raise CoreLoadError(
77
+ f"Binary not found for {os_name}-{arch}. Expected at: {specific}"
78
+ )
79
+
80
+ return str(specific)
81
+
82
+
83
+ class CoreLib:
84
+ _instance = None
85
+
86
+ def __new__(cls):
87
+ if cls._instance is None:
88
+ cls._instance = super().__new__(cls)
89
+ cls._instance._load()
90
+ return cls._instance
91
+
92
+ def _load(self) -> None:
93
+ lib_path = find_library()
94
+
95
+ try:
96
+ loader = ctypes.WinDLL if platform.system() == "Windows" else ctypes.CDLL
97
+ self.lib = loader(lib_path)
98
+ except OSError as e:
99
+ raise CoreLoadError(f"Failed to load core library: {e}")
100
+
101
+ self.lib.owswp_version.restype = c_void_p
102
+ self.lib.owswp_version.argtypes = []
103
+
104
+ self.lib.owswp_info.restype = c_void_p
105
+ self.lib.owswp_info.argtypes = []
106
+
107
+ self.lib.owswp_generate_key_pair.restype = c_void_p
108
+ self.lib.owswp_generate_key_pair.argtypes = []
109
+
110
+ self.lib.owswp_validate_key_pair.restype = c_void_p
111
+ self.lib.owswp_validate_key_pair.argtypes = [c_char_p, c_char_p]
112
+
113
+ self.lib.owswp_create_license.restype = c_void_p
114
+ self.lib.owswp_create_license.argtypes = [c_char_p, c_char_p, c_char_p]
115
+
116
+ self.lib.owswp_validate_key.restype = c_void_p
117
+ self.lib.owswp_validate_key.argtypes = [c_char_p, c_char_p, c_char_p]
118
+
119
+ self.lib.owswp_inspect_key.restype = c_void_p
120
+ self.lib.owswp_inspect_key.argtypes = [c_char_p, c_char_p]
121
+
122
+ self.lib.owswp_protection_verify.restype = c_void_p
123
+ self.lib.owswp_protection_verify.argtypes = [c_char_p, c_char_p]
124
+
125
+ self.lib.owswp_decrement_credit.restype = c_void_p
126
+ self.lib.owswp_decrement_credit.argtypes = [c_char_p, c_char_p, c_int64]
127
+
128
+ self.lib.owswp_start_session.restype = c_void_p
129
+ self.lib.owswp_start_session.argtypes = [c_char_p, c_char_p]
130
+
131
+ self.lib.owswp_stop_session.restype = c_void_p
132
+ self.lib.owswp_stop_session.argtypes = [c_char_p, c_char_p]
133
+
134
+ self.lib.owswp_export_state.restype = c_void_p
135
+ self.lib.owswp_export_state.argtypes = [c_char_p, c_char_p]
136
+
137
+ self.lib.owswp_import_state.restype = c_void_p
138
+ self.lib.owswp_import_state.argtypes = [c_char_p, c_char_p, c_char_p]
139
+
140
+ self.lib.owswp_free_string.restype = None
141
+ self.lib.owswp_free_string.argtypes = [c_void_p]
142
+
143
+ def _call(self, func, *args) -> dict:
144
+ encoded = [a.encode("utf-8") if isinstance(a, str) else a for a in args]
145
+ ptr = func(*encoded)
146
+
147
+ if not ptr:
148
+ raise CoreLoadError("Core returned null pointer")
149
+
150
+ try:
151
+ raw = ctypes.cast(ptr, c_char_p).value
152
+ if raw is None:
153
+ raise CoreLoadError("Core returned empty response")
154
+ return json.loads(raw.decode("utf-8"))
155
+ except json.JSONDecodeError as e:
156
+ raise CoreLoadError(f"Invalid JSON from core: {e}")
157
+ finally:
158
+ self.lib.owswp_free_string(ptr)
159
+
160
+ def version(self) -> dict:
161
+ return self._call(self.lib.owswp_version)
162
+
163
+ def info(self) -> dict:
164
+ return self._call(self.lib.owswp_info)
165
+
166
+ def generate_key_pair(self) -> dict:
167
+ return self._call(self.lib.owswp_generate_key_pair)
168
+
169
+ def validate_key_pair(self, secret: str, public: str) -> dict:
170
+ return self._call(self.lib.owswp_validate_key_pair, secret, public)
171
+
172
+ def create_license(self, secret: str, license_type: str, payload_json: str) -> dict:
173
+ return self._call(self.lib.owswp_create_license, secret, license_type, payload_json)
174
+
175
+ def validate_key(self, token: str, public: str, swpb_path: str) -> dict:
176
+ return self._call(self.lib.owswp_validate_key, token, public, swpb_path)
177
+
178
+ def inspect_key(self, token: str, public: str) -> dict:
179
+ return self._call(self.lib.owswp_inspect_key, token, public)
180
+
181
+ def protection_verify(self, public: str, swpb_path: str) -> dict:
182
+ return self._call(self.lib.owswp_protection_verify, public, swpb_path)
183
+
184
+ def decrement_credit(self, public: str, swpb_path: str, amount: int) -> dict:
185
+ return self._call(self.lib.owswp_decrement_credit, public, swpb_path, amount)
186
+
187
+ def start_session(self, public: str, swpb_path: str) -> dict:
188
+ return self._call(self.lib.owswp_start_session, public, swpb_path)
189
+
190
+ def stop_session(self, public: str, swpb_path: str) -> dict:
191
+ return self._call(self.lib.owswp_stop_session, public, swpb_path)
192
+
193
+ def export_state(self, public: str, swpb_path: str) -> dict:
194
+ return self._call(self.lib.owswp_export_state, public, swpb_path)
195
+
196
+ def import_state(self, public: str, swpb_path: str, data: str) -> dict:
197
+ return self._call(self.lib.owswp_import_state, public, swpb_path, data)
198
+
199
+
200
+ _core_instance = None
201
+
202
+
203
+ def get_core() -> CoreLib:
204
+ global _core_instance
205
+ if _core_instance is None:
206
+ _core_instance = CoreLib()
207
+ return _core_instance
208
+
209
+
210
+ def retry_on_locked(func, *args, retries: int = 3, delay: float = 0.1, **kwargs):
211
+ last_error = None
212
+ for attempt in range(retries):
213
+ try:
214
+ return func(*args, **kwargs)
215
+ except FileLockedError as e:
216
+ last_error = e
217
+ time.sleep(delay * (2 ** attempt))
218
+ if last_error:
219
+ raise last_error
220
+ raise FileLockedError("File locked")
onewayswp/admin.py ADDED
@@ -0,0 +1,187 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Optional, Tuple
4
+
5
+ from ._core import get_core, detect_platform
6
+ from .exceptions import (
7
+ OnewaySWPError,
8
+ SecurityError,
9
+ InvalidKeyError,
10
+ raise_from_response,
11
+ )
12
+
13
+
14
+ SECRET_FILE = "secret.key"
15
+ PUBLIC_FILE = "public.key"
16
+ DEFAULT_DIR = Path.home() / ".owswp"
17
+
18
+
19
+ class Admin:
20
+ def __init__(self, key_dir: Optional[str] = None) -> None:
21
+ self.key_dir: Path = Path(key_dir).expanduser() if key_dir else DEFAULT_DIR
22
+ self.secret_key: Optional[str] = None
23
+ self.public_key: Optional[str] = None
24
+ self._core = get_core()
25
+
26
+ def keys_exist(self, key_dir: Optional[str] = None) -> bool:
27
+ check_dir = Path(key_dir).expanduser() if key_dir else self.key_dir
28
+ secret_path = check_dir / SECRET_FILE
29
+ public_path = check_dir / PUBLIC_FILE
30
+ return secret_path.exists() and public_path.exists()
31
+
32
+ def generate_keys(
33
+ self,
34
+ auto_save: bool = True,
35
+ file_path: Optional[str] = None,
36
+ ) -> Tuple[str, str]:
37
+ response = self._core.generate_key_pair()
38
+ if not response.get("status"):
39
+ raise_from_response(response)
40
+
41
+ self.secret_key = response.get("secret_key")
42
+ self.public_key = response.get("public_key")
43
+
44
+ if auto_save:
45
+ save_dir = Path(file_path).expanduser() if file_path else self.key_dir
46
+ self.save_keys(target_dir=str(save_dir))
47
+
48
+ return self.secret_key, self.public_key
49
+
50
+ def save_keys(self, target_dir: Optional[str] = None) -> None:
51
+ if not self.secret_key or not self.public_key:
52
+ raise OnewaySWPError("No keys to save. Call generate_keys() first.")
53
+
54
+ save_dir = Path(target_dir).expanduser() if target_dir else self.key_dir
55
+ save_dir.mkdir(parents=True, exist_ok=True)
56
+
57
+ secret_path = save_dir / SECRET_FILE
58
+ public_path = save_dir / PUBLIC_FILE
59
+
60
+ secret_path.write_text(self.secret_key)
61
+ public_path.write_text(self.public_key)
62
+
63
+ try:
64
+ secret_path.chmod(0o600)
65
+ except (OSError, NotImplementedError):
66
+ pass
67
+
68
+ def load_keys(
69
+ self,
70
+ key_dir: Optional[str] = None,
71
+ validate: bool = True,
72
+ auto_generate: bool = False,
73
+ ) -> Tuple[str, str]:
74
+ load_dir = Path(key_dir).expanduser() if key_dir else self.key_dir
75
+ secret_path = load_dir / SECRET_FILE
76
+ public_path = load_dir / PUBLIC_FILE
77
+
78
+ if not secret_path.exists() or not public_path.exists():
79
+ if auto_generate:
80
+ self.key_dir = load_dir
81
+ self.generate_keys(auto_save=True, file_path=str(load_dir))
82
+ if validate:
83
+ self.validate_keys()
84
+ return self.secret_key, self.public_key
85
+ raise InvalidKeyError(f"Key files not found in {load_dir}")
86
+
87
+ self.secret_key = secret_path.read_text().strip()
88
+ self.public_key = public_path.read_text().strip()
89
+
90
+ if validate:
91
+ self.validate_keys()
92
+
93
+ return self.secret_key, self.public_key
94
+
95
+ def set_keys(
96
+ self,
97
+ secret_key: str,
98
+ public_key: str,
99
+ validate: bool = True,
100
+ ) -> None:
101
+ if not secret_key or not public_key:
102
+ raise InvalidKeyError("Both secret_key and public_key required")
103
+
104
+ self.secret_key = secret_key.strip()
105
+ self.public_key = public_key.strip()
106
+
107
+ if validate:
108
+ self.validate_keys()
109
+
110
+ def validate_keys(self) -> bool:
111
+ if not self.secret_key or not self.public_key:
112
+ raise InvalidKeyError("Keys not loaded")
113
+
114
+ response = self._core.validate_key_pair(self.secret_key, self.public_key)
115
+ if not response.get("status"):
116
+ raise SecurityError(
117
+ response.get("message", "Key mismatch"),
118
+ code=response.get("code"),
119
+ )
120
+ return True
121
+
122
+ def create_license(
123
+ self,
124
+ type: str = "date",
125
+ expires: Optional[str] = None,
126
+ credits: Optional[int] = None,
127
+ duration_seconds: Optional[int] = None,
128
+ hours: Optional[float] = None,
129
+ additional_data: Optional[dict] = None,
130
+ ) -> dict:
131
+ if not self.secret_key:
132
+ raise OnewaySWPError(
133
+ "Secret key not loaded. Call load_keys() or generate_keys()."
134
+ )
135
+
136
+ if type not in ("date", "credit", "duration"):
137
+ raise OnewaySWPError(
138
+ f"Invalid type '{type}'. Must be: date, credit, duration"
139
+ )
140
+
141
+ if type == "date" and not expires:
142
+ raise OnewaySWPError("type='date' requires 'expires' (YYYY-MM-DD)")
143
+
144
+ if type == "credit" and credits is None:
145
+ raise OnewaySWPError("type='credit' requires 'credits'")
146
+
147
+ if type == "duration" and duration_seconds is None and hours is None:
148
+ raise OnewaySWPError(
149
+ "type='duration' requires 'duration_seconds' or 'hours'"
150
+ )
151
+
152
+ payload: dict = {}
153
+ if expires:
154
+ payload["expires"] = expires
155
+ if credits is not None:
156
+ payload["credits"] = credits
157
+ if duration_seconds is not None:
158
+ payload["duration_seconds"] = duration_seconds
159
+ elif hours is not None:
160
+ payload["duration_seconds"] = int(hours * 3600)
161
+ if additional_data:
162
+ payload["additional_data"] = additional_data
163
+
164
+ response = self._core.create_license(
165
+ self.secret_key,
166
+ type,
167
+ json.dumps(payload),
168
+ )
169
+
170
+ if not response.get("status"):
171
+ raise_from_response(response)
172
+
173
+ return response
174
+
175
+ def regenerate_keys(self, key_dir: Optional[str] = None) -> Tuple[str, str]:
176
+ target_dir = Path(key_dir).expanduser() if key_dir else self.key_dir
177
+ return self.generate_keys(auto_save=True, file_path=str(target_dir))
178
+
179
+ def platform(self) -> dict:
180
+ os_name, arch = detect_platform()
181
+ return {"os": os_name, "arch": arch}
182
+
183
+ def version(self) -> str:
184
+ response = self._core.version()
185
+ if not response.get("status"):
186
+ raise_from_response(response)
187
+ return response.get("version", "")
onewayswp/client.py ADDED
@@ -0,0 +1,245 @@
1
+ from pathlib import Path
2
+ from typing import Optional, List
3
+
4
+ from ._core import get_core, retry_on_locked
5
+ from .exceptions import (
6
+ OnewaySWPError,
7
+ SecurityError,
8
+ InvalidKeyError,
9
+ InvalidTokenError,
10
+ raise_from_response,
11
+ )
12
+
13
+
14
+ class Client:
15
+ def __init__(
16
+ self,
17
+ data_path: Optional[str] = None,
18
+ extra_paths: Optional[List[str]] = None,
19
+ ) -> None:
20
+ self.data_path: Path = (
21
+ Path(data_path).expanduser()
22
+ if data_path
23
+ else Path.cwd() / "license.swpb"
24
+ )
25
+ self.extra_paths: List[Path] = (
26
+ [Path(p).expanduser() for p in extra_paths] if extra_paths else []
27
+ )
28
+ self.public_key: Optional[str] = None
29
+ self._last_status: Optional[dict] = None
30
+ self._core = get_core()
31
+
32
+ def open_key(self, path: str) -> None:
33
+ p = Path(path).expanduser()
34
+
35
+ if p.is_dir():
36
+ p = p / "public.key"
37
+
38
+ if not p.exists():
39
+ raise InvalidKeyError(f"Key file not found: {p}")
40
+
41
+ content = p.read_text().strip()
42
+ self.set_public_key(content)
43
+
44
+ def set_public_key(self, key: str) -> None:
45
+ if not key:
46
+ raise InvalidKeyError("Public key is empty")
47
+
48
+ key = key.strip()
49
+
50
+ if key.startswith("OWSWP-sec-"):
51
+ raise SecurityError(
52
+ "Security error: you provided a secret key. "
53
+ "Client must use public key only."
54
+ )
55
+
56
+ if not key.startswith("OWSWP-pub-"):
57
+ raise InvalidKeyError(
58
+ "Invalid key format. Must start with 'OWSWP-pub-'"
59
+ )
60
+
61
+ self.public_key = key
62
+
63
+ def is_active(self) -> bool:
64
+ if not self.public_key:
65
+ return False
66
+
67
+ if not self.data_path.exists():
68
+ return False
69
+
70
+ try:
71
+ result = self.protection_verify()
72
+ return result.get("status", False)
73
+ except Exception:
74
+ return False
75
+
76
+ def validate_key(self, token: str) -> dict:
77
+ if not self.public_key:
78
+ raise InvalidKeyError(
79
+ "Public key not set. Call open_key() or set_public_key()."
80
+ )
81
+
82
+ if not token or not isinstance(token, str):
83
+ raise OnewaySWPError("Token is required and must be a string")
84
+
85
+ token = token.strip()
86
+
87
+ if not token.startswith("OWSWP-"):
88
+ raise InvalidTokenError(
89
+ "Invalid token format. Must start with 'OWSWP-'"
90
+ )
91
+
92
+ try:
93
+ self.data_path.parent.mkdir(parents=True, exist_ok=True)
94
+ except OSError as e:
95
+ raise OnewaySWPError(f"Cannot create directory: {e}")
96
+
97
+ response = retry_on_locked(
98
+ self._core.validate_key,
99
+ token,
100
+ self.public_key,
101
+ str(self.data_path),
102
+ )
103
+ self._last_status = response
104
+ return response
105
+
106
+ def inspect_key(self, token: str) -> dict:
107
+ if not self.public_key:
108
+ raise InvalidKeyError("Public key not set.")
109
+
110
+ if not token or not isinstance(token, str):
111
+ raise OnewaySWPError("Token is required and must be a string")
112
+
113
+ response = self._core.inspect_key(token.strip(), self.public_key)
114
+ return response
115
+
116
+ def protection_verify(self) -> dict:
117
+ if not self.public_key:
118
+ raise InvalidKeyError("Public key not set.")
119
+
120
+ if not self.data_path.exists():
121
+ return {
122
+ "status": False,
123
+ "code": "NO_STATE_FILE",
124
+ "message": f"State file not found: {self.data_path}",
125
+ }
126
+
127
+ response = retry_on_locked(
128
+ self._core.protection_verify,
129
+ self.public_key,
130
+ str(self.data_path),
131
+ )
132
+ self._last_status = response
133
+ return response
134
+
135
+ def use_credit(self, amount: int = 1) -> dict:
136
+ if not self.public_key:
137
+ raise InvalidKeyError("Public key not set.")
138
+
139
+ if not self.data_path.exists():
140
+ raise OnewaySWPError("State file not found. Validate key first.")
141
+
142
+ if amount <= 0:
143
+ raise OnewaySWPError("Amount must be greater than zero")
144
+
145
+ response = retry_on_locked(
146
+ self._core.decrement_credit,
147
+ self.public_key,
148
+ str(self.data_path),
149
+ amount,
150
+ )
151
+ self._last_status = response
152
+ return response
153
+
154
+ def session(self):
155
+ return _SessionContext(self)
156
+
157
+ def _start_session(self) -> dict:
158
+ if not self.public_key:
159
+ raise InvalidKeyError("Public key not set.")
160
+ if not self.data_path.exists():
161
+ raise OnewaySWPError("State file not found. Validate key first.")
162
+ return self._core.start_session(self.public_key, str(self.data_path))
163
+
164
+ def _stop_session(self) -> dict:
165
+ if not self.public_key:
166
+ raise InvalidKeyError("Public key not set.")
167
+ if not self.data_path.exists():
168
+ raise OnewaySWPError("State file not found. Validate key first.")
169
+ return self._core.stop_session(self.public_key, str(self.data_path))
170
+
171
+ def export_state(self) -> dict:
172
+ if not self.public_key:
173
+ raise InvalidKeyError("Public key not set.")
174
+ if not self.data_path.exists():
175
+ raise OnewaySWPError("State file not found.")
176
+ return self._core.export_state(self.public_key, str(self.data_path))
177
+
178
+ def import_state(self, data) -> dict:
179
+ if not self.public_key:
180
+ raise InvalidKeyError("Public key not set.")
181
+
182
+ try:
183
+ self.data_path.parent.mkdir(parents=True, exist_ok=True)
184
+ except OSError as e:
185
+ raise OnewaySWPError(f"Cannot create directory: {e}")
186
+
187
+ if isinstance(data, dict):
188
+ import json as _json
189
+ data = _json.dumps(data)
190
+
191
+ response = self._core.import_state(
192
+ self.public_key,
193
+ str(self.data_path),
194
+ data,
195
+ )
196
+ self._last_status = response
197
+ return response
198
+
199
+ def status(self) -> dict:
200
+ if self._last_status:
201
+ return self._last_status
202
+
203
+ if not self.data_path.exists():
204
+ return {
205
+ "status": False,
206
+ "code": "NO_STATE_FILE",
207
+ "message": "No status available",
208
+ }
209
+
210
+ return self.protection_verify()
211
+
212
+ def state_path(self) -> str:
213
+ return str(self.data_path)
214
+
215
+ def reset(self) -> None:
216
+ if self.data_path.exists():
217
+ self.data_path.unlink()
218
+ self._last_status = None
219
+
220
+ def version(self) -> str:
221
+ response = self._core.version()
222
+ if not response.get("status"):
223
+ raise_from_response(response)
224
+ return response.get("version", "")
225
+
226
+
227
+ class _SessionContext:
228
+ def __init__(self, client: Client) -> None:
229
+ self.client = client
230
+ self._started = False
231
+
232
+ def __enter__(self) -> Client:
233
+ result = self.client._start_session()
234
+ if not result.get("status"):
235
+ raise_from_response(result)
236
+ self._started = True
237
+ return self.client
238
+
239
+ def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
240
+ if self._started:
241
+ try:
242
+ self.client._stop_session()
243
+ except Exception:
244
+ pass
245
+ return False