pythia-plsql 0.4.6__tar.gz → 0.4.8__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.6/scripts/pythia_plsql.egg-info → pythia_plsql-0.4.8}/PKG-INFO +1 -1
  2. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/pyproject.toml +1 -1
  3. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/scripts/pythia.py +139 -6
  4. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8/scripts/pythia_plsql.egg-info}/PKG-INFO +1 -1
  5. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/tests/test_install.py +92 -2
  6. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/LICENSE +0 -0
  7. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/README.md +0 -0
  8. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/queries/compile-errors.sql +0 -0
  9. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/queries/dependencies.sql +0 -0
  10. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/queries/impact.sql +0 -0
  11. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/queries/invalid-objects.sql +0 -0
  12. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/queries/name-occupants.sql +0 -0
  13. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/queries/object-source.sql +0 -0
  14. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/queries/plscope-enabled.sql +0 -0
  15. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/queries/plscope-statements.sql +0 -0
  16. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/queries/plscope-usages.sql +0 -0
  17. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/queries/session-privileges.sql +0 -0
  18. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/queries/similar-candidates.sql +0 -0
  19. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/queries/source.sql +0 -0
  20. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/scripts/pythia_plsql.egg-info/SOURCES.txt +0 -0
  21. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/scripts/pythia_plsql.egg-info/dependency_links.txt +0 -0
  22. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/scripts/pythia_plsql.egg-info/entry_points.txt +0 -0
  23. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/scripts/pythia_plsql.egg-info/requires.txt +0 -0
  24. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/scripts/pythia_plsql.egg-info/top_level.txt +0 -0
  25. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/setup.cfg +0 -0
  26. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/skills/pythia-apply/SKILL.md +0 -0
  27. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/skills/pythia-explore/SKILL.md +0 -0
  28. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/skills/pythia-explore/reference/data-dictionary.md +0 -0
  29. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/skills/pythia-impact/SKILL.md +0 -0
  30. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/skills/pythia-review/SKILL.md +0 -0
  31. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/skills/pythia-review/reference/antipatterns.md +0 -0
  32. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/skills/pythia-setup/SKILL.md +0 -0
  33. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/skills/pythia-skill-author/SKILL.md +0 -0
  34. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/skills/pythia-write/SKILL.md +0 -0
  35. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/skills/pythia-write/reference/patterns.md +0 -0
  36. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/tests/test_phase1.py +0 -0
  37. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/tests/test_phase2.py +0 -0
  38. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/tests/test_phase3.py +0 -0
  39. {pythia_plsql-0.4.6 → pythia_plsql-0.4.8}/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.6
3
+ Version: 0.4.8
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.6"
7
+ version = "0.4.8"
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" }
@@ -185,6 +185,114 @@ def paint_diff_line(ln, enabled):
185
185
  return ln
186
186
 
187
187
 
188
+ NL = chr(10) # a literal escape does not survive every editing path
189
+
190
+
191
+ def offer_path_fix(scripts_dir, interactive, forced=False):
192
+ """Put pythia on PATH for the developer instead of handing them a command
193
+ to run. Only ever touches the user PATH, and only with a yes."""
194
+ if not scripts_dir or os.name != "nt":
195
+ return False
196
+ if not forced:
197
+ if not interactive:
198
+ return False
199
+ try:
200
+ answer = input(NL + "Add it to your PATH now? [Y/n] ").strip().lower()
201
+ except (EOFError, KeyboardInterrupt):
202
+ print()
203
+ return False
204
+ if answer not in ("", "y", "yes"):
205
+ return False
206
+ try:
207
+ changed = add_to_user_path(scripts_dir)
208
+ except OSError as e:
209
+ print(f"Could not update your PATH: {e}")
210
+ return False
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)
215
+ print(" Open a NEW terminal for it to take effect — an already-running")
216
+ print(" one keeps the environment it started with, and so do its tabs.")
217
+ print(f" Until then: {pathlib.Path(sys.executable).stem} -m pythia <command>")
218
+ return True
219
+
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
+
249
+ def path_contains(path_value, directory):
250
+ """Is this directory already listed in a PATH string? Compared the way
251
+ the platform does: case-insensitively on Windows, ignoring trailing
252
+ separators and empty entries."""
253
+ want = os.path.normcase(os.path.normpath(str(directory)))
254
+ return any(os.path.normcase(os.path.normpath(p)) == want
255
+ for p in str(path_value).split(os.pathsep) if p.strip())
256
+
257
+
258
+ def _read_user_path():
259
+ import winreg
260
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment") as k:
261
+ try:
262
+ return winreg.QueryValueEx(k, "Path")[0]
263
+ except FileNotFoundError:
264
+ return ""
265
+
266
+
267
+ def _write_user_path(value):
268
+ import winreg
269
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment", 0,
270
+ winreg.KEY_SET_VALUE) as k:
271
+ winreg.SetValueEx(k, "Path", 0, winreg.REG_EXPAND_SZ, value)
272
+
273
+
274
+ def add_to_user_path(directory, read=None, write=None):
275
+ """Append a directory to the *user* PATH, returning whether anything
276
+ changed.
277
+
278
+ Reads the stored user value, never `os.environ["PATH"]`. The process
279
+ environment is the system and user values merged, so writing it back into
280
+ user scope copies every system entry into the user's -- doubling the
281
+ effective PATH and leaving a stale snapshot that shadows the real system
282
+ one after it next changes. That mistake is easy to make and outlives the
283
+ install, which is why this function exists rather than a one-line command.
284
+ """
285
+ read = read or _read_user_path
286
+ write = write or _write_user_path
287
+ current = read() or ""
288
+ if path_contains(current, directory):
289
+ return False
290
+ joined = str(directory) if not current.strip() else \
291
+ current.rstrip(os.pathsep) + os.pathsep + str(directory)
292
+ write(joined)
293
+ return True
294
+
295
+
188
296
  def installed_scripts_dir():
189
297
  """The directory pip put our executable in. The default scheme is wrong
190
298
  for `pip install --user` — the common case on Windows — so the candidate
@@ -203,7 +311,7 @@ def installed_scripts_dir():
203
311
  return candidates[-1] if candidates else None
204
312
 
205
313
 
206
- def entry_point_hint(found, scripts_dir):
314
+ def entry_point_hint(found, scripts_dir, in_stored_path=False):
207
315
  """What to say when `pythia` will not resolve as a command. pip installs
208
316
  the executable into a scripts directory that is often not on PATH — the
209
317
  default for `pip install --user` on Windows — and the shell's
@@ -211,13 +319,32 @@ def entry_point_hint(found, scripts_dir):
211
319
  step immediately after the install the docs tell you to run."""
212
320
  if found:
213
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
+ ]))
214
337
  out = ["`pythia` is installed but not on your PATH."]
215
338
  if scripts_dir:
216
339
  out.append(" It lives in: " + str(scripts_dir))
217
340
  if os.name == "nt":
218
- out.append(" Add it once, in PowerShell, then reopen the terminal:")
219
- out.append(" [Environment]::SetEnvironmentVariable('PATH',")
220
- out.append(" \"$env:PATH;" + str(scripts_dir) + "\", 'User')")
341
+ # Deliberately not a PowerShell one-liner over $env:PATH:
342
+ # that variable is system and user merged, so writing it
343
+ # back into user scope copies every system entry into the
344
+ # user's. Let the tool do it correctly instead.
345
+ out.append(" Put it there with: "
346
+ + pathlib.Path(sys.executable).stem
347
+ + " -m pythia install --add-to-path")
221
348
  else:
222
349
  out.append(" Add it to your shell profile:")
223
350
  out.append(" export PATH=\"$PATH:" + str(scripts_dir) + "\"")
@@ -1974,11 +2101,14 @@ def cmd_install(conn, schema, ns):
1974
2101
  clean_legacy_skills(ns.project_root)
1975
2102
  print(f"\nNext: fill in {path}")
1976
2103
  print(f"Then: {invocation()} check")
1977
- hint = entry_point_hint(shutil.which("pythia"),
1978
- installed_scripts_dir())
2104
+ scripts_dir = installed_scripts_dir()
2105
+ hint = entry_point_hint(shutil.which("pythia"), scripts_dir,
2106
+ in_stored_path=stored_path_has(scripts_dir))
1979
2107
  if hint:
1980
2108
  print()
1981
2109
  print(hint)
2110
+ offer_path_fix(scripts_dir, interactive,
2111
+ getattr(ns, "add_to_path", False))
1982
2112
 
1983
2113
 
1984
2114
  COMMANDS = {"check": cmd_check, "ls": cmd_ls, "src": cmd_src, "args": cmd_args,
@@ -2112,6 +2242,9 @@ def build_parser():
2112
2242
  s.add_argument("-g", "--global", dest="glob", action="store_true",
2113
2243
  help="install the skill pack machine-wide (serves every "
2114
2244
  "project; per-project installs then skip skills)")
2245
+ s.add_argument("--add-to-path", action="store_true", dest="add_to_path",
2246
+ help="put the pythia command on your PATH without asking "
2247
+ "(Windows; user PATH only)")
2115
2248
  s.add_argument("--source", default=DEFAULT_SKILLS_SOURCE,
2116
2249
  help="skills repo for `npx skills add` "
2117
2250
  f"(default {DEFAULT_SKILLS_SOURCE})")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythia-plsql
3
- Version: 0.4.6
3
+ Version: 0.4.8
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
@@ -5,6 +5,7 @@ no network, no npx needed.
5
5
  Run: python tests/test_install.py
6
6
  """
7
7
  import json
8
+ import os
8
9
  import pathlib
9
10
  import sys
10
11
  import tempfile
@@ -187,9 +188,9 @@ def test_entry_point_hint_names_the_directory_to_add():
187
188
  """pip install --user on Windows puts pythia.exe in a Scripts directory
188
189
  that is not on PATH, so the very next documented command fails with
189
190
  CommandNotFound -- naming nothing that helps. Seen on a real machine."""
190
- hint = pythia.entry_point_hint(found=None, scripts_dir=r"C:\Py\Scripts")
191
+ hint = pythia.entry_point_hint(found=None, scripts_dir=SCRIPTS)
191
192
  assert hint is not None
192
- assert r"C:\Py\Scripts" in hint # the exact directory to add
193
+ assert SCRIPTS in hint # the exact directory to add
193
194
  assert "-m pythia" in hint # the fallback that works now
194
195
  # once the command resolves, say nothing
195
196
  assert pythia.entry_point_hint(found="/usr/local/bin/pythia",
@@ -235,6 +236,95 @@ def test_cmd_install_runs_end_to_end():
235
236
  assert env is not None
236
237
 
237
238
 
239
+ # Native paths: on POSIX ':' is the PATH separator and would cut a
240
+ # Windows path in half, so these are built from os.sep.
241
+ SCRIPTS = os.path.join(os.sep + 'opt', 'a', 'Scripts')
242
+ USERBIN = os.path.join(os.sep + 'opt', 'a', 'bin')
243
+ SYS1 = os.sep + 'windows'
244
+ SYS2 = os.path.join(os.sep + 'windows', 'system32')
245
+
246
+
247
+ def test_path_contains_normalises_like_the_platform_does():
248
+ """Paths must be native: on POSIX the separator is ':', which cuts a
249
+ Windows path in half, and normcase does not fold case."""
250
+ import os
251
+ here = os.path.join(os.sep + "opt", "a", "Scripts")
252
+ other = os.path.join(os.sep + "opt", "a", "Other")
253
+ p = os.pathsep.join([os.sep + "usr", here, ""]) # trailing empty entry
254
+ assert pythia.path_contains(p, here)
255
+ assert pythia.path_contains(p, here + os.sep) # trailing separator
256
+ assert not pythia.path_contains(p, other)
257
+ assert not pythia.path_contains("", here)
258
+ if os.name == "nt": # case folds on Windows only
259
+ assert pythia.path_contains(p, here.upper())
260
+
261
+
262
+ def test_add_to_user_path_reads_the_stored_value_not_the_process_env():
263
+ """The process PATH is system+user merged. Writing that back into user
264
+ scope copies every system entry into the user's -- a mess that outlives
265
+ the install. This is the mistake this function exists to not repeat."""
266
+ import os
267
+ written = []
268
+ stored = USERBIN # what the registry holds: user only
269
+ os.environ["PATH"] = os.pathsep.join( # what the process sees: much more
270
+ [SYS1, SYS2, stored])
271
+ changed = pythia.add_to_user_path(SCRIPTS,
272
+ read=lambda: stored,
273
+ write=written.append)
274
+ assert changed is True
275
+ assert written == [stored + os.pathsep + SCRIPTS]
276
+ assert SYS1 not in written[0] # no system entries leaked in
277
+
278
+
279
+ def test_add_to_user_path_is_idempotent():
280
+ written = []
281
+ changed = pythia.add_to_user_path(SCRIPTS,
282
+ read=lambda: SCRIPTS,
283
+ write=written.append)
284
+ assert changed is False and written == []
285
+
286
+
287
+ def test_add_to_user_path_handles_an_empty_and_untidy_value():
288
+ import os
289
+ written = []
290
+ pythia.add_to_user_path(SCRIPTS, read=lambda: "", write=written.append)
291
+ assert written == [SCRIPTS]
292
+ written.clear()
293
+ pythia.add_to_user_path(SCRIPTS, read=lambda: USERBIN + os.pathsep,
294
+ write=written.append)
295
+ assert written == [USERBIN + os.pathsep + SCRIPTS] # no doubled separator
296
+
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
+
238
328
  def main():
239
329
  failed = 0
240
330
  for name, fn in sorted(globals().items()):
File without changes
File without changes
File without changes