aiuse 3.3.1__tar.gz → 3.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 (100) hide show
  1. {aiuse-3.3.1/src/aiuse.egg-info → aiuse-3.3.2}/PKG-INFO +1 -1
  2. {aiuse-3.3.1 → aiuse-3.3.2}/pyproject.toml +1 -1
  3. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/__init__.py +1 -1
  4. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/tty.py +27 -2
  5. {aiuse-3.3.1 → aiuse-3.3.2/src/aiuse.egg-info}/PKG-INFO +1 -1
  6. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_tty.py +36 -0
  7. {aiuse-3.3.1 → aiuse-3.3.2}/LICENSE +0 -0
  8. {aiuse-3.3.1 → aiuse-3.3.2}/README.md +0 -0
  9. {aiuse-3.3.1 → aiuse-3.3.2}/setup.cfg +0 -0
  10. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/__main__.py +0 -0
  11. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/agent_notes.py +0 -0
  12. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/ai_stub.py +0 -0
  13. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/analysis/__init__.py +0 -0
  14. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/analysis/history.py +0 -0
  15. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/analysis/local_runtimes.py +0 -0
  16. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/analysis/pace.py +0 -0
  17. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/analysis/selfdescribe.py +0 -0
  18. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/analysis/suggest.py +0 -0
  19. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/analysis/use_or_lose.py +0 -0
  20. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/attribute.py +0 -0
  21. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/chat_format.py +0 -0
  22. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/cli.py +0 -0
  23. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/__init__.py +0 -0
  24. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/bailian.py +0 -0
  25. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/base.py +0 -0
  26. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/caut.py +0 -0
  27. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/clinepass.py +0 -0
  28. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/codexbar.py +0 -0
  29. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/cswap.py +0 -0
  30. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/grok.py +0 -0
  31. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/hermes.py +0 -0
  32. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/muse.py +0 -0
  33. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/opencode_go.py +0 -0
  34. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/opencode_zen.py +0 -0
  35. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/openrouter.py +0 -0
  36. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/openusage.py +0 -0
  37. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/openusage_sh.py +0 -0
  38. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/qwencloud.py +0 -0
  39. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/runner.py +0 -0
  40. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/throttle.py +0 -0
  41. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/collectors/tokscale.py +0 -0
  42. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/config.py +0 -0
  43. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/credentials.py +0 -0
  44. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/ledger.py +0 -0
  45. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/macos_trust.py +0 -0
  46. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/models.py +0 -0
  47. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/report.py +0 -0
  48. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/sampler.py +0 -0
  49. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/secretspec.py +0 -0
  50. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/serve.py +0 -0
  51. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/tui/__init__.py +0 -0
  52. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/tui/app.py +0 -0
  53. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/tui/builders.py +0 -0
  54. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse/watch.py +0 -0
  55. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse.egg-info/SOURCES.txt +0 -0
  56. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse.egg-info/dependency_links.txt +0 -0
  57. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse.egg-info/entry_points.txt +0 -0
  58. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse.egg-info/requires.txt +0 -0
  59. {aiuse-3.3.1 → aiuse-3.3.2}/src/aiuse.egg-info/top_level.txt +0 -0
  60. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_attribution.py +0 -0
  61. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_bailian.py +0 -0
  62. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_base_run_json.py +0 -0
  63. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_caut_parse.py +0 -0
  64. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_chat_format.py +0 -0
  65. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_claude_sublimits.py +0 -0
  66. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_cli.py +0 -0
  67. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_clinepass.py +0 -0
  68. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_codexbar_parse.py +0 -0
  69. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_config.py +0 -0
  70. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_credentials.py +0 -0
  71. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_cswap_parse.py +0 -0
  72. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_grok.py +0 -0
  73. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_hermes_parse.py +0 -0
  74. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_history.py +0 -0
  75. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_install_deps.py +0 -0
  76. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_local_runtimes.py +0 -0
  77. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_macos_trust.py +0 -0
  78. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_models_cli_binary.py +0 -0
  79. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_models_pace.py +0 -0
  80. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_muse.py +0 -0
  81. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_opencode_go.py +0 -0
  82. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_opencode_zen.py +0 -0
  83. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_openrouter.py +0 -0
  84. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_openusage_cli_resolution.py +0 -0
  85. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_openusage_parse.py +0 -0
  86. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_pace.py +0 -0
  87. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_query_throttle.py +0 -0
  88. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_qwencloud.py +0 -0
  89. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_release_script.py +0 -0
  90. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_report.py +0 -0
  91. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_runner_consolidation.py +0 -0
  92. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_selfdescribe.py +0 -0
  93. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_serve.py +0 -0
  94. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_shared_quota_semantics.py +0 -0
  95. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_snapshots_persistence.py +0 -0
  96. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_suggest.py +0 -0
  97. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_tokscale_parse.py +0 -0
  98. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_tui.py +0 -0
  99. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_use_or_lose.py +0 -0
  100. {aiuse-3.3.1 → aiuse-3.3.2}/tests/test_watch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiuse
3
- Version: 3.3.1
3
+ Version: 3.3.2
4
4
  Summary: Aggregate AI subscription/API usage and flag use-it-or-lose-it allotments
5
5
  Author: djbclark
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aiuse"
7
- version = "3.3.1"
7
+ version = "3.3.2"
8
8
  description = "Aggregate AI subscription/API usage and flag use-it-or-lose-it allotments"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """Aggregate provider quotas and flag use-it-or-lose-it allotments."""
2
2
 
3
- __version__ = "3.3.1"
3
+ __version__ = "3.3.2"
@@ -15,6 +15,7 @@ three are normally the same device), so ``aiuse </dev/null`` is protected too.
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ import os
18
19
  import sys
19
20
  from typing import Any
20
21
 
@@ -48,8 +49,27 @@ def save_stdin_tty() -> Any | None:
48
49
  return None
49
50
 
50
51
 
52
+ def _owns_terminal(fd: int) -> bool:
53
+ """True when this process may write terminal attributes without SIGTTOU.
54
+
55
+ ``tcsetattr`` from a process that is not in the terminal's foreground
56
+ process group raises SIGTTOU, which *stops* the process (``aiuse &``,
57
+ ``brew test``, any job-control runner): a hang, not an error. Such a
58
+ process has no business resetting someone else's terminal anyway; the
59
+ foreground shell restores it at its next prompt.
60
+ """
61
+ try:
62
+ return os.tcgetpgrp(fd) == os.getpgrp()
63
+ except (OSError, AttributeError):
64
+ return False
65
+
66
+
51
67
  def restore_stdin_tty(saved: Any | None) -> None:
52
- """Restore termios attrs previously returned by ``save_stdin_tty``."""
68
+ """Restore termios attrs previously returned by ``save_stdin_tty``.
69
+
70
+ A no-op when the attributes are unchanged (the common case) and when this
71
+ process is not the terminal's foreground job (see ``_owns_terminal``).
72
+ """
53
73
  if saved is None:
54
74
  return
55
75
  try:
@@ -58,6 +78,11 @@ def restore_stdin_tty(saved: Any | None) -> None:
58
78
  return
59
79
  try:
60
80
  fd, attrs = saved
61
- termios.tcsetattr(int(fd), termios.TCSADRAIN, attrs)
81
+ fd = int(fd)
82
+ if termios.tcgetattr(fd) == attrs:
83
+ return
84
+ if not _owns_terminal(fd):
85
+ return
86
+ termios.tcsetattr(fd, termios.TCSADRAIN, attrs)
62
87
  except (termios.error, OSError, ValueError, TypeError):
63
88
  return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiuse
3
- Version: 3.3.1
3
+ Version: 3.3.2
4
4
  Summary: Aggregate AI subscription/API usage and flag use-it-or-lose-it allotments
5
5
  Author: djbclark
6
6
  License-Expression: MIT
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import pytest
6
+
5
7
  from aiuse import tty
6
8
  from aiuse.collectors import runner
7
9
  from aiuse.tty import restore_stdin_tty, save_stdin_tty
@@ -50,11 +52,45 @@ def test_restore_targets_the_saved_fd(monkeypatch):
50
52
  import termios
51
53
 
52
54
  calls: list[tuple[int, int, object]] = []
55
+ monkeypatch.setattr(termios, "tcgetattr", lambda fd: ["raw"])
53
56
  monkeypatch.setattr(termios, "tcsetattr", lambda fd, when, attrs: calls.append((fd, when, attrs)))
57
+ monkeypatch.setattr(tty, "_owns_terminal", lambda fd: True)
54
58
  restore_stdin_tty((2, ["attrs"]))
55
59
  assert calls == [(2, termios.TCSADRAIN, ["attrs"])]
56
60
 
57
61
 
62
+ def test_restore_is_a_noop_when_nothing_changed(monkeypatch):
63
+ """No tcsetattr at all in the common case (no SIGTTOU risk, no drain)."""
64
+ import termios
65
+
66
+ monkeypatch.setattr(termios, "tcgetattr", lambda fd: ["attrs"])
67
+ monkeypatch.setattr(termios, "tcsetattr", lambda *_a: pytest.fail("tcsetattr on unchanged attrs"))
68
+ monkeypatch.setattr(tty, "_owns_terminal", lambda fd: pytest.fail("checked pgrp on unchanged attrs"))
69
+ restore_stdin_tty((2, ["attrs"]))
70
+
71
+
72
+ def test_restore_never_writes_from_a_background_job(monkeypatch):
73
+ """``aiuse --version &`` with stderr on the tty: tcsetattr would SIGTTOU-stop us.
74
+
75
+ Regression: 3.3.1's ``brew test`` hung for five minutes in state T.
76
+ """
77
+ import termios
78
+
79
+ monkeypatch.setattr(termios, "tcgetattr", lambda fd: ["raw"])
80
+ monkeypatch.setattr(termios, "tcsetattr", lambda *_a: pytest.fail("tcsetattr from a background process group"))
81
+ monkeypatch.setattr(tty.os, "tcgetpgrp", lambda fd: 4242)
82
+ monkeypatch.setattr(tty.os, "getpgrp", lambda: 1)
83
+ restore_stdin_tty((2, ["attrs"]))
84
+
85
+
86
+ def test_owns_terminal_matches_foreground_pgrp(monkeypatch):
87
+ monkeypatch.setattr(tty.os, "tcgetpgrp", lambda fd: 7)
88
+ monkeypatch.setattr(tty.os, "getpgrp", lambda: 7)
89
+ assert tty._owns_terminal(2)
90
+ monkeypatch.setattr(tty.os, "tcgetpgrp", lambda fd: (_ for _ in ()).throw(OSError("not a tty")))
91
+ assert not tty._owns_terminal(2)
92
+
93
+
58
94
  def test_run_collectors_restores_the_terminal_before_returning(monkeypatch):
59
95
  """A child that leaves the terminal raw must not corrupt the report printed next."""
60
96
  events: list[str] = []
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
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
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes