pythia-plsql 0.4.7__tar.gz → 0.4.9__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.4.7/scripts/pythia_plsql.egg-info → pythia_plsql-0.4.9}/PKG-INFO +1 -1
  2. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/pyproject.toml +1 -1
  3. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/scripts/pythia.py +92 -8
  4. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9/scripts/pythia_plsql.egg-info}/PKG-INFO +1 -1
  5. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/tests/test_install.py +30 -0
  6. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/tests/test_phase1.py +26 -0
  7. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/LICENSE +0 -0
  8. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/README.md +0 -0
  9. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/compile-errors.sql +0 -0
  10. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/dependencies.sql +0 -0
  11. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/impact.sql +0 -0
  12. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/invalid-objects.sql +0 -0
  13. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/name-occupants.sql +0 -0
  14. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/object-source.sql +0 -0
  15. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/plscope-enabled.sql +0 -0
  16. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/plscope-statements.sql +0 -0
  17. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/plscope-usages.sql +0 -0
  18. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/session-privileges.sql +0 -0
  19. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/similar-candidates.sql +0 -0
  20. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/source.sql +0 -0
  21. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/scripts/pythia_plsql.egg-info/SOURCES.txt +0 -0
  22. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/scripts/pythia_plsql.egg-info/dependency_links.txt +0 -0
  23. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/scripts/pythia_plsql.egg-info/entry_points.txt +0 -0
  24. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/scripts/pythia_plsql.egg-info/requires.txt +0 -0
  25. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/scripts/pythia_plsql.egg-info/top_level.txt +0 -0
  26. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/setup.cfg +0 -0
  27. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-apply/SKILL.md +0 -0
  28. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-explore/SKILL.md +0 -0
  29. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-explore/reference/data-dictionary.md +0 -0
  30. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-impact/SKILL.md +0 -0
  31. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-review/SKILL.md +0 -0
  32. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-review/reference/antipatterns.md +0 -0
  33. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-setup/SKILL.md +0 -0
  34. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-skill-author/SKILL.md +0 -0
  35. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-write/SKILL.md +0 -0
  36. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-write/reference/patterns.md +0 -0
  37. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/tests/test_phase2.py +0 -0
  38. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/tests/test_phase3.py +0 -0
  39. {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/tests/test_phase5.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythia-plsql
3
- Version: 0.4.7
3
+ Version: 0.4.9
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.4.7"
7
+ version = "0.4.9"
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" }
@@ -209,12 +209,43 @@ def offer_path_fix(scripts_dir, interactive, forced=False):
209
209
  print(f"Could not update your PATH: {e}")
210
210
  return False
211
211
  print(NL + f" {'Added' if changed else 'Already in'} your user PATH: {scripts_dir}")
212
+ warn = path_length_warning(os.environ.get("PATH", ""))
213
+ if warn:
214
+ print(" " + warn)
212
215
  print(" Open a NEW terminal for it to take effect — an already-running")
213
216
  print(" one keeps the environment it started with, and so do its tabs.")
214
217
  print(f" Until then: {pathlib.Path(sys.executable).stem} -m pythia <command>")
215
218
  return True
216
219
 
217
220
 
221
+ def path_length_warning(combined):
222
+ """Windows tooling still truncates PATH around 2047 characters, and a
223
+ freshly appended entry sits last -- so it is the first thing lost, and it
224
+ is lost silently. The usual cause is a PATH that had the system entries
225
+ copied into the user's."""
226
+ n = len(combined or "")
227
+ if n < 1900:
228
+ return None
229
+ return (f"Your PATH is {n} characters; Windows truncates near 2047, and the "
230
+ "entry just added is last in line.\n"
231
+ " The usual cause is duplicate entries — the system PATH copied "
232
+ "into the user PATH by\n"
233
+ " a `$env:PATH` one-liner. Compare the two scopes and remove what "
234
+ "appears in both.")
235
+
236
+
237
+ def stored_path_has(directory):
238
+ """Is the directory in the PATH as *stored*, rather than the one this
239
+ process inherited? The two differ for every terminal opened before the
240
+ last change, which is the common confusion."""
241
+ if not directory or os.name != "nt":
242
+ return False
243
+ try:
244
+ return path_contains(_read_user_path(), directory)
245
+ except OSError:
246
+ return False
247
+
248
+
218
249
  def path_contains(path_value, directory):
219
250
  """Is this directory already listed in a PATH string? Compared the way
220
251
  the platform does: case-insensitively on Windows, ignoring trailing
@@ -280,7 +311,7 @@ def installed_scripts_dir():
280
311
  return candidates[-1] if candidates else None
281
312
 
282
313
 
283
- def entry_point_hint(found, scripts_dir):
314
+ def entry_point_hint(found, scripts_dir, in_stored_path=False):
284
315
  """What to say when `pythia` will not resolve as a command. pip installs
285
316
  the executable into a scripts directory that is often not on PATH — the
286
317
  default for `pip install --user` on Windows — and the shell's
@@ -288,6 +319,21 @@ def entry_point_hint(found, scripts_dir):
288
319
  step immediately after the install the docs tell you to run."""
289
320
  if found:
290
321
  return None
322
+ if in_stored_path:
323
+ # The directory is in the stored PATH but not in this process: the
324
+ # terminal started before the change. Sending someone to re-run the
325
+ # install here wastes their time -- it already worked.
326
+ return (NL.join([
327
+ "`pythia` is already on your PATH — this terminal just started "
328
+ "before that was true.",
329
+ " Open a NEW terminal WINDOW. A new tab is not enough: tabs "
330
+ "inherit the",
331
+ " environment of the window that spawned them, so if you use "
332
+ "Windows Terminal,",
333
+ " VS Code or Cursor, restart the application itself.",
334
+ " In this one, keep using: "
335
+ + pathlib.Path(sys.executable).stem + " -m pythia <command>",
336
+ ]))
291
337
  out = ["`pythia` is installed but not on your PATH."]
292
338
  if scripts_dir:
293
339
  out.append(" It lives in: " + str(scripts_dir))
@@ -921,10 +967,46 @@ def json_envelope(command, connection, schema, cols, rows, truncated, **extra):
921
967
 
922
968
  # --- database access ---------------------------------------------------------
923
969
 
924
- def connect_failure_message(exc, conn_name):
925
- """A failure to connect should say which entry failed and what to check —
926
- a driver stack trace tells the reader nothing actionable."""
927
- return (f"Could not connect using connection {conn_name!r}: {exc}\n"
970
+ def authenticating_account(user):
971
+ """The Oracle account that actually authenticates. With proxy
972
+ authentication the connect string is `agent[owner]` and it is the agent
973
+ whose password is checked, whose account locks, and whose name a DBA
974
+ needs — not the schema in front of you."""
975
+ if not user:
976
+ return None
977
+ return str(user).split("[", 1)[0].strip().upper()
978
+
979
+
980
+ def connect_failure_message(exc, conn_name, user=None):
981
+ """A failure to connect should say which entry failed and what to do. For
982
+ the three errors Oracle has already diagnosed precisely, generic advice
983
+ wastes the reader's time, so name the actual next step instead."""
984
+ text = str(exc)
985
+ account = authenticating_account(user)
986
+ who = account or "<the connecting user>"
987
+ head = f"Could not connect using connection {conn_name!r}: {text}"
988
+ if "ORA-28000" in text:
989
+ return (f"{head}\n"
990
+ f"The account {who} is locked. A DBA or superuser unlocks it:\n"
991
+ f" ALTER USER {who} ACCOUNT UNLOCK;\n"
992
+ "Then find out why, or it locks again — a run of wrong "
993
+ "passwords trips\nFAILED_LOGIN_ATTEMPTS:\n"
994
+ f" SELECT account_status, lock_date, profile FROM dba_users "
995
+ f"WHERE username = '{who}';")
996
+ if "ORA-28001" in text or "ORA-28002" in text:
997
+ return (f"{head}\n"
998
+ f"The password for {who} has expired. A DBA sets a new one:\n"
999
+ f" ALTER USER {who} IDENTIFIED BY \"<new password>\";\n"
1000
+ "Update it in connections.json in the same breath. To stop the "
1001
+ "clock for a\nservice account, put it on a profile with "
1002
+ "PASSWORD_LIFE_TIME UNLIMITED.")
1003
+ if "ORA-01017" in text:
1004
+ return (f"{head}\n"
1005
+ f"Wrong username or password for {who}. Check the entry in "
1006
+ "connections.json —\nand check it before retrying: repeated "
1007
+ "attempts trip FAILED_LOGIN_ATTEMPTS and\nlock the account, "
1008
+ "which needs a DBA to undo.")
1009
+ return (f"{head}\n"
928
1010
  "Check host/port/service_name, the credentials, and that the database "
929
1011
  "is reachable from here. Use --conn NAME to try a different entry.")
930
1012
 
@@ -2055,11 +2137,13 @@ def cmd_install(conn, schema, ns):
2055
2137
  clean_legacy_skills(ns.project_root)
2056
2138
  print(f"\nNext: fill in {path}")
2057
2139
  print(f"Then: {invocation()} check")
2058
- hint = entry_point_hint(shutil.which("pythia"), installed_scripts_dir())
2140
+ scripts_dir = installed_scripts_dir()
2141
+ hint = entry_point_hint(shutil.which("pythia"), scripts_dir,
2142
+ in_stored_path=stored_path_has(scripts_dir))
2059
2143
  if hint:
2060
2144
  print()
2061
2145
  print(hint)
2062
- offer_path_fix(installed_scripts_dir(), interactive,
2146
+ offer_path_fix(scripts_dir, interactive,
2063
2147
  getattr(ns, "add_to_path", False))
2064
2148
 
2065
2149
 
@@ -2250,7 +2334,7 @@ def main(argv=None):
2250
2334
  except (oracledb.Error, OSError) as e:
2251
2335
  # OSError too: a DNS or socket failure arrives raw from the socket layer,
2252
2336
  # not wrapped as an oracledb.Error.
2253
- sys.exit(connect_failure_message(e, name))
2337
+ sys.exit(connect_failure_message(e, name, c.get("user")))
2254
2338
  try:
2255
2339
  if ns.command == "journal": # only restore reaches here
2256
2340
  if not ns.id:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythia-plsql
3
- Version: 0.4.7
3
+ Version: 0.4.9
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
@@ -295,6 +295,36 @@ def test_add_to_user_path_handles_an_empty_and_untidy_value():
295
295
  assert written == [USERBIN + os.pathsep + SCRIPTS] # no doubled separator
296
296
 
297
297
 
298
+ def test_hint_tells_a_stale_shell_apart_from_an_unconfigured_one():
299
+ """The directory being in the stored PATH while the running process
300
+ cannot see it means one thing only: this terminal started earlier. Saying
301
+ "not on your PATH" there sends people to re-run an install that already
302
+ worked -- which is exactly what happened in the field."""
303
+ stale = pythia.entry_point_hint(found=None, scripts_dir=SCRIPTS,
304
+ in_stored_path=True)
305
+ assert "already" in stale.lower()
306
+ assert "new" in stale.lower() and "tab" in stale.lower() # window, not tab
307
+ assert "--add-to-path" not in stale # nothing to redo
308
+ unconfigured = pythia.entry_point_hint(found=None, scripts_dir=SCRIPTS,
309
+ in_stored_path=False)
310
+ assert SCRIPTS in unconfigured and "-m pythia" in unconfigured
311
+ # how you fix it is platform-specific: pythia can edit the registry on
312
+ # Windows, while a POSIX shell profile is the developer's to edit
313
+ assert ("--add-to-path" if os.name == "nt" else "profile") in unconfigured
314
+ # and once the command resolves, still silence
315
+ assert pythia.entry_point_hint(found="/x/pythia", scripts_dir=SCRIPTS,
316
+ in_stored_path=True) is None
317
+
318
+
319
+ def test_long_path_is_flagged_before_it_silently_truncates():
320
+ """Windows tooling still truncates PATH near 2047 characters, and the
321
+ entry we just appended is the last one -- the first to be lost."""
322
+ assert pythia.path_length_warning("x" * 1000) is None
323
+ w = pythia.path_length_warning("x" * 2040)
324
+ assert w is not None and "2047" in w
325
+ assert "duplicate" in w.lower() # names the usual cause
326
+
327
+
298
328
  def main():
299
329
  failed = 0
300
330
  for name, fn in sorted(globals().items()):
@@ -301,6 +301,32 @@ def test_json_envelope():
301
301
  assert d["rows"] == [{"A": 1, "B": "2026-01-02"}]
302
302
 
303
303
 
304
+ def test_connect_failure_names_the_fix_for_common_oracle_errors():
305
+ """Generic advice is useless for an error Oracle already diagnosed. The
306
+ three that actually strand people each have one specific next step."""
307
+ locked = pythia.connect_failure_message(
308
+ Exception("ORA-28000: The account is locked."), "dev", user="agent[owner]")
309
+ assert "ACCOUNT UNLOCK" in locked and "AGENT" in locked # the real account
310
+ assert "DBA" in locked or "superuser" in locked.lower()
311
+
312
+ expired = pythia.connect_failure_message(
313
+ Exception("ORA-28001: the password has expired"), "dev", user="agent")
314
+ assert "PASSWORD EXPIRE" in expired or "new password" in expired.lower()
315
+
316
+ wrong = pythia.connect_failure_message(
317
+ Exception("ORA-01017: invalid username/password"), "dev", user="agent")
318
+ assert "lock the account" in wrong.lower() # warn before they retry into one
319
+
320
+ plain = pythia.connect_failure_message(OSError("getaddrinfo failed"), "dev")
321
+ assert "ORA-" not in plain and "dev" in plain # unchanged for the rest
322
+
323
+
324
+ def test_oracle_account_taken_from_a_proxy_connect_string():
325
+ assert pythia.authenticating_account("agent[owner]") == "AGENT"
326
+ assert pythia.authenticating_account("plain_user") == "PLAIN_USER"
327
+ assert pythia.authenticating_account(None) is None
328
+
329
+
304
330
  def main():
305
331
  failed = 0
306
332
  for name, fn in sorted(globals().items()):
File without changes
File without changes
File without changes