mapepire-python 0.1.3__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.
@@ -0,0 +1,132 @@
1
+ import json
2
+ from enum import Enum
3
+ from typing import Any, Dict, Generic, List, Optional, TypeVar, Union
4
+
5
+ from ..types import QueryOptions
6
+ from .sql_job import SQLJob
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ class QueryState(Enum):
12
+ NOT_YET_RUN = (1,)
13
+ RUN_MORE_DATA_AVAIL = (2,)
14
+ RUN_DONE = (3,)
15
+ ERROR = 4
16
+
17
+
18
+ def get_query_options(opts: Optional[Union[Dict[str, Any], QueryOptions]] = None) -> QueryOptions:
19
+ if isinstance(opts, QueryOptions):
20
+ return opts
21
+ elif opts:
22
+ return QueryOptions(**opts)
23
+ else:
24
+ return QueryOptions(isClCommand=False, parameters=None, autoClose=False)
25
+
26
+
27
+ class Query(Generic[T]):
28
+ global_query_list: List["Query[Any]"] = []
29
+
30
+ def __init__(self, job: SQLJob, query: str, opts: QueryOptions) -> None:
31
+ self.job = job
32
+ self.is_prepared: bool = True if opts.parameters is not None else False
33
+ self.parameters: Optional[List[str]] = opts.parameters
34
+ self.sql: str = query
35
+ self.is_cl_command: Optional[bool] = opts.isClCommand
36
+ self.should_auto_close: Optional[bool] = opts.autoClose
37
+ self.is_terse_results: Optional[bool] = opts.isTerseResults
38
+
39
+ self._rows_to_fetch: int = 100
40
+ self.state: QueryState = QueryState.NOT_YET_RUN
41
+
42
+ Query.global_query_list.append(self)
43
+
44
+ def run(self, rows_to_fetch: Optional[int] = None) -> Dict[str, Any]:
45
+ if rows_to_fetch is None:
46
+ rows_to_fetch = self._rows_to_fetch
47
+ else:
48
+ self._rows_to_fetch = rows_to_fetch
49
+
50
+ # check Query state first
51
+ if self.state == QueryState.RUN_MORE_DATA_AVAIL:
52
+ raise Exception("Statement has already been run")
53
+ elif self.state == QueryState.RUN_DONE:
54
+ raise Exception("Statement has already been fully run")
55
+
56
+ query_object: Dict[str, Any] = {}
57
+ if self.is_cl_command:
58
+ query_object = {
59
+ "id": self.job._get_unique_id("clcommand"),
60
+ "type": "cl",
61
+ "terse": self.is_terse_results,
62
+ "cmd": self.sql,
63
+ }
64
+ else:
65
+ query_object = {
66
+ "id": self.job._get_unique_id("query"),
67
+ "type": "prepare_sql_execute" if self.is_prepared else "sql",
68
+ "sql": self.sql,
69
+ "terse": self.is_terse_results,
70
+ "rows": rows_to_fetch,
71
+ "parameters": self.parameters,
72
+ }
73
+
74
+ self.job.send(json.dumps(query_object))
75
+ query_result: Dict[str, Any] = json.loads(self.job._socket.recv())
76
+
77
+ self.state = (
78
+ QueryState.RUN_DONE
79
+ if query_result.get("is_done", False)
80
+ else QueryState.RUN_MORE_DATA_AVAIL
81
+ )
82
+
83
+ if not query_result.get("success", False) and not self.is_cl_command:
84
+ print(query_result)
85
+ self.state = QueryState.ERROR
86
+ error_keys = ["error", "sql_state", "sql_rc"]
87
+ error_list = {
88
+ key: query_result[key] for key in error_keys if key in query_result.keys()
89
+ }
90
+ if len(error_list) == 0:
91
+ error_list["error"] = "failed to run query for unknown reason"
92
+
93
+ raise Exception(error_list)
94
+
95
+ self._correlation_id = query_result["id"]
96
+
97
+ return query_result
98
+
99
+ def fetch_more(self, rows_to_fetch: Optional[int] = None) -> Dict[str, Any]:
100
+ if rows_to_fetch is None:
101
+ rows_to_fetch = self._rows_to_fetch
102
+ else:
103
+ self._rows_to_fetch = rows_to_fetch
104
+
105
+ if self.state == QueryState.NOT_YET_RUN:
106
+ raise Exception("Statement has not been run")
107
+ elif self.state == QueryState.RUN_DONE:
108
+ raise Exception("Statement has already been fully run")
109
+
110
+ query_object = {
111
+ "id": self.job._get_unique_id("fetchMore"),
112
+ "cont_id": self._correlation_id,
113
+ "type": "sqlmore",
114
+ "sql": self.sql,
115
+ "rows": rows_to_fetch,
116
+ }
117
+
118
+ self._rows_to_fetch = rows_to_fetch
119
+ self.job.send(json.dumps(query_object))
120
+ query_result: Dict[str, Any] = json.loads(self.job._socket.recv())
121
+
122
+ self.state = (
123
+ QueryState.RUN_DONE
124
+ if query_result.get("is_done", False)
125
+ else QueryState.RUN_MORE_DATA_AVAIL
126
+ )
127
+
128
+ if not query_result["success"]:
129
+ self.state = QueryState.ERROR
130
+ raise Exception(query_result["error"] or "Failed to run Query (unknown error)")
131
+
132
+ return query_result
@@ -0,0 +1,130 @@
1
+ import base64
2
+ import json
3
+ import ssl
4
+ from typing import Any, Dict, Optional, Union
5
+
6
+ from websocket import WebSocket, create_connection
7
+
8
+ from ..types import DaemonServer, JobStatus, QueryOptions
9
+
10
+
11
+ class SQLJob:
12
+ def __init__(self, options: Dict[Any, Any] = {}) -> None:
13
+ self.options = options
14
+ self._unique_id_counter: int = 0
15
+ self._reponse_emitter = None
16
+ self._status: JobStatus = JobStatus.NotStarted
17
+ self._trace_file = None
18
+ self._is_tracing_channeldata: bool = True
19
+
20
+ self.__unique_id = self._get_unique_id("sqljob")
21
+ self.id: Optional[str] = None
22
+
23
+ def _get_unique_id(self, prefix: str = "id") -> str:
24
+ self._unique_id_counter += 1
25
+ return f"{prefix}{self._unique_id_counter}"
26
+
27
+ def _get_channel(self, db2_server: DaemonServer) -> WebSocket:
28
+ uri = f"wss://{db2_server.host}:{db2_server.port}/db/"
29
+ headers = {
30
+ "Authorization": "Basic "
31
+ + base64.b64encode(f"{db2_server.user}:{db2_server.password}".encode()).decode("ascii")
32
+ }
33
+
34
+ # Prepare SSL context if necessary
35
+ ssl_opts: Dict[str, Any] = {}
36
+ if db2_server.ignoreUnauthorized:
37
+ ssl_opts["cert_reqs"] = ssl.CERT_NONE
38
+ if db2_server.ca:
39
+ ssl_context = ssl.create_default_context(cadata=db2_server.ca)
40
+ ssl_context.check_hostname = False
41
+ ssl_opts["ssl_context"] = ssl_context
42
+ ssl_opts["cert_reqs"] = ssl.CERT_NONE # ignore certs for now
43
+
44
+ # Create WebSocket connection
45
+ socket = create_connection(uri, header=headers, sslopt=ssl_opts)
46
+
47
+ return socket
48
+
49
+ def send(self, content):
50
+ self._socket.send(content)
51
+
52
+ def connect(self, db2_server: DaemonServer) -> Dict[Any, Any]:
53
+ self._socket: WebSocket = self._get_channel(db2_server)
54
+
55
+ props = ";".join(
56
+ [
57
+ f'{prop}={",".join(self.options[prop]) if isinstance(self.options[prop], list) else self.options[prop]}'
58
+ for prop in self.options
59
+ ]
60
+ )
61
+
62
+ connection_props = {
63
+ "id": self._get_unique_id(),
64
+ "type": "connect",
65
+ "technique": "tcp",
66
+ "application": "Python Client",
67
+ "props": props if len(props) > 0 else "",
68
+ }
69
+
70
+ self.send(json.dumps(connection_props))
71
+ result: Dict[str, Any] = {}
72
+ try:
73
+ result = json.loads(self._socket.recv())
74
+ except Exception as e:
75
+ print(f"an error occured while loading connect result: {e}")
76
+
77
+ if result.get("success", False):
78
+ self._status = JobStatus.Ready
79
+ else:
80
+ self._status = JobStatus.NotStarted
81
+ raise Exception(result.get("error", "Failed to connect to server"))
82
+
83
+ self.id = result["job"]
84
+ self._is_tracing_channeldata = False
85
+
86
+ return result
87
+
88
+ def query(
89
+ self,
90
+ sql: str,
91
+ opts: Optional[Union[Dict[str, Any], QueryOptions]] = None,
92
+ ):
93
+ """
94
+ Create a Query object using provided SQL and options. If opts is None,
95
+ the default options defined in Query constructor are used. opts can be a
96
+ dictionary to be converted to QueryOptions, or a QueryOptions object directly.
97
+
98
+ Args:
99
+ sql (str): The SQL query string.
100
+ opts (Optional[Union[Dict[str, Any], QueryOptions]]): Additional options
101
+ for the query which can be a dictionary or a QueryOptions object.
102
+
103
+ Returns:
104
+ Query: A configured Query object.
105
+ """
106
+ from .query import Query
107
+
108
+ if opts is not None and not isinstance(opts, (dict, QueryOptions)):
109
+ raise ValueError("opts must be a dictionary, a QueryOptions object, or None")
110
+
111
+ query_options = (
112
+ opts
113
+ if isinstance(opts, QueryOptions)
114
+ else (
115
+ QueryOptions(**opts)
116
+ if opts
117
+ else QueryOptions(isClCommand=False, parameters=None, autoClose=False)
118
+ )
119
+ )
120
+
121
+ return Query(job=self, query=sql, opts=query_options)
122
+
123
+ def query_and_run(
124
+ self, sql: str, opts: Optional[Dict[str, Any]] = None, **kwargs
125
+ ) -> Dict[str, Any]:
126
+ query = self.query(sql, opts)
127
+ return query.run(**kwargs)
128
+
129
+ def close(self):
130
+ self._socket.close()
@@ -0,0 +1,29 @@
1
+ # from dataclasses import dataclass
2
+ # from typing import List, Optional
3
+
4
+
5
+ # from mapepire_python.client.sql_job import SQLJob
6
+ # from mapepire_python.types import *
7
+
8
+
9
+ # @dataclass
10
+ # class PoolOptions:
11
+ # creds: DaemonServer
12
+ # opts: Optional[JDBCOptions]
13
+ # max_size: int
14
+ # starting_size: int
15
+
16
+
17
+ # class Pool:
18
+ # def __init__(self, options: PoolOptions) -> None:
19
+ # self.options = options
20
+ # self.jobs: List[SQLJob] = []
21
+
22
+ # def init(self):
23
+ # ...
24
+
25
+ # async def add_job(self):
26
+ # new_job = SQLJob(self.options.opts)
27
+ # self.jobs.append(new_job)
28
+ # new_job.connect(self.options.creds)
29
+ # return new_job
File without changes
mapepire_python/ssl.py ADDED
@@ -0,0 +1,19 @@
1
+ import socket
2
+ import ssl
3
+ from typing import Optional
4
+
5
+ from .types import DaemonServer
6
+
7
+
8
+ def get_certificate(creds: DaemonServer) -> Optional[bytes]:
9
+ context = ssl.create_default_context()
10
+ context.check_hostname = False
11
+ context.verify_mode = ssl.CERT_NONE
12
+
13
+ if creds.ca:
14
+ context.load_verify_locations(
15
+ cadata=creds.ca if isinstance(creds.ca, str) else creds.ca.decode()
16
+ )
17
+ with socket.create_connection((creds.host, creds.port)) as sock:
18
+ with context.wrap_socket(sock, server_hostname=creds.host) as ssock:
19
+ return ssock.getpeercert(binary_form=True)
@@ -0,0 +1,239 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum
3
+ from typing import Any, List, Optional, Union
4
+
5
+ from dataclasses_json import dataclass_json
6
+
7
+
8
+ class JobStatus(Enum):
9
+ NotStarted = "notStarted"
10
+ Ready = "ready"
11
+ Busy = "busy"
12
+ Ended = "ended"
13
+
14
+
15
+ class ExplainType(Enum):
16
+ Run = 0
17
+ DoNotRun = 1
18
+
19
+
20
+ class TransactionEndType(Enum):
21
+ COMMIT = 0
22
+ ROLLBACK = 1
23
+
24
+
25
+ class ServerTraceLevel(Enum):
26
+ OFF = "OFF"
27
+ ON = "ON"
28
+ ERRORS = "ERRORS"
29
+ DATASTREAM = "DATASTREAM"
30
+
31
+
32
+ class ServerTraceDest(Enum):
33
+ FILE = "FILE"
34
+ IN_MEM = "IN_MEM"
35
+
36
+
37
+ @dataclass
38
+ class DaemonServer:
39
+ host: str
40
+ user: str
41
+ password: str
42
+ port: int
43
+ ignoreUnauthorized: Optional[bool] = None
44
+ ca: Optional[Union[str, bytes]] = None
45
+
46
+
47
+ @dataclass_json
48
+ @dataclass
49
+ class ServerResponse:
50
+ id: str
51
+ success: bool
52
+ sql_rc: int
53
+ sql_state: str
54
+ error: Optional[str] = None
55
+
56
+
57
+ @dataclass_json
58
+ @dataclass
59
+ class ConnectionResult(ServerResponse):
60
+ job: str
61
+ id: str = field(init=False)
62
+ success: bool = field(init=False)
63
+ sql_rc: int = field(init=False)
64
+ sql_state: str = field(init=False)
65
+ error: Optional[str] = field(default=None, init=False)
66
+
67
+
68
+ @dataclass_json
69
+ @dataclass
70
+ class VersionCheckResult(ServerResponse):
71
+ build_date: str
72
+ version: str
73
+ id: str = field(init=False)
74
+ success: bool = field(init=False)
75
+ sql_rc: int = field(init=False)
76
+ sql_state: str = field(init=False)
77
+ error: Optional[str] = field(default=None, init=False)
78
+
79
+
80
+ @dataclass
81
+ class ColumnMetaData:
82
+ display_size: int
83
+ label: str
84
+ name: str
85
+ type: str
86
+
87
+
88
+ @dataclass
89
+ class QueryMetaData:
90
+ column_count: int
91
+ columns: List[ColumnMetaData]
92
+ job: str
93
+
94
+
95
+ @dataclass
96
+ class QueryResult:
97
+ metadata: QueryMetaData
98
+ is_done: bool
99
+ has_results: bool
100
+ update_count: int
101
+ data: List[Any]
102
+
103
+
104
+ @dataclass
105
+ class ExplainResults(QueryResult):
106
+ vemetadata: QueryMetaData
107
+ vedata: Any
108
+
109
+
110
+ @dataclass_json
111
+ @dataclass
112
+ class GetTraceDataResult(ServerResponse):
113
+ tracedata: str
114
+ id: str = field(init=False)
115
+ success: bool = field(init=False)
116
+ sql_rc: int = field(init=False)
117
+ sql_state: str = field(init=False)
118
+ error: Optional[str] = field(default=None, init=False)
119
+
120
+
121
+ @dataclass
122
+ class JobLogEntry:
123
+ MESSAGE_ID: str
124
+ SEVERITY: str
125
+ MESSAGE_TIMESTAMP: str
126
+ FROM_LIBRARY: str
127
+ FROM_PROGRAM: str
128
+ MESSAGE_TYPE: str
129
+ MESSAGE_TEXT: str
130
+ MESSAGE_SECOND_LEVEL_TEXT: str
131
+
132
+
133
+ @dataclass_json
134
+ @dataclass
135
+ class CLCommandResult(ServerResponse):
136
+ joblog: List[JobLogEntry]
137
+ id: str = field(init=False)
138
+ success: bool = field(init=False)
139
+ sql_rc: int = field(init=False)
140
+ sql_state: str = field(init=False)
141
+ error: Optional[str] = field(default=None, init=False)
142
+
143
+
144
+ @dataclass
145
+ class QueryOptions:
146
+ isTerseResults: Optional[bool] = None
147
+ isClCommand: Optional[bool] = None
148
+ parameters: Optional[List[str]] = None
149
+ autoClose: Optional[bool] = None
150
+
151
+
152
+ @dataclass_json
153
+ @dataclass
154
+ class SetConfigResult(ServerResponse):
155
+ tracedest: ServerTraceDest
156
+ tracelevel: ServerTraceLevel
157
+ id: str = field(init=False)
158
+ success: bool = field(init=False)
159
+ sql_rc: int = field(init=False)
160
+ sql_state: str = field(init=False)
161
+ error: Optional[str] = field(default=None, init=False)
162
+
163
+
164
+ @dataclass
165
+ class JDBCOptions:
166
+ naming: Optional[str] = None
167
+ date_format: Optional[str] = None
168
+ date_separator: Optional[str] = None
169
+ decimal_separator: Optional[str] = None
170
+ time_format: Optional[str] = None
171
+ time_separator: Optional[str] = None
172
+ full_open: Optional[bool] = None
173
+ access: Optional[str] = None
174
+ autocommit_exception: Optional[bool] = None
175
+ bidi_string_type: Optional[str] = None
176
+ bidi_implicit_reordering: Optional[bool] = None
177
+ bidi_numeric_ordering: Optional[bool] = None
178
+ data_truncation: Optional[bool] = None
179
+ driver: Optional[str] = None
180
+ errors: Optional[str] = None
181
+ extended_metadata: Optional[bool] = None
182
+ hold_input_locators: Optional[bool] = None
183
+ hold_statements: Optional[bool] = None
184
+ ignore_warnings: Optional[str] = None
185
+ keep_alive: Optional[bool] = None
186
+ key_ring_name: Optional[str] = None
187
+ key_ring_password: Optional[str] = None
188
+ metadata_source: Optional[str] = None
189
+ proxy_server: Optional[str] = None
190
+ remarks: Optional[str] = None
191
+ secondary_URL: Optional[str] = None
192
+ secure: Optional[bool] = None
193
+ server_trace: Optional[str] = None
194
+ thread_used: Optional[bool] = None
195
+ toolbox_trace: Optional[str] = None
196
+ trace: Optional[bool] = None
197
+ translate_binary: Optional[bool] = None
198
+ translate_boolean: Optional[bool] = None
199
+ libraries: Optional[List[str]] = None
200
+ auto_commit: Optional[bool] = None
201
+ concurrent_access_resolution: Optional[str] = None
202
+ cursor_hold: Optional[bool] = None
203
+ cursor_sensitivity: Optional[str] = None
204
+ database_name: Optional[str] = None
205
+ decfloat_rounding_mode: Optional[str] = None
206
+ maximum_precision: Optional[str] = None
207
+ maximum_scale: Optional[str] = None
208
+ minimum_divide_scale: Optional[str] = None
209
+ package_ccsid: Optional[str] = None
210
+ transaction_isolation: Optional[str] = None
211
+ translate_hex: Optional[str] = None
212
+ true_autocommit: Optional[bool] = None
213
+ XA_loosely_coupled_support: Optional[str] = None
214
+ big_decimal: Optional[bool] = None
215
+ block_criteria: Optional[str] = None
216
+ block_size: Optional[str] = None
217
+ data_compression: Optional[bool] = None
218
+ extended_dynamic: Optional[bool] = None
219
+ lazy_close: Optional[bool] = None
220
+ lob_threshold: Optional[str] = None
221
+ maximum_blocked_input_rows: Optional[str] = None
222
+ package: Optional[str] = None
223
+ package_add: Optional[bool] = None
224
+ package_cache: Optional[bool] = None
225
+ package_criteria: Optional[str] = None
226
+ package_error: Optional[str] = None
227
+ package_library: Optional[str] = None
228
+ prefetch: Optional[bool] = None
229
+ qaqqinilib: Optional[str] = None
230
+ query_optimize_goal: Optional[str] = None
231
+ query_timeout_mechanism: Optional[str] = None
232
+ query_storage_limit: Optional[str] = None
233
+ receive_buffer_size: Optional[str] = None
234
+ send_buffer_size: Optional[str] = None
235
+ variable_field_compression: Optional[bool] = None
236
+ sort: Optional[str] = None
237
+ sort_language: Optional[str] = None
238
+ sort_table: Optional[str] = None
239
+ sort_weight: Optional[str] = None
@@ -0,0 +1,11 @@
1
+ _MAJOR = "0"
2
+ _MINOR = "1"
3
+ # On main and in a nightly release the patch should be one ahead of the last
4
+ # released build.
5
+ _PATCH = "3"
6
+ # This is mainly for nightly builds which have the suffix ".dev$DATE". See
7
+ # https://semver.org/#is-v123-a-semantic-version for the semantics.
8
+ _SUFFIX = ""
9
+
10
+ VERSION_SHORT = "{0}.{1}".format(_MAJOR, _MINOR)
11
+ VERSION = "{0}.{1}.{2}{3}".format(_MAJOR, _MINOR, _PATCH, _SUFFIX)