pythia-plsql 0.3.0__tar.gz → 0.3.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.
Files changed (39) hide show
  1. {pythia_plsql-0.3.0/scripts/pythia_plsql.egg-info → pythia_plsql-0.3.2}/PKG-INFO +1 -1
  2. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/pyproject.toml +1 -1
  3. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/scripts/pythia.py +60 -13
  4. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2/scripts/pythia_plsql.egg-info}/PKG-INFO +1 -1
  5. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/tests/test_install.py +23 -0
  6. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/tests/test_phase3.py +29 -0
  7. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/LICENSE +0 -0
  8. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/README.md +0 -0
  9. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/queries/compile-errors.sql +0 -0
  10. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/queries/dependencies.sql +0 -0
  11. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/queries/impact.sql +0 -0
  12. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/queries/invalid-objects.sql +0 -0
  13. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/queries/name-occupants.sql +0 -0
  14. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/queries/object-source.sql +0 -0
  15. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/queries/plscope-enabled.sql +0 -0
  16. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/queries/plscope-statements.sql +0 -0
  17. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/queries/plscope-usages.sql +0 -0
  18. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/queries/session-privileges.sql +0 -0
  19. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/queries/similar-candidates.sql +0 -0
  20. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/queries/source.sql +0 -0
  21. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/scripts/pythia_plsql.egg-info/SOURCES.txt +0 -0
  22. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/scripts/pythia_plsql.egg-info/dependency_links.txt +0 -0
  23. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/scripts/pythia_plsql.egg-info/entry_points.txt +0 -0
  24. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/scripts/pythia_plsql.egg-info/requires.txt +0 -0
  25. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/scripts/pythia_plsql.egg-info/top_level.txt +0 -0
  26. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/setup.cfg +0 -0
  27. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/skills/pythia-apply/SKILL.md +0 -0
  28. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/skills/pythia-explore/SKILL.md +0 -0
  29. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/skills/pythia-explore/reference/data-dictionary.md +0 -0
  30. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/skills/pythia-impact/SKILL.md +0 -0
  31. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/skills/pythia-review/SKILL.md +0 -0
  32. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/skills/pythia-review/reference/antipatterns.md +0 -0
  33. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/skills/pythia-setup/SKILL.md +0 -0
  34. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/skills/pythia-skill-author/SKILL.md +0 -0
  35. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/skills/pythia-write/SKILL.md +0 -0
  36. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/skills/pythia-write/reference/patterns.md +0 -0
  37. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/tests/test_phase1.py +0 -0
  38. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/tests/test_phase2.py +0 -0
  39. {pythia_plsql-0.3.0 → pythia_plsql-0.3.2}/tests/test_phase5.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythia-plsql
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/thaildhe172591/pythia
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythia-plsql"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -201,17 +201,38 @@ def invocation():
201
201
  return pathlib.Path(prog).name
202
202
 
203
203
 
204
+ HEADLESS_YES_MSG = (
205
+ "--yes is the developer's flag, and no terminal is attached to this "
206
+ "session.\nAgents preview, relay the diff and the impact verbatim, "
207
+ "STOP, and pass --confirm <token> only after the developer's explicit "
208
+ "approval in chat.\n(Real pipelines set PYTHIA_CI=1.)")
209
+
210
+
204
211
  def human_at_the_keyboard():
205
- """True only when a person is typing: stdin is a TTY. An agent driving
206
- the CLI through a subprocess has no TTY and cannot fake one — which is
207
- exactly why the loosening actions below key on this. PYTHIA_CI=1 is the
208
- documented escape for real pipelines."""
212
+ """True only when a person is typing at a real console.
213
+
214
+ isatty() alone is not enough on Windows: NUL is a character device, so
215
+ a subprocess launched with stdin=DEVNULL reports isatty() == True and
216
+ would sail straight through this gate. Only a real console answers
217
+ GetConsoleMode, so ask that too. PYTHIA_CI=1 is the documented escape
218
+ for real pipelines."""
209
219
  if os.environ.get("PYTHIA_CI"):
210
220
  return True
211
221
  try:
212
- return sys.stdin.isatty()
222
+ if not sys.stdin.isatty():
223
+ return False
213
224
  except (AttributeError, ValueError):
214
225
  return False
226
+ if os.name == "nt":
227
+ try:
228
+ import ctypes
229
+ handle = ctypes.windll.kernel32.GetStdHandle(-10) # STD_INPUT
230
+ mode = ctypes.c_ulong()
231
+ return bool(ctypes.windll.kernel32.GetConsoleMode(
232
+ handle, ctypes.byref(mode)))
233
+ except Exception: # noqa: BLE001 — any failure means "not a console"
234
+ return False
235
+ return True
215
236
 
216
237
 
217
238
  def forbid_write_flag(argv):
@@ -610,6 +631,12 @@ def load_settings(root):
610
631
  sys.exit(f"Cannot parse {path}: {e}")
611
632
 
612
633
 
634
+ def undo_group_action(ns):
635
+ """Undoing a CREATE is a DROP, which is `structural` — so the policy on
636
+ that group decides whether the restore command we print can run."""
637
+ return load_policy(ns.project_root)["structural"][0]
638
+
639
+
613
640
  def journal_root(root):
614
641
  return pathlib.Path(root) / CONFIG_DIR / "journal"
615
642
 
@@ -1124,11 +1151,7 @@ def run_apply(conn, schema, ns, file_text, origin=None):
1124
1151
  db_source = "".join(cell(r[0]) for r in rows)
1125
1152
  token = apply_token(otype, name, file_text, db_source)
1126
1153
  if ns.yes and not human_at_the_keyboard():
1127
- sys.exit("--yes is the developer's flag, and no terminal is attached "
1128
- "to this session.\nAgents preview, relay the diff and the "
1129
- "impact verbatim, STOP, and pass --confirm <token> only "
1130
- "after the developer's explicit approval in chat.\n"
1131
- "(Real pipelines set PYTHIA_CI=1.)")
1154
+ sys.exit(HEADLESS_YES_MSG)
1132
1155
  confirmed = bool(ns.yes) or ns.confirm == token
1133
1156
  if ns.confirm and ns.confirm != token:
1134
1157
  sys.exit("The confirmation token does not match: the file or the "
@@ -1234,6 +1257,9 @@ def run_apply(conn, schema, ns, file_text, origin=None):
1234
1257
  "type": otype, "errors": [list(r) for r in own_errors],
1235
1258
  "newly_invalid": [list(x) for x in broke],
1236
1259
  "restore": f"{invocation()} journal restore {entry}",
1260
+ "restore_is_drop": created,
1261
+ "restore_blocked_by_policy": (
1262
+ created and undo_group_action(ns) == "deny"),
1237
1263
  "exit": 0 if ok else 3}))
1238
1264
  else:
1239
1265
  en = getattr(ns, "color", False)
@@ -1254,9 +1280,21 @@ def run_apply(conn, schema, ns, file_text, origin=None):
1254
1280
  print(f" {len(broke)} objects were VALID before and are INVALID now:")
1255
1281
  for n2, t2 in broke:
1256
1282
  print(paint(f" {n2} ({t2})", "red", en))
1257
- undo = "dropping it (it did not exist before)" if created else None
1258
- print(f"\n To undo{' — note: undo means ' + undo if undo else ''}:")
1259
- print(" " + paint(f"{invocation()} journal restore {entry}", "cyan", en))
1283
+ if created:
1284
+ print("\n To undo — note: undo means DROPPING it "
1285
+ "(it did not exist before):")
1286
+ else:
1287
+ print("\n To undo:")
1288
+ print(" " + paint(f"{invocation()} journal restore {entry}",
1289
+ "cyan", en))
1290
+ if created and undo_group_action(ns) == "deny":
1291
+ # the honest half: that restore is a DROP, DROP is structural,
1292
+ # and structural is deny — so the line above would be refused
1293
+ print(paint(" ...but that restore is a DROP, and structural is "
1294
+ "set to deny, so it will be refused.", "yellow", en))
1295
+ print(paint(" The developer decides: "
1296
+ f"{invocation()} policy set structural confirm",
1297
+ "yellow", en))
1260
1298
  return 0 if ok else 3
1261
1299
 
1262
1300
 
@@ -1842,6 +1880,15 @@ def main(argv=None):
1842
1880
  except AttributeError:
1843
1881
  pass
1844
1882
  ns = build_parser().parse_args(argv)
1883
+ # refusals that need no database come first — an unreachable or locked
1884
+ # connection must never mask them
1885
+ if ns.command == "sql" and not is_readonly_sql(
1886
+ " ".join(ns.statement).strip().rstrip(";")):
1887
+ sys.exit("Only SELECT/WITH statements are allowed; sql is read-only."
1888
+ f"\nThe write path is `{invocation()} apply <file>` — "
1889
+ "snapshot, preview and verify included.")
1890
+ if getattr(ns, "yes", False) and not human_at_the_keyboard():
1891
+ sys.exit(HEADLESS_YES_MSG)
1845
1892
  ns.color = color_enabled() and not ns.json
1846
1893
  if ns.color and os.name == "nt":
1847
1894
  # Constant empty string, never user input — safe from injection. This
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythia-plsql
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/thaildhe172591/pythia
@@ -122,6 +122,29 @@ def test_agent_user_json_saves_the_same_password_it_prints():
122
122
  os.chdir(cwd)
123
123
 
124
124
 
125
+ def test_devnull_stdin_is_not_a_human():
126
+ """The gate must hold for a real subprocess, not just in theory. On
127
+ Windows NUL is a character device and isatty() says True for it, so a
128
+ stdin=DEVNULL child would otherwise self-approve writes."""
129
+ import os
130
+ import subprocess
131
+ env = {k: v for k, v in os.environ.items() if k != "PYTHIA_CI"}
132
+ with tempfile.TemporaryDirectory() as td:
133
+ (pathlib.Path(td) / ".pythia").mkdir()
134
+ (pathlib.Path(td) / ".pythia" / "connections.json").write_text(
135
+ json.dumps({"dev": {"host": "h", "user": "U", "password": "p",
136
+ "schema": "U"}}), encoding="utf-8")
137
+ env["PYTHONPATH"] = str(ROOT / "scripts") # -m pythia from the repo
138
+ r = subprocess.run(
139
+ [sys.executable, "-m", "pythia", "policy", "set", "structural",
140
+ "allow"],
141
+ cwd=td, env=env, stdin=subprocess.DEVNULL, text=True,
142
+ capture_output=True)
143
+ assert r.returncode != 0, r.stdout + r.stderr
144
+ assert "developer" in (r.stdout + r.stderr).lower()
145
+ assert not (pathlib.Path(td) / ".pythia" / "policy.json").is_file()
146
+
147
+
125
148
  def test_one_version_across_every_manifest():
126
149
  """pyproject, plugin.json, marketplace.json and npm/package.json must all
127
150
  carry the same version — the release workflow publishes from one tag."""
@@ -658,6 +658,35 @@ def test_headless_policy_loosening_is_refused_tightening_allowed():
658
658
  assert pythia.load_policy(td)["plsql_source"][0] == "deny"
659
659
 
660
660
 
661
+ def test_report_admits_a_created_objects_undo_is_blocked():
662
+ """pythia exists so nothing lies about rollback. Undoing a CREATE is a
663
+ DROP, DROP is structural, and structural is deny by default — so the
664
+ printed restore command would be refused. Say so, in the same breath."""
665
+ import contextlib
666
+ import io
667
+ with tempfile.TemporaryDirectory() as td:
668
+ conn = FakeConn(base_script(db_source="")) # object did not exist
669
+ buf = io.StringIO()
670
+ with contextlib.redirect_stdout(buf):
671
+ pythia.run_apply(conn, "APP", apply_ns(td, file="f.sql", yes=True),
672
+ NEW_FILE)
673
+ out = buf.getvalue().lower()
674
+ assert "dropping it" in out
675
+ assert "will be refused" in out, out[-400:]
676
+ assert "policy set structural confirm" in out
677
+ # and once the developer has loosened it, the warning goes away
678
+ with tempfile.TemporaryDirectory() as td:
679
+ pathlib.Path(td, ".pythia").mkdir()
680
+ pathlib.Path(td, ".pythia", "policy.json").write_text(
681
+ '{"structural": "confirm"}', encoding="utf-8")
682
+ conn = FakeConn(base_script(db_source=""))
683
+ buf = io.StringIO()
684
+ with contextlib.redirect_stdout(buf):
685
+ pythia.run_apply(conn, "APP", apply_ns(td, file="f.sql", yes=True),
686
+ NEW_FILE)
687
+ assert "will be refused" not in buf.getvalue().lower()
688
+
689
+
661
690
  def main():
662
691
  failed = 0
663
692
  for name, fn in sorted(globals().items()):
File without changes
File without changes
File without changes