fp-cloud-cli 0.0.1b1__tar.gz → 0.0.1b2__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 (130) hide show
  1. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/CHANGELOG.md +30 -0
  2. {fp_cloud_cli-0.0.1b1/fp_cloud_cli.egg-info → fp_cloud_cli-0.0.1b2}/PKG-INFO +1 -1
  3. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/_click_compat.py +31 -8
  4. fp_cloud_cli-0.0.1b2/fp_cli/_version.py +1 -0
  5. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/permissions.py +1 -0
  6. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2/fp_cloud_cli.egg-info}/PKG-INFO +1 -1
  7. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cloud_cli.egg-info/SOURCES.txt +2 -0
  8. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/pyproject.toml +9 -2
  9. fp_cloud_cli-0.0.1b2/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  10. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/conftest.cpython-312-pytest-9.1.1.pyc +0 -0
  11. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_alerting.cpython-312-pytest-9.1.1.pyc +0 -0
  12. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_analytics.cpython-312-pytest-9.1.1.pyc +0 -0
  13. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_audits.cpython-312-pytest-9.1.1.pyc +0 -0
  14. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_auth.cpython-312-pytest-9.1.1.pyc +0 -0
  15. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_auth_mode.cpython-312-pytest-9.1.1.pyc +0 -0
  16. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_click_compat.cpython-312-pytest-9.1.1.pyc +0 -0
  17. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_client.cpython-312-pytest-9.1.1.pyc +0 -0
  18. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_commands.cpython-312-pytest-9.1.1.pyc +0 -0
  19. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_config.cpython-312-pytest-9.1.1.pyc +0 -0
  20. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_dashboards_agent.cpython-312-pytest-9.1.1.pyc +0 -0
  21. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_dates.cpython-312-pytest-9.1.1.pyc +0 -0
  22. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_enforcement_logic.cpython-312-pytest-9.1.1.pyc +0 -0
  23. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_facets.cpython-312-pytest-9.1.1.pyc +0 -0
  24. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_failproofai_home.cpython-312-pytest-9.1.1.pyc +0 -0
  25. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_failproofai_home.cpython-312.pyc +0 -0
  26. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_fp_home_contract.cpython-312-pytest-9.1.1.pyc +0 -0
  27. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_hardening.cpython-312-pytest-9.1.1.pyc +0 -0
  28. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_help_table_coverage.cpython-312-pytest-9.1.1.pyc +0 -0
  29. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_keys_queries.cpython-312-pytest-9.1.1.pyc +0 -0
  30. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_list.cpython-312-pytest-9.1.1.pyc +0 -0
  31. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_multivalue.cpython-312-pytest-9.1.1.pyc +0 -0
  32. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_no_customer_identifiers.cpython-312-pytest-9.1.1.pyc +0 -0
  33. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_operator.cpython-312-pytest-9.1.1.pyc +0 -0
  34. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_orgs.cpython-312-pytest-9.1.1.pyc +0 -0
  35. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_output.cpython-312-pytest-9.1.1.pyc +0 -0
  36. fp_cloud_cli-0.0.1b2/tests/__pycache__/test_pagination.cpython-312-pytest-9.1.1.pyc +0 -0
  37. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_policy_check.cpython-312-pytest-9.1.1.pyc +0 -0
  38. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_readme_matches_reality.cpython-312-pytest-9.1.1.pyc +0 -0
  39. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_review_fixes.cpython-312-pytest-9.1.1.pyc +0 -0
  40. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_telemetry_completeness.cpython-312-pytest-9.1.1.pyc +0 -0
  41. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_usage.cpython-312-pytest-9.1.1.pyc +0 -0
  42. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_v1_origin_diagnostic.cpython-312-pytest-9.1.1.pyc +0 -0
  43. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_v1_routing.cpython-312-pytest-9.1.1.pyc +0 -0
  44. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__pycache__/test_whoami.cpython-312-pytest-9.1.1.pyc +0 -0
  45. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_click_compat.py +54 -0
  46. fp_cloud_cli-0.0.1b2/tests/test_pagination.py +204 -0
  47. fp_cloud_cli-0.0.1b1/fp_cli/_version.py +0 -1
  48. fp_cloud_cli-0.0.1b1/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  49. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/LICENSE +0 -0
  50. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/MANIFEST.in +0 -0
  51. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/README.md +0 -0
  52. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/__init__.py +0 -0
  53. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/__main__.py +0 -0
  54. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/_context.py +0 -0
  55. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/analytics.py +0 -0
  56. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/analytics_config.py +0 -0
  57. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/analytics_registry.py +0 -0
  58. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/app.py +0 -0
  59. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/auth.py +0 -0
  60. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/client.py +0 -0
  61. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/__init__.py +0 -0
  62. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/_write.py +0 -0
  63. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/agent_cmds.py +0 -0
  64. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/alerts_cmds.py +0 -0
  65. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/audits_cmds.py +0 -0
  66. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/auth_cmds.py +0 -0
  67. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/errors_cmds.py +0 -0
  68. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/evals_cmds.py +0 -0
  69. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/events_cmds.py +0 -0
  70. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/fleet_cmds.py +0 -0
  71. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/guardrails_cmds.py +0 -0
  72. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/incidents_cmds.py +0 -0
  73. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/keys_cmds.py +0 -0
  74. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/list_cmds.py +0 -0
  75. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/orgs_cmds.py +0 -0
  76. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/policies_cmds.py +0 -0
  77. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/queries_cmds.py +0 -0
  78. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/sessions_cmds.py +0 -0
  79. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/settings_cmds.py +0 -0
  80. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/usage_cmds.py +0 -0
  81. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/commands/users_cmds.py +0 -0
  82. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/config.py +0 -0
  83. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/dates.py +0 -0
  84. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/enforcement.py +0 -0
  85. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/errors.py +0 -0
  86. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/models.py +0 -0
  87. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/orgs.py +0 -0
  88. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/output.py +0 -0
  89. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/policy_check.py +0 -0
  90. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/py.typed +0 -0
  91. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/select.py +0 -0
  92. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cli/theme.py +0 -0
  93. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cloud_cli.egg-info/dependency_links.txt +0 -0
  94. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cloud_cli.egg-info/entry_points.txt +0 -0
  95. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cloud_cli.egg-info/requires.txt +0 -0
  96. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/fp_cloud_cli.egg-info/top_level.txt +0 -0
  97. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/setup.cfg +0 -0
  98. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/__init__.py +0 -0
  99. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/conftest.py +0 -0
  100. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_alerting.py +0 -0
  101. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_analytics.py +0 -0
  102. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_audits.py +0 -0
  103. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_auth.py +0 -0
  104. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_auth_mode.py +0 -0
  105. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_client.py +0 -0
  106. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_commands.py +0 -0
  107. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_config.py +0 -0
  108. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_dashboards_agent.py +0 -0
  109. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_dates.py +0 -0
  110. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_enforcement_logic.py +0 -0
  111. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_facets.py +0 -0
  112. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_failproofai_home.py +0 -0
  113. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_fp_home_contract.py +0 -0
  114. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_hardening.py +0 -0
  115. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_help_table_coverage.py +0 -0
  116. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_keys_queries.py +0 -0
  117. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_list.py +0 -0
  118. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_multivalue.py +0 -0
  119. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_no_customer_identifiers.py +0 -0
  120. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_operator.py +0 -0
  121. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_orgs.py +0 -0
  122. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_output.py +0 -0
  123. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_policy_check.py +0 -0
  124. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_readme_matches_reality.py +0 -0
  125. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_review_fixes.py +0 -0
  126. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_telemetry_completeness.py +0 -0
  127. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_usage.py +0 -0
  128. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_v1_origin_diagnostic.py +0 -0
  129. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_v1_routing.py +0 -0
  130. {fp_cloud_cli-0.0.1b1 → fp_cloud_cli-0.0.1b2}/tests/test_whoami.py +0 -0
@@ -1,5 +1,35 @@
1
1
  # Changelog — `fp` CLI
2
2
 
3
+ ## 0.0.1b2 — 2026-08-25
4
+
5
+ Open for the next release. `0.0.1b1` published on 2026-08-24 and the `bump` job
6
+ moved the version here automatically. Add entries as changes merge — this section
7
+ becomes the GitHub Release body when it ships.
8
+
9
+ ### Fixes
10
+
11
+ - **Typed errors keep their exit codes under typer 0.27.2.** That release moved
12
+ `Abort` out of its vendored `typer._click.exceptions`, and `_click_compat`
13
+ imported all six of its symbols under a single
14
+ `try: … except ImportError: from click import …`. One missing name was enough
15
+ to send the whole block to the fallback, binding `ClickException`,
16
+ `UsageError`, `BadParameter`, `Command` and `Parameter` to the pip `click`
17
+ distribution — which is *not* the Click Typer runs, and Typer catches only its
18
+ own. So every typed error escaped its handler: `fp alerts show ghost` exited
19
+ **1 with an empty stderr** instead of **6** with `no alert named "ghost"`, and
20
+ likewise for exits 2, 3, 4 and 5. Nothing warned; the CLI imported and every
21
+ happy path passed. The Click is now decided **once**, on whether `typer._click`
22
+ exists at all, and every symbol imported from that decision, so a future move
23
+ fails at import — a CLI that refuses to start — instead of silently flattening
24
+ the exit-code contract. `Abort` is resolved from `typer.Abort`, which is by
25
+ construction the class `typer.prompt` raises and typer's own `_main` catches on
26
+ every version in range. Suite verified green against typer 0.25.1, 0.27.0,
27
+ 0.27.1 and 0.27.2 (#771)
28
+
29
+ ### Dependencies
30
+
31
+ - typer 0.27.1 → 0.27.2, click 8.4.2 → 8.5.0, posthog 7.42.0 → 7.44.2 (#771)
32
+
3
33
  ## 0.0.1b1 — 2026-08-24
4
34
 
5
35
  ### The package is `fp-cloud-cli`; the command is still `fp`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fp-cloud-cli
3
- Version: 0.0.1b1
3
+ Version: 0.0.1b2
4
4
  Summary: Command line client for FailproofAI Cloud — sessions, events, evaluations and audits from your terminal
5
5
  Author-email: Failproof AI <failproofai@exosphere.host>
6
6
  License: MIT License
@@ -11,9 +11,10 @@ compiles, and every happy path still passes:
11
11
  should print ``✗ …`` and exit 5 exits **1 with an empty stderr**, and the
12
12
  ``rich_format_error`` hook in ``app.py`` (reached only *after* that catch) never
13
13
  runs. Same for the ``UsageError``/``BadParameter`` we raise by hand.
14
- * ``Abort`` — ``typer.prompt`` raises its own Click's ``Abort`` on closed stdin, so a
15
- pip-Click ``except click.Abort`` stops matching and the clean "no TTY, pass a slug"
16
- usage error becomes a bare abort.
14
+ * ``Abort`` — ``typer.prompt`` raises the ``Abort`` **typer** exports on closed stdin
15
+ (0.27.2 moved that class out of the vendored Click entirely), so a pip-Click
16
+ ``except click.Abort`` stops matching and the clean "no TTY, pass a slug" usage error
17
+ becomes a bare abort.
17
18
  * Options — Typer 0.26+ has no ``Option`` class in its vendored Click *at all*: every
18
19
  option in a Typer-built tree is a ``typer.core.TyperOption``, subclassing
19
20
  ``Parameter`` directly. ``isinstance(param, click.Option)`` is then quietly always
@@ -27,14 +28,36 @@ again, and that our errors are still the class Typer catches.
27
28
 
28
29
  from __future__ import annotations
29
30
 
31
+ # `Abort` is resolved from `typer` itself rather than from either Click, because it is
32
+ # the one symbol here that is not stably a Click class: typer 0.27.2 moved it out of the
33
+ # vendored `typer._click.exceptions` into a plain `typer.exceptions.Abort(RuntimeError)`.
34
+ # `typer.Abort` tracks that move — it is `click.Abort` before 0.26, the vendored class
35
+ # through 0.27.1, the RuntimeError from 0.27.2 — and it is by construction the class
36
+ # `typer.prompt` raises and typer's own `_main` catches, which is the only property
37
+ # `select.py`'s `except click.Abort` needs.
38
+ from typer import Abort as Abort
39
+
40
+ # Pick the Click ONCE, on whether typer vendors one at all, then import every symbol
41
+ # from that choice. Deciding per symbol — a single `try` around the whole vendored
42
+ # import block, falling back to pip `click` on any ImportError — is what shipped
43
+ # through 0.27.1 and it failed exactly as silently as this module exists to prevent:
44
+ # 0.27.2 removing `typer._click.exceptions.Abort` made that one missing name rebind ALL
45
+ # SIX symbols to pip Click, so every typed error escaped Typer's handler as exit 1 with
46
+ # an empty stderr. A name that goes missing inside the chosen Click must raise here, at
47
+ # import, where it is a CLI that refuses to start rather than one that silently stops
48
+ # reporting errors.
30
49
  try:
31
- # typer >= 0.26. pip `click` may well still be installed — it is simply not the
32
- # Click in play, so binding to it here would reintroduce the whole class of bug.
33
- from typer._click import ClickException, Command, Parameter
34
- from typer._click.exceptions import Abort, BadParameter, UsageError
50
+ import typer._click as _typer_click # typer >= 0.26 vendors its own Click
35
51
  except ImportError: # typer < 0.26 drives the pip `click` distribution directly
52
+ _typer_click = None # type: ignore[assignment]
53
+
54
+ if _typer_click is not None:
55
+ # pip `click` may well still be installed — it is simply not the Click in play, so
56
+ # binding to it here would reintroduce the whole class of bug.
57
+ from typer._click import ClickException, Command, Parameter
58
+ from typer._click.exceptions import BadParameter, UsageError
59
+ else:
36
60
  from click import ( # type: ignore[assignment]
37
- Abort,
38
61
  BadParameter,
39
62
  ClickException,
40
63
  Command,
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1b2"
@@ -26,6 +26,7 @@ ALL_PERMISSIONS: List[str] = [
26
26
  "users:delete",
27
27
  "evaluations:read",
28
28
  "evaluations:trigger",
29
+ "evaluations:run",
29
30
  "dashboards:read",
30
31
  "dashboards:write",
31
32
  "dashboards:delete",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fp-cloud-cli
3
- Version: 0.0.1b1
3
+ Version: 0.0.1b2
4
4
  Summary: Command line client for FailproofAI Cloud — sessions, events, evaluations and audits from your terminal
5
5
  Author-email: Failproof AI <failproofai@exosphere.host>
6
6
  License: MIT License
@@ -79,6 +79,7 @@ tests/test_no_customer_identifiers.py
79
79
  tests/test_operator.py
80
80
  tests/test_orgs.py
81
81
  tests/test_output.py
82
+ tests/test_pagination.py
82
83
  tests/test_policy_check.py
83
84
  tests/test_readme_matches_reality.py
84
85
  tests/test_review_fixes.py
@@ -114,6 +115,7 @@ tests/__pycache__/test_no_customer_identifiers.cpython-312-pytest-9.1.1.pyc
114
115
  tests/__pycache__/test_operator.cpython-312-pytest-9.1.1.pyc
115
116
  tests/__pycache__/test_orgs.cpython-312-pytest-9.1.1.pyc
116
117
  tests/__pycache__/test_output.cpython-312-pytest-9.1.1.pyc
118
+ tests/__pycache__/test_pagination.cpython-312-pytest-9.1.1.pyc
117
119
  tests/__pycache__/test_policy_check.cpython-312-pytest-9.1.1.pyc
118
120
  tests/__pycache__/test_readme_matches_reality.cpython-312-pytest-9.1.1.pyc
119
121
  tests/__pycache__/test_review_fixes.cpython-312-pytest-9.1.1.pyc
@@ -31,7 +31,14 @@ dependencies = [
31
31
  # through that — which is what makes both sides of the split work. Import Click from
32
32
  # there and nowhere else; `tests/test_click_compat.py` enforces it, because every way
33
33
  # this breaks is silent (typed errors collapse to exit 1 with an empty stderr, and the
34
- # telemetry flag catalog empties). Suite verified against 0.25.1 and 0.27.0.
34
+ # telemetry flag catalog empties). Suite verified against 0.25.1, 0.27.0, 0.27.1 and
35
+ # 0.27.2.
36
+ #
37
+ # The split keeps moving INSIDE the vendored tree, so the shim resolves each symbol
38
+ # separately: 0.27.2 moved `Abort` out of `typer._click.exceptions` into
39
+ # `typer.exceptions`, and the one all-or-nothing `try/except ImportError` that used to
40
+ # wrap the vendored import sent every OTHER symbol to pip Click over that single
41
+ # missing name — 105 tests red, every typed error at exit 1.
35
42
  #
36
43
  # The floor is 0.13, not 0.12, because 0.12 cannot BUILD this app: the global
37
44
  # `--insecure/--secure` option in `app.py` is an `Optional[bool]` with a
@@ -39,7 +46,7 @@ dependencies = [
39
46
  # rejects with `TypeError: Secondary flag is not valid for non-boolean flag`.
40
47
  # That fires while the command tree is constructed, before any parsing, so
41
48
  # `fp --version`, `fp --help` and every subcommand die identically — an
42
- # installed CLI that cannot run one command. `uv.lock` pins 0.27.1, so
49
+ # installed CLI that cannot run one command. `uv.lock` pins 0.27.2, so
43
50
  # neither CI nor the publish smoke test ever installed the broken part of the
44
51
  # range. Measured across 0.12.0/0.12.3/0.12.5 (all fail) and 0.13.0 upward
45
52
  # (all pass).
@@ -11,6 +11,7 @@ from __future__ import annotations
11
11
  import ast
12
12
  import pathlib
13
13
 
14
+ import pytest
14
15
  import typer
15
16
  from typer.testing import CliRunner
16
17
 
@@ -95,6 +96,59 @@ def test_error_exit_codes_are_distinct_per_class():
95
96
  assert issubclass(KeyModeUnsupportedError, _click_compat.ClickException)
96
97
 
97
98
 
99
+ def test_every_symbol_comes_from_the_click_typer_runs():
100
+ """No symbol may quietly come from pip Click while Typer is running its own.
101
+
102
+ This is the alarm for a *partial* miss. `_click_compat` used to wrap the whole
103
+ vendored import in one `try: … except ImportError: from click import …`, so a single
104
+ name disappearing from `typer._click` rebound **all six** symbols to pip Click — and
105
+ that is not hypothetical: typer 0.27.2 moved `Abort` out of
106
+ `typer._click.exceptions`, and the fallback fired, and 105 tests went red at once
107
+ with typed errors collapsing to exit 1. Assert the provenance of each symbol
108
+ separately, so the next move is one failure naming one symbol.
109
+ """
110
+ typer_click = pytest.importorskip(
111
+ "typer._click", reason="typer < 0.26 has no vendored Click; pip click IS the right one"
112
+ )
113
+ wrong = {
114
+ name: obj.__module__
115
+ for name in ("ClickException", "UsageError", "BadParameter", "Command", "Parameter")
116
+ if not (obj := getattr(_click_compat, name)).__module__.startswith(typer_click.__name__)
117
+ }
118
+ assert not wrong, f"these came from pip Click, not the Click Typer runs: {wrong}"
119
+
120
+
121
+ def test_abort_is_the_one_typer_raises():
122
+ """`Abort` is the exception NOT to pin to a Click — pin it to Typer.
123
+
124
+ It is `click.Abort` before typer 0.26, the vendored class through 0.27.1, and a
125
+ plain `typer.exceptions.Abort(RuntimeError)` from 0.27.2 on. `typer.Abort` is the
126
+ class `typer.prompt` raises and typer's own `_main` catches on every one of those,
127
+ which is the only thing `select.py`'s `except click.Abort` needs to be true.
128
+ """
129
+ assert _click_compat.Abort is typer.Abort
130
+
131
+
132
+ def test_hand_raised_usage_errors_reach_typers_handler():
133
+ """The other half of the contract: `UsageError`/`BadParameter` we raise ourselves.
134
+
135
+ `test_typed_errors_reach_typers_handler` covers the `ClickException` subclasses in
136
+ `errors.py`; these two are raised directly through the shim (`raise
137
+ click.UsageError(...)`) from a dozen call sites, and bound to the wrong Click they
138
+ escape uncaught the same way — exit 1, empty stderr, instead of a clean exit 2.
139
+ """
140
+ for exc_class in (_click_compat.UsageError, _click_compat.BadParameter):
141
+ app = typer.Typer()
142
+
143
+ @app.command()
144
+ def boom() -> None:
145
+ raise exc_class("bad input")
146
+
147
+ result = CliRunner().invoke(app, [])
148
+ assert result.exit_code == 2, (exc_class, result.output)
149
+ assert "bad input" in result.output
150
+
151
+
98
152
  def test_is_option_recognises_typer_options():
99
153
  """Typer's vendored Click has no `Option` class, so `isinstance` cannot answer this.
100
154
 
@@ -0,0 +1,204 @@
1
+ """When `--all` stops early, the response must say so.
2
+
3
+ The defect these pin was measured against a live instance, on the CLI this one
4
+ was moved from:
5
+
6
+ fp --json sessions --since 7d --limit 200 -> 200 rows, next_cursor set
7
+ fp --json sessions --since 7d --all -> 50 rows (25x undercount)
8
+ fp --json sessions --since 7d --all --limit 5000 -> 1,274 rows
9
+
10
+ `--all` inherits `--limit`'s default of 50 — which is documented and deliberate
11
+ here — but the envelope then hard-coded `next_cursor: null`, so a 4% sample of a
12
+ customer's fleet came back with exit 0, no warning, and a response that read as
13
+ complete. Nothing in the output could tell you otherwise.
14
+
15
+ The fix (`Walk`, carried through `client.paginate`) is present in this repo and
16
+ was untested when these were written. That combination is the one worth
17
+ guarding: a silent-completeness bug cannot be caught by reading the code, only
18
+ by asserting on what the envelope claims.
19
+
20
+ So every test here checks the REPORTED completeness, not just the row count.
21
+
22
+ The fake feed is a real cursor server — it honours `limit` and `cursor` — not a
23
+ list of canned pages. A fixture that ignored `limit` would pass no matter how
24
+ few rows the CLI asked for, which is to say it could not fail on the bug.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+
31
+ import httpx
32
+ import pytest
33
+ import respx
34
+
35
+ from fp_cli import client
36
+ from fp_cli.app import app
37
+
38
+ from .conftest import BASE_URL as BASE
39
+
40
+
41
+ def _feed(key: str, total: int, row):
42
+ """respx side_effect: a cursor-paginated feed holding ``total`` rows.
43
+
44
+ The cursor is the offset of the next row (opaque to the CLI, which only
45
+ round-trips it). ``limit`` is honoured exactly, so the CLI's own page sizing
46
+ decides how much of the feed it can actually reach.
47
+ """
48
+
49
+ def handler(request: httpx.Request) -> httpx.Response:
50
+ params = request.url.params
51
+ limit = int(params["limit"])
52
+ offset = int(params.get("cursor") or 0)
53
+ rows = [row(i) for i in range(offset, min(offset + limit, total))]
54
+ nxt = offset + len(rows)
55
+ return httpx.Response(
56
+ 200, json={key: rows, "next_cursor": str(nxt) if nxt < total else None}
57
+ )
58
+
59
+ return handler
60
+
61
+
62
+ def _event_row(i: int) -> dict:
63
+ return {
64
+ "id": i, "session_id": "s", "agent_id": "a", "event_type": "tool_use",
65
+ "ts": "2026-08-24T12:00:00Z", "environment": "prod", "summary": f"row {i}",
66
+ }
67
+
68
+
69
+ def _session_row(i: int) -> dict:
70
+ return {
71
+ "session_id": f"sess-{i}", "agent_id": "a", "environment": "prod",
72
+ "last_event_at": "2026-08-24T12:00:00Z", "latest_evaluation": None,
73
+ }
74
+
75
+
76
+ def _eval_row(i: int) -> dict:
77
+ return {
78
+ "id": f"eval-{i}", "session_id": "s", "agent_id": "a", "environment": "prod",
79
+ "status": "done", "scores": {"helpfulness": 0.9}, "created_at": "2026-08-24T12:00:00Z",
80
+ }
81
+
82
+
83
+ # (command, endpoint, server key, envelope key, row builder). Every command that
84
+ # takes `--all`; the bug was reported on `sessions` but lived in the shared
85
+ # walker, so all four had it. `errors` is the one where the two keys differ: it
86
+ # reads the shared errored-events feed (`{"events": …}`) and re-labels it
87
+ # `errors` on the way out.
88
+ FEEDS = [
89
+ ("events", "/api/events/summary", "events", "events", _event_row),
90
+ ("sessions", "/api/sessions", "sessions", "sessions", _session_row),
91
+ ("evals", "/api/evaluations", "evaluations", "evaluations", _eval_row),
92
+ ("errors", "/api/events/summary", "events", "errors", _event_row),
93
+ ]
94
+ IDS = [f[0] for f in FEEDS]
95
+
96
+ TOTAL = 274 # > the 50 default, and not a multiple of the 200 page size
97
+
98
+
99
+ @pytest.mark.parametrize("command,endpoint,server_key,key,row", FEEDS, ids=IDS)
100
+ @respx.mock
101
+ def test_all_stopped_by_the_default_limit_is_reported_not_silent(
102
+ logged_in, runner, command, endpoint, server_key, key, row
103
+ ):
104
+ """THE REGRESSION. `--all` stopping at 50 is the documented behaviour; saying
105
+ the feed was exhausted is the bug."""
106
+ respx.get(f"{BASE}{endpoint}").mock(side_effect=_feed(server_key, TOTAL, row))
107
+ res = runner.invoke(app, ["--base-url", BASE, "--json", command, "--all"])
108
+ assert res.exit_code == 0, res.output
109
+ out = json.loads(res.stdout)
110
+ assert len(out[key]) == 50, "the documented default bound still applies"
111
+ assert out["next_cursor"] is not None, (
112
+ "a walk that stopped on --limit must hand back a resumable cursor; "
113
+ "null here is the 4%-sample-reported-as-complete bug"
114
+ )
115
+
116
+
117
+ @pytest.mark.parametrize("command,endpoint,server_key,key,row", FEEDS, ids=IDS)
118
+ @respx.mock
119
+ def test_all_that_exhausts_the_feed_reports_complete(
120
+ logged_in, runner, command, endpoint, server_key, key, row
121
+ ):
122
+ """The other direction, and the reason `truncated` cannot just be "we hit the
123
+ limit": a walk that took every row must NOT claim there is more."""
124
+ respx.get(f"{BASE}{endpoint}").mock(side_effect=_feed(server_key, 30, row))
125
+ res = runner.invoke(app, ["--base-url", BASE, "--json", command, "--all", "--limit", "500"])
126
+ assert res.exit_code == 0, res.output
127
+ out = json.loads(res.stdout)
128
+ assert len(out[key]) == 30
129
+ assert out["next_cursor"] is None, "an exhausted feed must not look truncated"
130
+
131
+
132
+ @respx.mock
133
+ def test_an_explicit_limit_is_honoured_and_reported(logged_in, runner):
134
+ """`--all --limit N` is how you actually get more than the default, so the
135
+ bound has to hold exactly and still report the remainder."""
136
+ respx.get(f"{BASE}/api/sessions").mock(side_effect=_feed("sessions", TOTAL, _session_row))
137
+ res = runner.invoke(
138
+ app, ["--base-url", BASE, "--json", "sessions", "--all", "--limit", "220"]
139
+ )
140
+ assert res.exit_code == 0, res.output
141
+ out = json.loads(res.stdout)
142
+ assert len(out["sessions"]) == 220
143
+ assert out["next_cursor"] is not None
144
+
145
+
146
+ @respx.mock
147
+ def test_all_pages_in_chunks_rather_than_one_giant_request(logged_in, runner):
148
+ """The walk must actually WALK. Asking for 220 in a single request would work
149
+ against this fake and fail against a server that caps a page at 200."""
150
+ route = respx.get(f"{BASE}/api/sessions").mock(
151
+ side_effect=_feed("sessions", TOTAL, _session_row)
152
+ )
153
+ res = runner.invoke(
154
+ app, ["--base-url", BASE, "--json", "sessions", "--all", "--limit", "220"]
155
+ )
156
+ assert res.exit_code == 0, res.output
157
+ assert route.call_count > 1, "a 220-row walk must span more than one request"
158
+ for call in route.calls:
159
+ assert int(call.request.url.params["limit"]) <= 200
160
+
161
+
162
+ # --- the walker itself, below the CLI ---------------------------------------
163
+
164
+
165
+ def _pages(total: int, page: int):
166
+ """A `fetch_page` over ``total`` rows, honouring the limit it is given."""
167
+
168
+ def fetch(cursor=None, limit=None):
169
+ offset = int(cursor or 0)
170
+ size = min(limit or page, page)
171
+ items = list(range(offset, min(offset + size, total)))
172
+ nxt = offset + len(items)
173
+ return client.Page(items=items, next_cursor=str(nxt) if nxt < total else None)
174
+
175
+ return fetch
176
+
177
+
178
+ def test_walk_records_the_cursor_it_stopped_on():
179
+ walk = client.Walk()
180
+ items = list(client.paginate(_pages(500, 200), limit=250, walk=walk))
181
+ assert len(items) == 250
182
+ assert walk.truncated is True
183
+ assert walk.next_cursor is not None, "a bounded walk must be resumable"
184
+
185
+
186
+ def test_walk_that_exhausts_the_feed_is_not_flagged_truncated():
187
+ walk = client.Walk()
188
+ items = list(client.paginate(_pages(120, 200), limit=500, walk=walk))
189
+ assert len(items) == 120
190
+ assert walk.truncated is False
191
+ assert walk.next_cursor is None
192
+
193
+
194
+ def test_truncated_is_not_derivable_from_the_row_count():
195
+ """The subtle case, and why `Walk` exists at all: a walk whose limit lands
196
+ EXACTLY on the last row is complete, and one that lands exactly on a page
197
+ boundary with more behind it is not. Row count cannot tell those apart."""
198
+ exact = client.Walk()
199
+ list(client.paginate(_pages(200, 200), limit=200, walk=exact))
200
+ assert exact.truncated is False, "taking every row is not a truncation"
201
+
202
+ boundary = client.Walk()
203
+ list(client.paginate(_pages(400, 200), limit=200, walk=boundary))
204
+ assert boundary.truncated is True, "stopping on a boundary with more behind it is"
@@ -1 +0,0 @@
1
- __version__ = "0.0.1b1"
File without changes
File without changes
File without changes