opa-python-client 2.0.0__tar.gz → 2.0.2__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.
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: opa-python-client
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Client for connection to the OPA service
5
5
  Home-page: https://github.com/Turall/OPA-python-client
6
6
  License: MIT
7
7
  Author: Tural Muradov
8
- Author-email: tural_m@hotmail.com
8
+ Author-email: tural.muradoov@gmail.com
9
9
  Requires-Python: >=3.9,<4.0
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: License :: OSI Approved :: MIT License
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
19
20
  Classifier: Programming Language :: Python :: 3 :: Only
20
21
  Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
21
22
  Requires-Dist: aiohttp[speedups] (>=3.10.9,<4.0.0)
@@ -25,6 +26,11 @@ Description-Content-Type: text/markdown
25
26
 
26
27
 
27
28
  # OpaClient - Open Policy Agent Python Client
29
+ [![MIT licensed](https://img.shields.io/github/license/Turall/OPA-python-client)](https://raw.githubusercontent.com/Turall/OPA-python-client/master/LICENSE)
30
+ [![GitHub stars](https://img.shields.io/github/stars/Turall/OPA-python-client.svg)](https://github.com/Turall/OPA-python-client/stargazers)
31
+ [![GitHub forks](https://img.shields.io/github/forks/Turall/OPA-python-client.svg)](https://github.com/Turall/OPA-python-client/network)
32
+ [![GitHub issues](https://img.shields.io/github/issues-raw/Turall/OPA-python-client)](https://github.com/Turall/OPA-python-client/issues)
33
+ [![Downloads](https://pepy.tech/badge/opa-python-client)](https://pepy.tech/project/opa-python-client)
28
34
 
29
35
  OpaClient is a Python client library designed to interact with the [Open Policy Agent (OPA)](https://www.openpolicyagent.org/). It supports both **synchronous** and **asynchronous** requests, making it easy to manage policies, data, and evaluate rules in OPA servers.
30
36
 
@@ -42,7 +48,7 @@ OpaClient is a Python client library designed to interact with the [Open Policy
42
48
  You can install the OpaClient package via `pip`:
43
49
 
44
50
  ```bash
45
- pip install OPA-python-client
51
+ pip install opa-python-client
46
52
  ```
47
53
 
48
54
  ## Quick Start
@@ -1,5 +1,10 @@
1
1
 
2
2
  # OpaClient - Open Policy Agent Python Client
3
+ [![MIT licensed](https://img.shields.io/github/license/Turall/OPA-python-client)](https://raw.githubusercontent.com/Turall/OPA-python-client/master/LICENSE)
4
+ [![GitHub stars](https://img.shields.io/github/stars/Turall/OPA-python-client.svg)](https://github.com/Turall/OPA-python-client/stargazers)
5
+ [![GitHub forks](https://img.shields.io/github/forks/Turall/OPA-python-client.svg)](https://github.com/Turall/OPA-python-client/network)
6
+ [![GitHub issues](https://img.shields.io/github/issues-raw/Turall/OPA-python-client)](https://github.com/Turall/OPA-python-client/issues)
7
+ [![Downloads](https://pepy.tech/badge/opa-python-client)](https://pepy.tech/project/opa-python-client)
3
8
 
4
9
  OpaClient is a Python client library designed to interact with the [Open Policy Agent (OPA)](https://www.openpolicyagent.org/). It supports both **synchronous** and **asynchronous** requests, making it easy to manage policies, data, and evaluate rules in OPA servers.
5
10
 
@@ -17,7 +22,7 @@ OpaClient is a Python client library designed to interact with the [Open Policy
17
22
  You can install the OpaClient package via `pip`:
18
23
 
19
24
  ```bash
20
- pip install OPA-python-client
25
+ pip install opa-python-client
21
26
  ```
22
27
 
23
28
  ## Quick Start
@@ -11,7 +11,7 @@ def create_opa_client(async_mode=False, *args, **kwargs):
11
11
  return OpaClient(*args, **kwargs)
12
12
 
13
13
 
14
- __version__ = "2.0.0"
14
+ __version__ = "2.0.2"
15
15
  __author__ = "Tural Muradov"
16
16
  __license__ = "MIT"
17
17
 
@@ -0,0 +1,147 @@
1
+ import os
2
+ from typing import Dict, Optional, Union
3
+ from urllib.parse import urlencode
4
+
5
+ from .errors import (
6
+ FileError,
7
+ PathNotFoundError,
8
+ )
9
+
10
+
11
+ class BaseClient:
12
+ """
13
+ Base class for OpaClient implementations.
14
+
15
+ This class contains common logic shared between synchronous and asynchronous clients.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ host: str = "localhost",
21
+ port: int = 8181,
22
+ version: str = "v1",
23
+ ssl: bool = False,
24
+ cert: Optional[Union[str, tuple]] = None,
25
+ headers: Optional[dict] = None,
26
+ timeout: float = 1.5,
27
+ ):
28
+ if not isinstance(port, int):
29
+ raise TypeError("The port must be an integer")
30
+
31
+ self.host = host.strip()
32
+ self.port = port
33
+ self.version = version
34
+ self.ssl = ssl
35
+ self.cert = cert
36
+ self.timeout = timeout
37
+
38
+ self.schema = "https://" if ssl else "http://"
39
+ self.root_url = f"{self.schema}{self.host}:{self.port}/{self.version}"
40
+
41
+ self.headers = headers
42
+
43
+ self._session = None # Will be initialized in the subclass
44
+ self.retries = 2
45
+ self.timeout = 1.5
46
+
47
+ def _build_url(
48
+ self, path: str, query_params: Dict[str, str] = None
49
+ ) -> str:
50
+ url = f"{self.root_url}/{path.lstrip('/')}"
51
+ if query_params:
52
+ url = f"{url}?{urlencode(query_params)}"
53
+ return url
54
+
55
+ def _load_policy_from_file(self, filepath: str) -> str:
56
+ if not os.path.isfile(filepath):
57
+ raise FileError(f"'{filepath}' is not a valid file")
58
+ with open(filepath, "r", encoding="utf-8") as file:
59
+ return file.read()
60
+
61
+ def _save_policy_to_file(
62
+ self, policy_raw: str, path: Optional[str], filename: str
63
+ ) -> bool:
64
+ full_path = os.path.join(path or "", filename)
65
+ try:
66
+ with open(full_path, "w", encoding="utf-8") as file:
67
+ file.write(policy_raw)
68
+ return True
69
+ except OSError as e:
70
+ raise PathNotFoundError(f"Failed to write to '{full_path}'") from e
71
+
72
+ # Abstract methods to be implemented in subclasses
73
+ def close_connection(self):
74
+ raise NotImplementedError
75
+
76
+ def check_connection(self) -> str:
77
+ raise NotImplementedError
78
+
79
+ def _init_session(self):
80
+ raise NotImplementedError
81
+
82
+ def check_health(
83
+ self, query: Dict[str, bool] = None, diagnostic_url: str = None
84
+ ) -> bool:
85
+ raise NotImplementedError
86
+
87
+ def get_policies_list(self) -> list:
88
+ raise NotImplementedError
89
+
90
+ def get_policies_info(self) -> dict:
91
+ raise NotImplementedError
92
+
93
+ def update_policy_from_string(
94
+ self, new_policy: str, endpoint: str
95
+ ) -> bool:
96
+ raise NotImplementedError
97
+
98
+ def update_policy_from_file(self, filepath: str, endpoint: str) -> bool:
99
+ raise NotImplementedError
100
+
101
+ def update_policy_from_url(self, url: str, endpoint: str) -> bool:
102
+ raise NotImplementedError
103
+
104
+ def update_or_create_data(self, new_data: dict, endpoint: str) -> bool:
105
+ raise NotImplementedError
106
+
107
+ def get_data(
108
+ self, data_name: str = "", query_params: Dict[str, bool] = None
109
+ ) -> dict:
110
+ raise NotImplementedError
111
+
112
+ def policy_to_file(
113
+ self,
114
+ policy_name: str,
115
+ path: Optional[str] = None,
116
+ filename: str = "opa_policy.rego",
117
+ ) -> bool:
118
+ raise NotImplementedError
119
+
120
+ def get_policy(self, policy_name: str) -> dict:
121
+ raise NotImplementedError
122
+
123
+ def delete_policy(self, policy_name: str) -> bool:
124
+ raise NotImplementedError
125
+
126
+ def delete_data(self, data_name: str) -> bool:
127
+ raise NotImplementedError
128
+
129
+ def check_permission(
130
+ self,
131
+ input_data: dict,
132
+ policy_name: str,
133
+ rule_name: str,
134
+ query_params: Dict[str, bool] = None,
135
+ ) -> dict:
136
+ raise NotImplementedError
137
+
138
+ def query_rule(
139
+ self,
140
+ input_data: dict,
141
+ package_path: str,
142
+ rule_name: Optional[str] = None,
143
+ ) -> dict:
144
+ raise NotImplementedError
145
+
146
+ def ad_hoc_query(self, query: str, input_data: dict = None) -> dict:
147
+ raise NotImplementedError
@@ -1,4 +1,3 @@
1
- import json
2
1
  import os
3
2
  import threading
4
3
  from typing import Dict, Optional
@@ -10,15 +9,15 @@ from urllib3.util.retry import Retry
10
9
 
11
10
  from .base import BaseClient
12
11
  from .errors import (
13
- CheckPermissionError,
14
- ConnectionsError,
15
- DeleteDataError,
16
- DeletePolicyError,
17
- FileError,
18
- PathNotFoundError,
19
- PolicyNotFoundError,
20
- RegoParseError,
21
- TypeException,
12
+ CheckPermissionError,
13
+ ConnectionsError,
14
+ DeleteDataError,
15
+ DeletePolicyError,
16
+ FileError,
17
+ PathNotFoundError,
18
+ PolicyNotFoundError,
19
+ RegoParseError,
20
+ TypeException,
22
21
  )
23
22
 
24
23
 
@@ -202,7 +201,7 @@ class OpaClient(BaseClient):
202
201
  bool: True if the policy was successfully updated.
203
202
  """
204
203
  if not os.path.isfile(filepath):
205
- raise FileError(f"'{filepath}' is not a valid file")
204
+ raise FileError("file_not_found",f"'{filepath}' is not a valid file")
206
205
 
207
206
  with open(filepath, "r", encoding="utf-8") as file:
208
207
  policy_str = file.read()
@@ -300,7 +299,7 @@ class OpaClient(BaseClient):
300
299
  policy_raw = policy.get("result", {}).get("raw", "")
301
300
 
302
301
  if not policy_raw:
303
- raise PolicyNotFoundError("Policy content is empty")
302
+ raise PolicyNotFoundError("resource_not_found", "Policy content is empty")
304
303
 
305
304
  full_path = os.path.join(path or "", filename)
306
305
 
@@ -309,7 +308,7 @@ class OpaClient(BaseClient):
309
308
  file.write(policy_raw)
310
309
  return True
311
310
  except OSError as e:
312
- raise PathNotFoundError(f"Failed to write to '{full_path}'") from e
311
+ raise PathNotFoundError("path_not_found", f"Failed to write to '{full_path}'") from e
313
312
 
314
313
  def get_policy(self, policy_name: str) -> dict:
315
314
  """
@@ -395,7 +394,7 @@ class OpaClient(BaseClient):
395
394
 
396
395
  if rule_name not in rules:
397
396
  raise CheckPermissionError(
398
- f"Rule '{rule_name}' not found in policy '{policy_name}'"
397
+ "resource_not_found", f"Rule '{rule_name}' not found in policy '{policy_name}'"
399
398
  )
400
399
 
401
400
  url = f"{self.root_url}/{package_path}/{rule_name}"
@@ -456,89 +455,6 @@ class OpaClient(BaseClient):
456
455
  response.raise_for_status()
457
456
  return response.json()
458
457
 
459
- # # Property methods for read-only access to certain attributes
460
- # @property
461
- # def host(self) -> str:
462
- # return self._host
463
-
464
- # @host.setter
465
- # def host(self, value: str):
466
- # self._host = value
467
-
468
- # @property
469
- # def port(self) -> int:
470
- # return self._port
471
-
472
- # @port.setter
473
- # def port(self, value: int):
474
- # if not isinstance(value, int):
475
- # raise TypeError('Port must be an integer')
476
- # self._port = value
477
-
478
- # @property
479
- # def version(self) -> str:
480
- # return self._version
481
-
482
- # @version.setter
483
- # def version(self, value: str):
484
- # self._version = value
485
-
486
- # @property
487
- # def schema(self) -> str:
488
- # return self._schema
489
-
490
- # @schema.setter
491
- # def schema(self, value: str):
492
- # self._schema = value
493
-
494
- # @property
495
- # def root_url(self) -> str:
496
- # return self._root_url
497
-
498
- # @root_url.setter
499
- # def root_url(self, value: str):
500
- # self._root_url = value
501
-
502
- # @property
503
- # def ssl(self) -> bool:
504
- # return self._ssl
505
-
506
- # @ssl.setter
507
- # def ssl(self, value: bool):
508
- # self._ssl = value
509
-
510
- # @property
511
- # def cert(self) -> Optional[str]:
512
- # return self._cert
513
-
514
- # @cert.setter
515
- # def cert(self, value: Optional[str]):
516
- # self._cert = value
517
-
518
- # @property
519
- # def headers(self) -> dict:
520
- # return self._headers
521
-
522
- # @headers.setter
523
- # def headers(self, value: dict):
524
- # self._headers = value
525
-
526
- # @property
527
- # def retries(self) -> int:
528
- # return self._retries
529
-
530
- # @retries.setter
531
- # def retries(self, value: int):
532
- # self._retries = value
533
-
534
- # @property
535
- # def timeout(self) -> float:
536
- # return self._timeout
537
-
538
- # @timeout.setter
539
- # def timeout(self, value: float):
540
- # self._timeout = value
541
-
542
458
 
543
459
  # Example usage:
544
460
  if __name__ == "__main__":
@@ -1,8 +1,8 @@
1
1
  [tool.poetry]
2
2
  name = "opa-python-client"
3
- version = "2.0.0"
3
+ version = "2.0.2"
4
4
  description = "Client for connection to the OPA service"
5
- authors = ["Tural Muradov <tural_m@hotmail.com>"]
5
+ authors = ["Tural Muradov <tural.muradoov@gmail.com>"]
6
6
  license = "MIT"
7
7
  readme = "README.md"
8
8
  homepage = "https://github.com/Turall/OPA-python-client"
@@ -1,286 +0,0 @@
1
- import os
2
- from typing import Dict, Optional, Union
3
- from urllib.parse import urlencode
4
-
5
- from .errors import (
6
- FileError,
7
- PathNotFoundError,
8
- )
9
-
10
-
11
- class BaseClient:
12
- """
13
- Base class for OpaClient implementations.
14
-
15
- This class contains common logic shared between synchronous and asynchronous clients.
16
- """
17
-
18
- def __init__(
19
- self,
20
- host: str = "localhost",
21
- port: int = 8181,
22
- version: str = "v1",
23
- ssl: bool = False,
24
- cert: Optional[Union[str, tuple]] = None,
25
- headers: Optional[dict] = None,
26
- timeout: float = 1.5,
27
- ):
28
- if not isinstance(port, int):
29
- raise TypeError("The port must be an integer")
30
-
31
- self.host = host.strip()
32
- self.port = port
33
- self.version = version
34
- self.ssl = ssl
35
- self.cert = cert
36
- self.timeout = timeout
37
-
38
- self.schema = "https://" if ssl else "http://"
39
- self.root_url = f"{self.schema}{self.host}:{self.port}/{self.version}"
40
-
41
- self.headers = headers
42
-
43
- self._session = None # Will be initialized in the subclass
44
- self.retries = 2
45
- self.timeout = 1.5
46
-
47
- def _build_url(
48
- self, path: str, query_params: Dict[str, str] = None
49
- ) -> str:
50
- url = f"{self.root_url}/{path.lstrip('/')}"
51
- if query_params:
52
- url = f"{url}?{urlencode(query_params)}"
53
- return url
54
-
55
- def _load_policy_from_file(self, filepath: str) -> str:
56
- if not os.path.isfile(filepath):
57
- raise FileError(f"'{filepath}' is not a valid file")
58
- with open(filepath, "r", encoding="utf-8") as file:
59
- return file.read()
60
-
61
- def _save_policy_to_file(
62
- self, policy_raw: str, path: Optional[str], filename: str
63
- ) -> bool:
64
- full_path = os.path.join(path or "", filename)
65
- try:
66
- with open(full_path, "w", encoding="utf-8") as file:
67
- file.write(policy_raw)
68
- return True
69
- except OSError as e:
70
- raise PathNotFoundError(f"Failed to write to '{full_path}'") from e
71
-
72
- # Abstract methods to be implemented in subclasses
73
- def close_connection(self):
74
- raise NotImplementedError
75
-
76
- def check_connection(self) -> str:
77
- raise NotImplementedError
78
-
79
- def _init_session(self):
80
- raise NotImplementedError
81
-
82
- def check_health(
83
- self, query: Dict[str, bool] = None, diagnostic_url: str = None
84
- ) -> bool:
85
- raise NotImplementedError
86
-
87
- def get_policies_list(self) -> list:
88
- raise NotImplementedError
89
-
90
- def get_policies_info(self) -> dict:
91
- raise NotImplementedError
92
-
93
- def update_policy_from_string(
94
- self, new_policy: str, endpoint: str
95
- ) -> bool:
96
- raise NotImplementedError
97
-
98
- def update_policy_from_file(self, filepath: str, endpoint: str) -> bool:
99
- raise NotImplementedError
100
-
101
- def update_policy_from_url(self, url: str, endpoint: str) -> bool:
102
- raise NotImplementedError
103
-
104
- def update_or_create_data(self, new_data: dict, endpoint: str) -> bool:
105
- raise NotImplementedError
106
-
107
- def get_data(
108
- self, data_name: str = "", query_params: Dict[str, bool] = None
109
- ) -> dict:
110
- raise NotImplementedError
111
-
112
- def policy_to_file(
113
- self,
114
- policy_name: str,
115
- path: Optional[str] = None,
116
- filename: str = "opa_policy.rego",
117
- ) -> bool:
118
- raise NotImplementedError
119
-
120
- def get_policy(self, policy_name: str) -> dict:
121
- raise NotImplementedError
122
-
123
- def delete_policy(self, policy_name: str) -> bool:
124
- raise NotImplementedError
125
-
126
- def delete_data(self, data_name: str) -> bool:
127
- raise NotImplementedError
128
-
129
- def check_permission(
130
- self,
131
- input_data: dict,
132
- policy_name: str,
133
- rule_name: str,
134
- query_params: Dict[str, bool] = None,
135
- ) -> dict:
136
- raise NotImplementedError
137
-
138
- def query_rule(
139
- self,
140
- input_data: dict,
141
- package_path: str,
142
- rule_name: Optional[str] = None,
143
- ) -> dict:
144
- raise NotImplementedError
145
-
146
- def ad_hoc_query(self, query: str, input_data: dict = None) -> dict:
147
- raise NotImplementedError
148
-
149
-
150
- # class OpaClient(BaseOpaClient):
151
- # """
152
- # Synchronous OpaClient implementation using requests.
153
- # """
154
-
155
- # def __init__(self, *args, **kwargs):
156
- # super().__init__(*args, **kwargs)
157
-
158
- # self._init_session()
159
-
160
- # def _init_session(self):
161
- # self._session = requests.Session()
162
- # self._session.headers.update(self.headers)
163
-
164
- # if self.ssl:
165
- # self._session.verify = self.cert if self.cert else True
166
-
167
- # # Optionally, configure retries and other session parameters
168
-
169
- # def close_connection(self):
170
- # if self._session:
171
- # self._session.close()
172
- # self._session = None
173
-
174
- # def check_connection(self) -> str:
175
- # url = self._build_url('policies/')
176
- # try:
177
- # response = self._session.get(url, timeout=self.timeout)
178
- # response.raise_for_status()
179
- # return "Yes, I'm here :)"
180
- # except requests.exceptions.RequestException as e:
181
- # raise ConnectionsError('Service unreachable', 'Check config and try again') from e
182
-
183
- # # Implement other synchronous methods similarly
184
- # # For example:
185
- # def get_policies_list(self) -> list:
186
- # url = self._build_url('policies/')
187
- # response = self._session.get(url, timeout=self.timeout)
188
- # response.raise_for_status()
189
- # policies = response.json().get('result', [])
190
- # return [policy.get('id') for policy in policies if policy.get('id')]
191
-
192
- # # ... Rest of synchronous methods ...
193
-
194
-
195
- # class AsyncOpaClient(BaseOpaClient):
196
- # """
197
- # Asynchronous OpaClient implementation using aiohttp.
198
- # """
199
-
200
- # async def __aenter__(self):
201
- # await self._init_session()
202
- # return self
203
-
204
- # async def __aexit__(self, exc_type, exc_value, traceback):
205
- # await self.close_connection()
206
-
207
- # async def _init_session(self):
208
- # ssl_context = None
209
-
210
- # if self.ssl:
211
- # ssl_context = ssl.create_default_context()
212
- # if self.cert:
213
- # if isinstance(self.cert, tuple):
214
- # ssl_context.load_cert_chain(*self.cert)
215
- # else:
216
- # ssl_context.load_cert_chain(self.cert)
217
- # else:
218
- # ssl_context.load_default_certs()
219
-
220
- # connector = aiohttp.TCPConnector(ssl=ssl_context)
221
-
222
- # self._session = aiohttp.ClientSession(
223
- # headers=self.headers,
224
- # connector=connector,
225
- # timeout=aiohttp.ClientTimeout(total=self.timeout),
226
- # )
227
-
228
- # async def close_connection(self):
229
- # if self._session and not self._session.closed:
230
- # await self._session.close()
231
- # self._session = None
232
-
233
- # async def check_connection(self) -> str:
234
- # url = self._build_url('policies/')
235
- # try:
236
- # async with self._session.get(url) as response:
237
- # if response.status == 200:
238
- # return "Yes, I'm here :)"
239
- # else:
240
- # raise ConnectionsError('Service unreachable', 'Check config and try again')
241
- # except Exception as e:
242
- # raise ConnectionsError('Service unreachable', 'Check config and try again') from e
243
-
244
- # # Implement other asynchronous methods similarly
245
- # # For example:
246
- # async def get_policies_list(self) -> list:
247
- # url = self._build_url('policies/')
248
- # async with self._session.get(url) as response:
249
- # response.raise_for_status()
250
- # policies = await response.json()
251
- # result = policies.get('result', [])
252
- # return [policy.get('id') for policy in result if policy.get('id')]
253
-
254
- # # ... Rest of asynchronous methods ...
255
-
256
-
257
- # # Example usage:
258
-
259
- # # Synchronous client
260
- # def sync_example():
261
- # client = OpaClient(host='localhost', port=8181)
262
- # try:
263
- # result = client.check_connection()
264
- # print(result)
265
- # policies = client.get_policies_list()
266
- # print("Policies:", policies)
267
- # finally:
268
- # client.close_connection()
269
-
270
-
271
- # # Asynchronous client
272
- # async def async_example():
273
- # async with AsyncOpaClient(host='localhost', port=8181) as client:
274
- # result = await client.check_connection()
275
- # print(result)
276
- # policies = await client.get_policies_list()
277
- # print("Policies:", policies)
278
-
279
-
280
- # if __name__ == '__main__':
281
- # # Run synchronous example
282
- # sync_example()
283
-
284
- # # Run asynchronous example
285
- # import asyncio
286
- # asyncio.run(async_example())