dbasim 0.3.1__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.
dbasim/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.3.1"
dbasim/cli.py ADDED
@@ -0,0 +1,485 @@
1
+ import argparse
2
+ import os
3
+ import random
4
+ import signal
5
+ import string
6
+ import subprocess
7
+ import sys
8
+ import time
9
+
10
+ from . import __version__, db, license, state
11
+ from .i18n import lang, t
12
+ from .scenarios import ALL, get
13
+
14
+ OK, BAD = "✓", "✗"
15
+
16
+
17
+ def _say(text=""):
18
+ print(text, flush=True)
19
+
20
+
21
+ def _password(rng):
22
+ alphabet = string.ascii_letters + string.digits
23
+ return "Sh" + "".join(rng.choice(alphabet) for _ in range(12))
24
+
25
+
26
+ # ---- workload process -------------------------------------------------------
27
+
28
+ def _launch_workload():
29
+ log = open(state.app_log_path(), "a", encoding="utf-8")
30
+ kwargs = {"stdout": subprocess.DEVNULL, "stderr": log}
31
+ if os.name == "nt":
32
+ kwargs["creationflags"] = 0x00000008 | 0x00000200 # DETACHED | NEW_PROCESS_GROUP
33
+ else:
34
+ kwargs["start_new_session"] = True
35
+ return subprocess.Popen([sys.executable, "-m", "dbasim.workload"], **kwargs).pid
36
+
37
+
38
+ def _stop_workload(active):
39
+ pid = (active or {}).get("workload_pid")
40
+ if not pid:
41
+ return
42
+ try:
43
+ os.kill(pid, signal.SIGTERM)
44
+ except (OSError, ProcessLookupError):
45
+ pass
46
+ active["workload_pid"] = None
47
+
48
+
49
+ def _alive(pid):
50
+ if not pid:
51
+ return False
52
+ if os.name == "nt":
53
+ # os.kill(pid, 0) would *terminate* the process on Windows
54
+ import ctypes
55
+ k32 = ctypes.windll.kernel32
56
+ handle = k32.OpenProcess(0x1000, False, pid) # PROCESS_QUERY_LIMITED_INFORMATION
57
+ if not handle:
58
+ return False
59
+ code = ctypes.c_ulong()
60
+ k32.GetExitCodeProcess(handle, ctypes.byref(code))
61
+ k32.CloseHandle(handle)
62
+ return code.value == 259 # STILL_ACTIVE
63
+ try:
64
+ os.kill(pid, 0)
65
+ return True
66
+ except OSError:
67
+ return False
68
+
69
+
70
+ # ---- commands ---------------------------------------------------------------
71
+
72
+ def cmd_doctor(args):
73
+ c = db.config()
74
+ _say(f"dbasim {__version__} python {sys.version.split()[0]}")
75
+ _say(f"DSN: {c['dsn']} user: {c['admin_user']}")
76
+ admin = db.admin_connect()
77
+ banner, con = db.assert_safe_target(admin)
78
+ _say(t(f"{OK} ต่อ DB ได้: {banner}", f"{OK} connected: {banner}"))
79
+ _say(f"{OK} container: {con}")
80
+ users_ts = db.scalar(admin, "select count(*) from dba_tablespaces where tablespace_name = 'USERS'")
81
+ _say(t(f"{OK if users_ts else BAD} มี tablespace USERS", f"{OK if users_ts else BAD} tablespace USERS exists"))
82
+ _say(t("\nพร้อมเล่นแล้ว ลอง: dbasim list", "\nReady to play. Try: dbasim list"))
83
+ return 0
84
+
85
+
86
+ def cmd_list(args):
87
+ st = state.load()
88
+ best = state.best_scores(st)
89
+ active = (st.get("active") or {}).get("scenario")
90
+ _say(t(f"{'ID':<5}{'ระดับ':<7}{'แผน':<6}{'คะแนนสูงสุด':<14}โจทย์",
91
+ f"{'ID':<5}{'Level':<8}{'Plan':<6}{'Best':<6}Scenario"))
92
+ for s in ALL:
93
+ mark = t(" <- กำลังเล่น", " <- playing") if s.id == active else ""
94
+ score = f"{best[s.id]}" if s.id in best else "-"
95
+ plan = "Pro" if s.premium else t("ฟรี", "Free")
96
+ if lang() == "en":
97
+ _say(f"{s.id:<5}{s.difficulty:<8}{plan:<6}{score:<6}{s.title}{mark}")
98
+ else:
99
+ _say(f"{s.id:<5}{s.difficulty:<7}{plan:<6}{score:<14}{s.title}{mark}")
100
+ if not license.on_sale() and any(s.premium for s in ALL):
101
+ _say(t("โจทย์ Pro ยังไม่เปิดขาย (เร็วๆ นี้) ตอนนี้เล่นโจทย์ฟรีได้ทุกข้อ",
102
+ "Pro scenarios are not on sale yet (coming soon). Every Free scenario is playable now"))
103
+ return 0
104
+
105
+
106
+ def cmd_start(args):
107
+ scenario = get(args.id)
108
+ st = state.load()
109
+ if st.get("active") and not st["active"].get("solved") and not args.force:
110
+ _say(t(f"ยังมีโจทย์ {st['active']['scenario']} ค้างอยู่ ใช้ dbasim reset ก่อน หรือเพิ่ม --force",
111
+ f"Scenario {st['active']['scenario']} is still in progress. Run dbasim reset first, or add --force"))
112
+ return 1
113
+ if scenario.premium and not getattr(args, "skip_license", False):
114
+ try:
115
+ license.require_pro()
116
+ except license.LicenseError as exc:
117
+ _say(f"{BAD} {exc}")
118
+ return 1
119
+ _stop_workload(st.get("active"))
120
+
121
+ rng = random.Random()
122
+ params = scenario.make_params(rng, db.scale())
123
+ params["shop_password"] = _password(rng)
124
+
125
+ admin = db.admin_connect()
126
+ db.assert_safe_target(admin)
127
+ _say(t(f"กำลังเตรียมสถานการณ์ {scenario.id}: {scenario.title} (อาจใช้เวลา 1-3 นาที) ...",
128
+ f"Preparing scenario {scenario.id}: {scenario.title} (may take 1-3 minutes) ..."))
129
+ scenario.teardown(admin, params)
130
+ scenario.setup(admin, params)
131
+ baseline = scenario.baseline(admin, params)
132
+ admin.close()
133
+
134
+ st["active"] = state.new_active(scenario.id, params, baseline)
135
+ state.app_log_path().write_text("", encoding="utf-8")
136
+ state.save(st) # the workload process reads this file, so save before launching it
137
+ st["active"]["workload_pid"] = _launch_workload()
138
+ state.save(st)
139
+
140
+ _say("\n" + "=" * 64)
141
+ _say(str(scenario.story))
142
+ _say("=" * 64)
143
+ _say(t("\nแอปจำลองเริ่มทำงานแล้ว ดู log แอป: dbasim logs | ขอคำใบ้: dbasim hint",
144
+ "\nThe simulated app is running. App log: dbasim logs | Hint: dbasim hint"))
145
+ _say(t("แก้เสร็จแล้วสั่ง: dbasim check", "When you have fixed it, run: dbasim check"))
146
+ return 0
147
+
148
+
149
+ def _require_active(st):
150
+ if not st.get("active"):
151
+ _say(t("ยังไม่มีโจทย์ที่กำลังเล่น ใช้ dbasim start <id>", "No scenario in progress. Use dbasim start <id>"))
152
+ return None
153
+ return st["active"]
154
+
155
+
156
+ def cmd_status(args):
157
+ st = state.load()
158
+ active = _require_active(st)
159
+ if not active:
160
+ return 1
161
+ s = get(active["scenario"])
162
+ minutes = (time.time() - active["started_at"]) / 60
163
+ running = bool(active.get("workload_pid"))
164
+ _say(t(f"โจทย์: {s.id} {s.title}", f"Scenario: {s.id} {s.title}"))
165
+ _say(t(f"เวลาที่ใช้: {minutes:.0f} นาที คำใบ้ที่เปิด: {active['hints_shown']}/{len(s.hints)}",
166
+ f"Time: {minutes:.0f} min Hints used: {active['hints_shown']}/{len(s.hints)}"))
167
+ _say(t(f"คะแนนถ้าผ่านตอนนี้: {state.score(active)}", f"Score if solved now: {state.score(active)}"))
168
+ _say(t(f"user ของแอป: SHOP / {active['params']['shop_password']}",
169
+ f"App user: SHOP / {active['params']['shop_password']}"))
170
+ _say(t(f"แอปจำลอง: {'ทำงานอยู่' if running else 'หยุดแล้ว'}",
171
+ f"Simulated app: {'running' if running else 'stopped'}"))
172
+ return 0
173
+
174
+
175
+ def cmd_logs(args):
176
+ path = state.app_log_path()
177
+ lines = path.read_text(encoding="utf-8").splitlines() if path.exists() else []
178
+ for line in lines[-args.n:]:
179
+ _say(line)
180
+ if not lines:
181
+ _say(t("(ยังไม่มี log รอสักครู่ให้แอปทำงาน)", "(no log yet - give the app a moment)"))
182
+ return 0
183
+
184
+
185
+ def cmd_hint(args):
186
+ st = state.load()
187
+ active = _require_active(st)
188
+ if not active:
189
+ return 1
190
+ s = get(active["scenario"])
191
+ i = active["hints_shown"]
192
+ if i >= len(s.hints):
193
+ _say(t("คำใบ้หมดแล้ว ถ้ายอมแพ้ใช้ dbasim solution", "No hints left. To give up, use dbasim solution"))
194
+ return 0
195
+ active["hints_shown"] = i + 1
196
+ state.save(st)
197
+ _say(t(f"คำใบ้ {i + 1}/{len(s.hints)} (หัก {state.HINT_PENALTY} คะแนน): {s.hints[i]}",
198
+ f"Hint {i + 1}/{len(s.hints)} (-{state.HINT_PENALTY} points): {s.hints[i]}"))
199
+ return 0
200
+
201
+
202
+ def cmd_check(args):
203
+ st = state.load()
204
+ active = _require_active(st)
205
+ if not active:
206
+ return 1
207
+ s = get(active["scenario"])
208
+ _say(t("กำลังตรวจ ...", "Checking ..."))
209
+ if s.check_needs_workload and not active.get("solved") and not _alive(active.get("workload_pid")):
210
+ _say(t(f"{BAD} แอปจำลองไม่ได้ทำงานอยู่ โจทย์นี้ต้องตรวจตอนที่แอปทำงาน "
211
+ f"เริ่มใหม่ด้วย dbasim start {s.id} --force",
212
+ f"{BAD} The simulated app is not running and this scenario must be checked while it runs. "
213
+ f"Restart with dbasim start {s.id} --force"))
214
+ return 1
215
+ # usually pause the simulated app so its load does not skew the measurement
216
+ was_running = bool(active.get("workload_pid")) and not active.get("solved")
217
+ paused = was_running and s.pause_workload_during_check
218
+ if paused:
219
+ _stop_workload(active)
220
+ state.save(st)
221
+
222
+ def resume():
223
+ if paused:
224
+ active["workload_pid"] = _launch_workload()
225
+ state.save(st)
226
+
227
+ admin = db.admin_connect()
228
+ try:
229
+ results = s.check(admin, active["params"], active["baseline"])
230
+ except Exception as exc:
231
+ _say(t(f"{BAD} ตรวจไม่ได้: {db.first_line(exc)}", f"{BAD} Could not check: {db.first_line(exc)}"))
232
+ _say(t("(ถ้า object ของโจทย์หายไป ให้เริ่มใหม่ด้วย dbasim start --force)",
233
+ "(if the scenario's objects are gone, restart with dbasim start --force)"))
234
+ resume()
235
+ return 1
236
+ finally:
237
+ admin.close()
238
+ for r in results:
239
+ _say(f" {OK if r.passed else BAD} {r.name}" + (f" — {r.detail}" if r.detail else ""))
240
+ if not all(r.passed for r in results):
241
+ _say(t("\nยังไม่ผ่าน ลองต่อ หรือขอคำใบ้ด้วย dbasim hint", "\nNot solved yet. Keep going, or ask for a hint: dbasim hint"))
242
+ resume()
243
+ return 2
244
+ if not active.get("solved"):
245
+ entry = state.record_solved(st, active)
246
+ active["solved"] = True
247
+ _stop_workload(active)
248
+ state.save(st)
249
+ _say(t(f"\nผ่าน! คะแนน {entry['score']} ใช้เวลา {entry['minutes']} นาที",
250
+ f"\nSolved! Score {entry['score']} in {entry['minutes']} min"))
251
+ _say(t("\n--- เฉลยแบบ DBA อาวุโส ---\n", "\n--- How a senior DBA would explain it ---\n") + str(s.solution))
252
+ return 0
253
+
254
+
255
+ def cmd_solution(args):
256
+ st = state.load()
257
+ active = _require_active(st)
258
+ if not active:
259
+ return 1
260
+ if not args.yes:
261
+ _say(t("ดูเฉลยแล้วโจทย์นี้จะได้ 0 คะแนน ถ้าแน่ใจให้สั่ง dbasim solution --yes",
262
+ "Viewing the solution scores 0 for this scenario. If you are sure: dbasim solution --yes"))
263
+ return 0
264
+ active["solution_revealed"] = True
265
+ state.save(st)
266
+ _say(str(get(active["scenario"]).solution))
267
+ return 0
268
+
269
+
270
+ def cmd_stop(args):
271
+ st = state.load()
272
+ _stop_workload(st.get("active"))
273
+ state.save(st)
274
+ _say(t("หยุดแอปจำลองแล้ว", "Simulated app stopped"))
275
+ return 0
276
+
277
+
278
+ def cmd_reset(args):
279
+ st = state.load()
280
+ _stop_workload(st.get("active"))
281
+ admin = db.admin_connect()
282
+ db.assert_safe_target(admin)
283
+ ALL[0].teardown(admin, {})
284
+ admin.close()
285
+ st["active"] = None
286
+ state.save(st)
287
+ _say(t("ล้างสถานการณ์เรียบร้อย", "Scenario cleaned up"))
288
+ return 0
289
+
290
+
291
+ def _print_criteria(label, results):
292
+ _say(f" [{label}]")
293
+ for r in results:
294
+ _say(f" {OK if r.passed else BAD} {r.name}" + (f" — {r.detail}" if r.detail else ""))
295
+
296
+
297
+ def cmd_selftest(args):
298
+ """Prove each scenario on this real database: broken before the fix, passing after it."""
299
+ import platform
300
+ from argparse import Namespace
301
+ import oracledb
302
+
303
+ admin = db.admin_connect()
304
+ banner, con = db.assert_safe_target(admin)
305
+ admin.close()
306
+ _say(f"dbasim {__version__} | python {platform.python_version()} | oracledb {oracledb.__version__}"
307
+ f" | {platform.system()} {platform.release()}")
308
+ _say(f"{banner} | {con} | DBASIM_SCALE={db.scale()}")
309
+
310
+ ids = args.ids or [s.id for s in ALL]
311
+ summary = []
312
+ for sid in ids:
313
+ s = get(sid)
314
+ _say(f"\n===== {s.id} {s.title} =====")
315
+ t0 = time.time()
316
+ verdict, note = "ERROR", ""
317
+ try:
318
+ if cmd_start(Namespace(id=s.id, force=True, skip_license=True)) != 0:
319
+ raise RuntimeError("start failed")
320
+ _say(f" setup {time.time() - t0:.0f}s, waiting {args.warmup}s for the app to hit the problem ...")
321
+ time.sleep(args.warmup)
322
+ active = state.load()["active"]
323
+ log_lines = state.app_log_path().read_text(encoding="utf-8").splitlines()
324
+ _say(" [app log, last 6 lines]")
325
+ for line in log_lines[-6:]:
326
+ _say(" " + line)
327
+ admin = db.admin_connect()
328
+ try:
329
+ before = s.check(admin, active["params"], active["baseline"])
330
+ _print_criteria("before fix — should NOT pass", before)
331
+ s.reference_fix(admin, active["params"])
332
+ time.sleep(3)
333
+ after = s.check(admin, active["params"], active["baseline"])
334
+ _print_criteria("after reference fix — should pass", after)
335
+ finally:
336
+ admin.close()
337
+ reproduced = not all(r.passed for r in before)
338
+ solvable = all(r.passed for r in after)
339
+ verdict = "OK" if reproduced and solvable else "FAIL"
340
+ note = ("" if reproduced else "problem did not reproduce; ") + ("" if solvable else "fix did not pass")
341
+ except Exception as exc:
342
+ note = db.first_line(exc)
343
+ _say(f" {BAD} {note}")
344
+ finally:
345
+ try:
346
+ cmd_reset(Namespace())
347
+ except Exception as exc:
348
+ _say(f" {BAD} reset failed: {db.first_line(exc)}")
349
+ summary.append((s.id, verdict, round(time.time() - t0), note))
350
+
351
+ _say("\n===== SUMMARY =====")
352
+ for sid, verdict, secs, note in summary:
353
+ _say(f"{sid} {verdict:<5} {secs:>4}s {note}")
354
+ return 0 if all(v == "OK" for _, v, _, _ in summary) else 2
355
+
356
+
357
+ # ---- dbasim Pro licence + language ----------------------------------------
358
+
359
+ def _show_license(lic):
360
+ _say(t(f"{OK} dbasim Pro ใช้งานได้", f"{OK} dbasim Pro is active"))
361
+ _say(f" key: {license.masked(lic['key'])}")
362
+ if lic.get("plan"):
363
+ _say(t(f" แผน: {lic['plan']}", f" plan: {lic['plan']}"))
364
+ if lic.get("email"):
365
+ _say(t(f" อีเมล: {lic['email']}", f" email: {lic['email']}"))
366
+
367
+
368
+ def cmd_activate(args):
369
+ try:
370
+ lic = license.activate(args.key)
371
+ except license.LicenseError as exc:
372
+ _say(f"{BAD} {exc}")
373
+ return 1
374
+ except license.Offline as exc:
375
+ _say(t(f"{BAD} ต่อ Lemon Squeezy ไม่ได้: {exc}", f"{BAD} Could not reach Lemon Squeezy: {exc}"))
376
+ return 1
377
+ _show_license(lic)
378
+ _say(t("เล่นได้ทุกโจทย์แล้ว ลอง: dbasim list", "Every scenario is unlocked. Try: dbasim list"))
379
+ return 0
380
+
381
+
382
+ def cmd_license(args):
383
+ lic = license.load()
384
+ if not lic:
385
+ _say(t("ยังไม่ได้เปิดใช้ dbasim Pro (3 โจทย์แรกเล่นฟรี)", "dbasim Pro is not activated (the first 3 scenarios are free)"))
386
+ if not license.on_sale():
387
+ _say(t(f"dbasim Pro ยังไม่เปิดขาย (เร็วๆ นี้) ติดตามได้ที่ {license.PROJECT_URL}",
388
+ f"dbasim Pro is not on sale yet (coming soon). Follow {license.PROJECT_URL}"))
389
+ return 0
390
+ _say(t(f"สมัคร: {license.pricing_url()} แล้วสั่ง dbasim activate <license-key>",
391
+ f"Subscribe: {license.pricing_url()} then run dbasim activate <licence-key>"))
392
+ return 0
393
+ try:
394
+ lic = license.refresh() if args.refresh else license.require_pro()
395
+ except license.LicenseError as exc:
396
+ _say(f"{BAD} {exc}")
397
+ return 1
398
+ except license.Offline as exc:
399
+ _say(t(f"{BAD} ต่อ Lemon Squeezy ไม่ได้: {exc}", f"{BAD} Could not reach Lemon Squeezy: {exc}"))
400
+ return 1
401
+ _show_license(lic)
402
+ return 0
403
+
404
+
405
+ def cmd_deactivate(args):
406
+ try:
407
+ removed = license.deactivate()
408
+ except license.LicenseError as exc:
409
+ _say(f"{BAD} {exc}")
410
+ return 1
411
+ _say(t("ถอด license ออกจากเครื่องนี้แล้ว ใช้ key เดิมกับเครื่องอื่นได้",
412
+ "Licence removed from this computer; you can use the key on another one")
413
+ if removed else t("เครื่องนี้ยังไม่ได้เปิดใช้ dbasim Pro", "dbasim Pro is not activated on this computer"))
414
+ return 0
415
+
416
+
417
+ def cmd_lang(args):
418
+ st = state.load()
419
+ if args.code:
420
+ st["lang"] = args.code
421
+ state.save(st)
422
+ _say(t("ภาษา: ไทย (เปลี่ยนเป็นอังกฤษ: dbasim lang en)", "Language: English (switch to Thai: dbasim lang th)"))
423
+ return 0
424
+
425
+
426
+ def build_parser():
427
+ p = argparse.ArgumentParser(prog="dbasim", description="Oracle DBA break-fix simulator")
428
+ p.add_argument("--version", action="version", version=__version__)
429
+ sub = p.add_subparsers(dest="cmd", required=True)
430
+ sub.add_parser("doctor", help=t("ตรวจว่าต่อ Oracle Free ได้และพร้อมเล่น",
431
+ "check the connection to Oracle Free")).set_defaults(fn=cmd_doctor)
432
+ sub.add_parser("list", help=t("ดูรายการโจทย์", "list scenarios")).set_defaults(fn=cmd_list)
433
+ sp = sub.add_parser("start", help=t("เริ่มโจทย์", "start a scenario"))
434
+ sp.add_argument("id")
435
+ sp.add_argument("--force", action="store_true")
436
+ sp.set_defaults(fn=cmd_start)
437
+ sub.add_parser("status", help=t("สถานะโจทย์ปัจจุบัน", "current scenario status")).set_defaults(fn=cmd_status)
438
+ lp = sub.add_parser("logs", help=t("ดู log ของแอปจำลอง", "show the simulated app's log"))
439
+ lp.add_argument("-n", type=int, default=30)
440
+ lp.set_defaults(fn=cmd_logs)
441
+ sub.add_parser("hint", help=t("ขอคำใบ้ (หักคะแนน)", "get a hint (costs points)")).set_defaults(fn=cmd_hint)
442
+ sub.add_parser("check", help=t("ตรวจคำตอบ", "check your fix")).set_defaults(fn=cmd_check)
443
+ so = sub.add_parser("solution", help=t("ดูเฉลย (ได้ 0 คะแนน)", "show the solution (scores 0)"))
444
+ so.add_argument("--yes", action="store_true")
445
+ so.set_defaults(fn=cmd_solution)
446
+ sub.add_parser("stop", help=t("หยุดแอปจำลอง", "stop the simulated app")).set_defaults(fn=cmd_stop)
447
+ sub.add_parser("reset", help=t("ล้างทุกอย่างที่ dbasim สร้าง",
448
+ "remove everything dbasim created")).set_defaults(fn=cmd_reset)
449
+ ap = sub.add_parser("activate", help=t("เปิดใช้ dbasim Pro ด้วย license key", "activate dbasim Pro with a licence key"))
450
+ ap.add_argument("key")
451
+ ap.set_defaults(fn=cmd_activate)
452
+ lc = sub.add_parser("license", help=t("ดูสถานะ dbasim Pro", "show dbasim Pro status"))
453
+ lc.add_argument("--refresh", action="store_true")
454
+ lc.set_defaults(fn=cmd_license)
455
+ sub.add_parser("deactivate", help=t("ถอด license ออกจากเครื่องนี้",
456
+ "remove the licence from this computer")).set_defaults(fn=cmd_deactivate)
457
+ lg = sub.add_parser("lang", help=t("เปลี่ยนภาษา th / en", "switch language th / en"))
458
+ lg.add_argument("code", nargs="?", choices=["th", "en"])
459
+ lg.set_defaults(fn=cmd_lang)
460
+ tp = sub.add_parser("selftest", help=t("(สำหรับผู้พัฒนา) ทดสอบทุกโจทย์กับ DB จริง: ต้องพังก่อนแก้ และผ่านหลังแก้",
461
+ "(developers) prove every scenario on a real DB: broken before, passing after"))
462
+ tp.add_argument("ids", nargs="*")
463
+ tp.add_argument("--warmup", type=int, default=45)
464
+ tp.set_defaults(fn=cmd_selftest)
465
+ return p
466
+
467
+
468
+ def main(argv=None):
469
+ if hasattr(sys.stdout, "reconfigure"):
470
+ sys.stdout.reconfigure(encoding="utf-8") # Thai text on Windows consoles
471
+ args = build_parser().parse_args(argv)
472
+ try:
473
+ return args.fn(args)
474
+ except db.SetupError as exc:
475
+ _say(f"{BAD} {exc.args[0] if exc.args else exc}")
476
+ return 1
477
+ except Exception as exc:
478
+ if db.ora_code(exc):
479
+ _say(f"{BAD} Oracle error: {db.first_line(exc)}")
480
+ return 1
481
+ raise
482
+
483
+
484
+ if __name__ == "__main__":
485
+ sys.exit(main())