plsqlwks 0.1.2__tar.gz → 0.1.3__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.
Files changed (69) hide show
  1. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/PKG-INFO +1 -1
  2. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/__init__.py +1 -1
  3. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/execution.py +138 -24
  4. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/models.py +3 -11
  5. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/session.py +23 -7
  6. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/__init__.py +7 -0
  7. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app.py +37 -13
  8. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app_db.py +324 -114
  9. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app_editor.py +161 -40
  10. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app_files.py +1 -4
  11. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app_input.py +28 -1
  12. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app_results.py +49 -21
  13. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app_tabs_browser.py +25 -6
  14. plsqlwks-0.1.3/plsqlwks/ui/db_worker.py +228 -0
  15. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/results.py +6 -1
  16. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/state.py +10 -7
  17. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks.egg-info/PKG-INFO +1 -1
  18. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks.egg-info/SOURCES.txt +2 -0
  19. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_db_helpers.py +199 -2
  20. plsqlwks-0.1.3/tests/test_db_worker.py +464 -0
  21. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_oracle_integration.py +51 -0
  22. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_ui_flows.py +186 -61
  23. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_ui_keys.py +203 -2
  24. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/README.md +0 -0
  25. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/license.txt +0 -0
  26. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/__main__.py +0 -0
  27. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/config/__init__.py +0 -0
  28. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/config/loader.py +0 -0
  29. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/config/models.py +0 -0
  30. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/config/paths.py +0 -0
  31. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/config/session.py +0 -0
  32. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/config/settings.py +0 -0
  33. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/__init__.py +0 -0
  34. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/editing.py +0 -0
  35. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/explain.py +0 -0
  36. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/health.py +0 -0
  37. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/metadata.py +0 -0
  38. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/sql_analysis.py +0 -0
  39. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/transactions.py +0 -0
  40. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/sqlbinds.py +0 -0
  41. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/sqlsplit.py +0 -0
  42. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app_render.py +0 -0
  43. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/browser.py +0 -0
  44. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/buffer.py +0 -0
  45. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/clipboard.py +0 -0
  46. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/commands.py +0 -0
  47. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/completion.py +0 -0
  48. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/constants.py +0 -0
  49. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/display.py +0 -0
  50. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/errors.py +0 -0
  51. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/help.py +0 -0
  52. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/keys.py +0 -0
  53. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/menu.py +0 -0
  54. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/sql.py +0 -0
  55. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/syntax.py +0 -0
  56. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/workspace.py +0 -0
  57. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks.egg-info/dependency_links.txt +0 -0
  58. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks.egg-info/entry_points.txt +0 -0
  59. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks.egg-info/requires.txt +0 -0
  60. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks.egg-info/top_level.txt +0 -0
  61. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/pyproject.toml +0 -0
  62. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/setup.cfg +0 -0
  63. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_config_workspace.py +0 -0
  64. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_db_editing.py +0 -0
  65. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_packaging.py +0 -0
  66. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_pty_keys.py +0 -0
  67. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_rchar.py +0 -0
  68. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_sqlbinds.py +0 -0
  69. {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_sqlsplit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plsqlwks
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: An ncurses SQL and PL/SQL workspace for Oracle databases.
5
5
  License-Expression: LicenseRef-plsqlwks-Donationware
6
6
  Project-URL: Repository, https://gitlab.com/unununu/plsqlwks
@@ -1,3 +1,3 @@
1
1
  """ncurses SQL and PL/SQL workspace."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.3"
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from dataclasses import dataclass
3
4
  from datetime import datetime
4
5
  from decimal import Decimal
5
6
  from pathlib import Path
6
7
  import re
7
8
  from typing import Any, Mapping
9
+ from uuid import uuid4
8
10
 
9
11
  import oracledb
10
12
 
@@ -42,6 +44,13 @@ CREATE_PLSQL_OBJECT_RE = re.compile(
42
44
  LOB_DISPLAY_LIMIT = 64 * 1024
43
45
 
44
46
 
47
+ @dataclass
48
+ class _QueryResultContinuationState:
49
+ cursor: Any
50
+ lookahead_row: Any
51
+ elapsed_seconds: float
52
+
53
+
45
54
  class ExecutionMixin:
46
55
  def execute_statement(
47
56
  self,
@@ -109,7 +118,11 @@ class ExecutionMixin:
109
118
  more = ""
110
119
  continuation = None
111
120
  if len(fetched) > self.config.max_rows:
112
- continuation = QueryResultContinuation(cursor, fetched[self.config.max_rows], elapsed)
121
+ continuation = self._register_result_continuation(
122
+ cursor,
123
+ fetched[self.config.max_rows],
124
+ elapsed,
125
+ )
113
126
  keep_cursor_open = True
114
127
  more = f" (limited to {len(rows)} rows)"
115
128
  return QueryResult(
@@ -146,40 +159,104 @@ class ExecutionMixin:
146
159
  if not keep_cursor_open:
147
160
  cursor.close()
148
161
 
149
- def fetch_more_rows(self, result: QueryResult) -> QueryResultPage:
150
- continuation = result.continuation
151
- if continuation is None:
152
- return QueryResultPage([], result.message)
162
+ def fetch_more_rows(
163
+ self,
164
+ continuation: QueryResultContinuation,
165
+ loaded_rows: int,
166
+ ) -> QueryResultPage:
167
+ state = self._result_continuations.get(continuation.token)
168
+ if state is None:
169
+ raise RuntimeError("Query result is stale or no longer available")
153
170
  started = datetime.now()
154
- fetched = [continuation.lookahead_row, *continuation.cursor.fetchmany(self.config.max_rows)]
155
- elapsed = continuation.elapsed_seconds + (datetime.now() - started).total_seconds()
156
- result_rows = fetched[: self.config.max_rows]
157
- rows, original_rows = materialize_result_rows(result_rows)
158
- loaded_rows = len(result.rows) + len(rows)
159
- more = ""
160
- if len(fetched) > self.config.max_rows:
161
- continuation.lookahead_row = fetched[self.config.max_rows]
162
- continuation.elapsed_seconds = elapsed
163
- more = f" (limited to {loaded_rows} rows)"
164
- else:
165
- continuation.close()
166
- result.continuation = None
167
- return QueryResultPage(
168
- rows,
169
- f"{loaded_rows} row(s){more} in {elapsed:.2f}s",
170
- original_rows,
171
+ try:
172
+ fetched = [state.lookahead_row, *state.cursor.fetchmany(self.config.max_rows)]
173
+ elapsed = state.elapsed_seconds + (datetime.now() - started).total_seconds()
174
+ result_rows = fetched[: self.config.max_rows]
175
+ rows, original_rows = materialize_result_rows(result_rows)
176
+ total_loaded_rows = loaded_rows + len(rows)
177
+ more = ""
178
+ next_continuation: QueryResultContinuation | None = None
179
+ if len(fetched) > self.config.max_rows:
180
+ state.lookahead_row = fetched[self.config.max_rows]
181
+ state.elapsed_seconds = elapsed
182
+ next_continuation = continuation
183
+ more = f" (limited to {total_loaded_rows} rows)"
184
+ else:
185
+ self.close_result_continuation(continuation)
186
+ return QueryResultPage(
187
+ rows,
188
+ f"{total_loaded_rows} row(s){more} in {elapsed:.2f}s",
189
+ original_rows,
190
+ next_continuation,
191
+ )
192
+ except BaseException:
193
+ try:
194
+ self.close_result_continuation(continuation)
195
+ except Exception:
196
+ pass
197
+ raise
198
+
199
+ def close_result_continuation(self, continuation: QueryResultContinuation) -> None:
200
+ state = self._result_continuations.pop(continuation.token, None)
201
+ if state is not None:
202
+ state.cursor.close()
203
+
204
+ def close_all_result_continuations(self) -> None:
205
+ states = list(self._result_continuations.values())
206
+ self._result_continuations.clear()
207
+ first_error: Exception | None = None
208
+ for state in states:
209
+ try:
210
+ state.cursor.close()
211
+ except Exception as exc:
212
+ if first_error is None:
213
+ first_error = exc
214
+ if first_error is not None:
215
+ raise first_error
216
+
217
+ def _register_result_continuation(
218
+ self,
219
+ cursor: Any,
220
+ lookahead_row: Any,
221
+ elapsed_seconds: float,
222
+ ) -> QueryResultContinuation:
223
+ continuation = QueryResultContinuation(uuid4().hex)
224
+ self._result_continuations[continuation.token] = _QueryResultContinuationState(
225
+ cursor,
226
+ lookahead_row,
227
+ elapsed_seconds,
171
228
  )
229
+ return continuation
172
230
 
173
231
  def execute_script(self, script: str) -> list[QueryResult]:
174
232
  results: list[QueryResult] = []
175
233
  statements = split_script(script)
176
234
  for idx, statement in enumerate(statements, start=1):
177
235
  title = f"Statement {idx} lines {statement.start_line}-{statement.end_line}"
178
- results.append(self.execute_statement(statement.text, title=title))
236
+ result = self.execute_statement(statement.text, title=title)
237
+ self._append_script_result(results, result)
179
238
  if not statements:
180
239
  results.append(QueryResult("Script", [], [], "No statements to execute."))
181
240
  return results
182
241
 
242
+ def _append_script_result(
243
+ self,
244
+ results: list[QueryResult],
245
+ result: QueryResult,
246
+ ) -> None:
247
+ if result.columns and not _is_dbms_output_result(result):
248
+ for previous in reversed(results):
249
+ if not previous.columns or _is_dbms_output_result(previous):
250
+ continue
251
+ if previous.continuation is not None:
252
+ try:
253
+ self.close_result_continuation(previous.continuation)
254
+ except Exception:
255
+ pass
256
+ previous.continuation = None
257
+ break
258
+ results.append(result)
259
+
183
260
  def export_result(self, result: QueryResult, path: Path) -> None:
184
261
  path.parent.mkdir(parents=True, exist_ok=True)
185
262
  with path.open("w", encoding="utf-8", newline="") as handle:
@@ -223,6 +300,10 @@ def format_value(value: Any, *, lob_limit: int = LOB_DISPLAY_LIMIT) -> str:
223
300
  return display
224
301
 
225
302
 
303
+ def _is_dbms_output_result(result: QueryResult) -> bool:
304
+ return len(result.columns) == 1 and result.columns[0].upper() == "DBMS_OUTPUT"
305
+
306
+
226
307
  def materialize_result_value(
227
308
  value: Any,
228
309
  *,
@@ -230,6 +311,9 @@ def materialize_result_value(
230
311
  ) -> tuple[str, Any]:
231
312
  if value is None:
232
313
  return NULL_DISPLAY_TOKEN, None
314
+ if isinstance(value, oracledb.Cursor):
315
+ value.close()
316
+ return "<REF CURSOR>", "<REF CURSOR>"
233
317
  if isinstance(value, oracledb.LOB):
234
318
  if lob_limit <= 0:
235
319
  raise ValueError("LOB display limit must be positive")
@@ -243,7 +327,37 @@ def materialize_result_value(
243
327
  return display, TruncatedLobValue(type_name, size)
244
328
  content = value.read()
245
329
  return _format_non_lob_value(content), content
246
- return _format_non_lob_value(value), value
330
+ materialized = _materialize_plain_result_value(value, lob_limit=lob_limit)
331
+ return _format_non_lob_value(materialized), materialized
332
+
333
+
334
+ def _materialize_plain_result_value(value: Any, *, lob_limit: int) -> Any:
335
+ """Return result data that contains no live python-oracledb handles."""
336
+ if isinstance(value, oracledb.Cursor):
337
+ value.close()
338
+ return "<REF CURSOR>"
339
+ if isinstance(value, oracledb.LOB):
340
+ return materialize_result_value(value, lob_limit=lob_limit)[1]
341
+ if isinstance(value, list):
342
+ return [_materialize_plain_result_value(item, lob_limit=lob_limit) for item in value]
343
+ if isinstance(value, tuple):
344
+ return tuple(_materialize_plain_result_value(item, lob_limit=lob_limit) for item in value)
345
+ if isinstance(value, dict):
346
+ return {
347
+ _materialize_plain_result_value(key, lob_limit=lob_limit): _materialize_plain_result_value(
348
+ item, lob_limit=lob_limit
349
+ )
350
+ for key, item in value.items()
351
+ }
352
+ if isinstance(value, set):
353
+ return {_materialize_plain_result_value(item, lob_limit=lob_limit) for item in value}
354
+ if isinstance(value, frozenset):
355
+ return frozenset(
356
+ _materialize_plain_result_value(item, lob_limit=lob_limit) for item in value
357
+ )
358
+ if type(value).__module__.startswith("oracledb"):
359
+ return str(value)
360
+ return value
247
361
 
248
362
 
249
363
  def _format_non_lob_value(value: Any) -> str:
@@ -39,18 +39,9 @@ class RowInsertResult:
39
39
  display_values: list[str]
40
40
 
41
41
 
42
- @dataclass
42
+ @dataclass(frozen=True)
43
43
  class QueryResultContinuation:
44
- cursor: Any
45
- lookahead_row: Any
46
- elapsed_seconds: float
47
- closed: bool = False
48
-
49
- def close(self) -> None:
50
- if self.closed:
51
- return
52
- self.closed = True
53
- self.cursor.close()
44
+ token: str = field(repr=False)
54
45
 
55
46
 
56
47
  @dataclass(frozen=True)
@@ -58,6 +49,7 @@ class QueryResultPage:
58
49
  rows: list[list[str]]
59
50
  message: str
60
51
  original_rows: list[list[Any]] = field(default_factory=list, repr=False, compare=False)
52
+ continuation: QueryResultContinuation | None = field(default=None, repr=False, compare=False)
61
53
 
62
54
 
63
55
  @dataclass
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import oracledb
4
+ from typing import Any
4
5
 
5
6
  from ..config import AppConfig
6
7
  from .editing import EditingMixin
@@ -24,27 +25,41 @@ class OracleWorkspace(
24
25
  self.read_only = config.read_only
25
26
  self.pending_rows_changed = 0
26
27
  self.pending_unknown_changes = False
28
+ self._result_continuations: dict[str, Any] = {}
27
29
 
28
30
  def connect(self) -> None:
29
31
  from . import read_password
30
32
 
31
33
  password = read_password(self.config.password_file)
32
34
  self.close()
33
- self.connection = oracledb.connect(
35
+ connection = oracledb.connect(
34
36
  user=self.config.user,
35
37
  password=password,
36
38
  dsn=self.config.dsn,
37
39
  )
38
- self.apply_autocommit()
39
- self.enable_dbms_output()
40
+ self.connection = connection
41
+ try:
42
+ self.apply_autocommit()
43
+ self.enable_dbms_output()
44
+ except Exception:
45
+ try:
46
+ connection.close()
47
+ except Exception:
48
+ pass
49
+ self.connection = None
50
+ raise
40
51
 
41
52
  def close(self) -> None:
42
- if self.connection is not None:
53
+ connection = self.connection
54
+ try:
55
+ self.close_all_result_continuations()
56
+ finally:
43
57
  try:
44
- self.connection.close()
58
+ if connection is not None:
59
+ connection.close()
45
60
  finally:
46
61
  self.connection = None
47
- self.clear_pending_transaction()
62
+ self.clear_pending_transaction()
48
63
 
49
64
  def ensure_connected(self) -> oracledb.Connection:
50
65
  if self.connection is None:
@@ -63,8 +78,9 @@ class OracleWorkspace(
63
78
  return True
64
79
 
65
80
  def set_autocommit(self, enabled: bool) -> None:
81
+ if self.connection is not None and hasattr(self.connection, "autocommit"):
82
+ self.connection.autocommit = enabled
66
83
  self.autocommit = enabled
67
- self.apply_autocommit()
68
84
 
69
85
  def set_read_only(self, enabled: bool) -> None:
70
86
  self.read_only = enabled
@@ -36,6 +36,7 @@ from .browser import *
36
36
  from .menu import *
37
37
  from .commands import *
38
38
  from .results import *
39
+ from .db_worker import *
39
40
  from .state import *
40
41
  from .app import App, main, parse_args
41
42
 
@@ -130,6 +131,12 @@ __all__ = (
130
131
  "DbOperationEvent",
131
132
  "DbOperationFinished",
132
133
  "DbOperationProgress",
134
+ "DatabaseWorker",
135
+ "DbCommandHandle",
136
+ "DbSessionState",
137
+ "DbWorkerEvent",
138
+ "DbWorkerFinished",
139
+ "DbWorkerProgress",
133
140
  "ERROR_LOCATION_PRIORITY_EXACT",
134
141
  "ERROR_LOCATION_PRIORITY_FALLBACK",
135
142
  "ERROR_LOCATION_PRIORITY_OFFSET",
@@ -8,6 +8,7 @@ from ..config import AppConfig, load_config
8
8
  from ..db import OracleWorkspace, workspace_health
9
9
  from ..workspace import ensure_workspace, list_workspace_files
10
10
  from .keys import configure_utf8_locale, disable_extended_keyboard_reporting, enable_extended_keyboard_reporting
11
+ from .db_worker import DatabaseWorker
11
12
  from .state import UIState
12
13
  from .app_render import AppRenderMixin
13
14
  from .app_db import AppDbMixin
@@ -77,7 +78,8 @@ class App(
77
78
  ):
78
79
  def __init__(self, screen: curses.window, config: AppConfig):
79
80
  self.screen = screen
80
- self.state = UIState(config=config, db=OracleWorkspace(config))
81
+ self.db_worker = DatabaseWorker(OracleWorkspace(config))
82
+ self.state = UIState(config=config, db=self.db_worker.session_state)
81
83
  self.state.files = list_workspace_files(config)
82
84
  self.running = True
83
85
  self.message_lines: list[str] = []
@@ -115,16 +117,38 @@ class App(
115
117
  self.handle_key(key)
116
118
  self.poll_db_operation()
117
119
  finally:
118
- self.wait_for_db_operation()
119
- if extended_keyboard_enabled:
120
- disable_extended_keyboard_reporting()
120
+ shutdown_timeout = 5.0 if self.state.db_operation is not None else None
121
121
  try:
122
- curses.nl()
123
- except curses.error:
124
- pass
125
- try:
126
- curses.noraw()
127
- except curses.error:
128
- pass
129
- self.close_all_result_continuations()
130
- self.state.db.close()
122
+ try:
123
+ if self.state.db_operation is not None:
124
+ try:
125
+ self.db_worker.cancel_current_operation(
126
+ self.state.db_operation.handle.command_id
127
+ )
128
+ except Exception:
129
+ pass
130
+ if shutdown_timeout is None:
131
+ self.wait_for_db_operation()
132
+ else:
133
+ self.wait_for_db_operation(timeout=shutdown_timeout)
134
+ finally:
135
+ if extended_keyboard_enabled:
136
+ disable_extended_keyboard_reporting()
137
+ finally:
138
+ try:
139
+ try:
140
+ curses.nl()
141
+ except curses.error:
142
+ pass
143
+ try:
144
+ curses.noraw()
145
+ except curses.error:
146
+ pass
147
+ finally:
148
+ try:
149
+ self.close_all_result_continuations()
150
+ finally:
151
+ if shutdown_timeout is None:
152
+ self.shutdown_database_worker()
153
+ else:
154
+ self.shutdown_database_worker(timeout=shutdown_timeout)