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.
- {pythia_plsql-0.4.7/scripts/pythia_plsql.egg-info → pythia_plsql-0.4.9}/PKG-INFO +1 -1
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/pyproject.toml +1 -1
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/scripts/pythia.py +92 -8
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9/scripts/pythia_plsql.egg-info}/PKG-INFO +1 -1
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/tests/test_install.py +30 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/tests/test_phase1.py +26 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/LICENSE +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/README.md +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/compile-errors.sql +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/dependencies.sql +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/impact.sql +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/invalid-objects.sql +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/name-occupants.sql +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/object-source.sql +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/plscope-enabled.sql +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/plscope-statements.sql +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/plscope-usages.sql +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/session-privileges.sql +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/similar-candidates.sql +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/queries/source.sql +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/scripts/pythia_plsql.egg-info/SOURCES.txt +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/scripts/pythia_plsql.egg-info/dependency_links.txt +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/scripts/pythia_plsql.egg-info/entry_points.txt +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/scripts/pythia_plsql.egg-info/requires.txt +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/scripts/pythia_plsql.egg-info/top_level.txt +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/setup.cfg +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-apply/SKILL.md +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-explore/SKILL.md +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-explore/reference/data-dictionary.md +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-impact/SKILL.md +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-review/SKILL.md +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-review/reference/antipatterns.md +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-setup/SKILL.md +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-skill-author/SKILL.md +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-write/SKILL.md +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-write/reference/patterns.md +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/tests/test_phase2.py +0 -0
- {pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/tests/test_phase3.py +0 -0
- {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.
|
|
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
|
+
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
|
|
925
|
-
"""
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/scripts/pythia_plsql.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythia_plsql-0.4.7 → pythia_plsql-0.4.9}/skills/pythia-explore/reference/data-dictionary.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|