dyno-phi 0.3.0__tar.gz → 0.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 (165) hide show
  1. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/.gitignore +4 -1
  2. dyno_phi-0.3.2/CHANGELOG.md +134 -0
  3. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/PKG-INFO +41 -19
  4. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/README.md +36 -14
  5. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/pyproject.toml +5 -5
  6. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/__init__.py +2 -0
  7. dyno_phi-0.3.2/src/phi/_version.py +1 -0
  8. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/async_client.py +39 -6
  9. dyno_phi-0.3.2/src/phi/cli/commands/audit.py +133 -0
  10. dyno_phi-0.3.2/src/phi/cli/commands/auth.py +283 -0
  11. dyno_phi-0.3.2/src/phi/cli/commands/billing.py +145 -0
  12. dyno_phi-0.3.2/src/phi/cli/commands/doctor.py +177 -0
  13. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/commands/jobs.py +31 -7
  14. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/commands/models.py +18 -2
  15. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/commands/pipeline.py +7 -19
  16. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/commands/quota.py +4 -1
  17. dyno_phi-0.3.2/src/phi/cli/commands/skills.py +251 -0
  18. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/document_sync.py +16 -5
  19. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/errors.py +3 -0
  20. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/main.py +8 -0
  21. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/state.py +5 -0
  22. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/client.py +33 -3
  23. dyno_phi-0.3.2/src/phi/config.py +291 -0
  24. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/errors.py +5 -1
  25. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/graph.py +26 -28
  26. dyno_phi-0.3.2/src/phi/models/__init__.py +16 -0
  27. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/_generated.py +2 -0
  28. dyno_phi-0.3.2/src/phi/models/audit.py +43 -0
  29. dyno_phi-0.3.2/src/phi/models/billing.py +92 -0
  30. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/job_types.py +5 -0
  31. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/jobs.py +17 -0
  32. dyno_phi-0.3.2/src/phi/models/quota.py +52 -0
  33. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/runs.py +45 -2
  34. dyno_phi-0.3.2/src/phi/oauth.py +670 -0
  35. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/profiles.py +38 -2
  36. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/__init__.py +0 -16
  37. dyno_phi-0.3.2/src/phi/resources/audit.py +292 -0
  38. dyno_phi-0.3.2/src/phi/resources/billing.py +113 -0
  39. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/jobs.py +45 -1
  40. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/quota.py +35 -6
  41. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/runs.py +5 -4
  42. dyno_phi-0.3.2/tests/cli/test_audit.py +125 -0
  43. dyno_phi-0.3.2/tests/cli/test_auth.py +502 -0
  44. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_cli_scaffold.py +3 -0
  45. dyno_phi-0.3.2/tests/cli/test_doctor.py +190 -0
  46. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_document_sync.py +59 -0
  47. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_jobs.py +73 -0
  48. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_json_contract.py +40 -5
  49. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_models_cmd.py +33 -0
  50. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_pipeline.py +19 -7
  51. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_quota.py +11 -1
  52. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_runs.py +3 -1
  53. dyno_phi-0.3.2/tests/cli/test_skills.py +175 -0
  54. dyno_phi-0.3.2/tests/test_audit.py +108 -0
  55. dyno_phi-0.3.2/tests/test_billing.py +87 -0
  56. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_errors.py +14 -0
  57. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_graph.py +0 -4
  58. dyno_phi-0.3.2/tests/test_oauth.py +657 -0
  59. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_packaging.py +31 -31
  60. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_quota.py +24 -0
  61. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_runs.py +15 -9
  62. dyno_phi-0.3.0/CHANGELOG.md +0 -53
  63. dyno_phi-0.3.0/src/phi/_version.py +0 -1
  64. dyno_phi-0.3.0/src/phi/cli/commands/auth.py +0 -166
  65. dyno_phi-0.3.0/src/phi/config.py +0 -100
  66. dyno_phi-0.3.0/src/phi/models/__init__.py +0 -7
  67. dyno_phi-0.3.0/src/phi/models/quota.py +0 -25
  68. dyno_phi-0.3.0/tests/cli/test_auth.py +0 -268
  69. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/examples/README.md +0 -0
  70. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/examples/async_concurrent.py +0 -0
  71. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/examples/compose_dag.py +0 -0
  72. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/examples/download_results.py +0 -0
  73. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/examples/pipelines/fold_and_analyze.yaml +0 -0
  74. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/examples/pipelines/mpnn_esmfold_alphafold.yaml +0 -0
  75. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/examples/quickstart/README.md +0 -0
  76. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/examples/quickstart/trp_cage.fasta +0 -0
  77. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/examples/run_pipeline.py +0 -0
  78. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/examples/submit_fold.py +0 -0
  79. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/_schema_validation.py +0 -0
  80. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/_transport.py +0 -0
  81. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/__init__.py +0 -0
  82. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/__main__.py +0 -0
  83. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/commands/__init__.py +0 -0
  84. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/commands/artifacts.py +0 -0
  85. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/commands/datasets.py +0 -0
  86. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/commands/documents.py +0 -0
  87. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/commands/export.py +0 -0
  88. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/commands/logs.py +0 -0
  89. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/commands/projects.py +0 -0
  90. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/commands/quickstart.py +0 -0
  91. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/commands/runs.py +0 -0
  92. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/commands/use.py +0 -0
  93. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/completion.py +0 -0
  94. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/params.py +0 -0
  95. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/cli/render.py +0 -0
  96. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/files.py +0 -0
  97. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/artifacts.py +0 -0
  98. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/common.py +0 -0
  99. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/datasets.py +0 -0
  100. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/designs.py +0 -0
  101. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/documents.py +0 -0
  102. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/imports.py +0 -0
  103. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/logs.py +0 -0
  104. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/projects.py +0 -0
  105. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/run_admin.py +0 -0
  106. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/sequences.py +0 -0
  107. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/models/tool_configs.py +0 -0
  108. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/py.typed +0 -0
  109. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/artifacts.py +0 -0
  110. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/datasets.py +0 -0
  111. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/designs.py +0 -0
  112. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/documents.py +0 -0
  113. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/files.py +0 -0
  114. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/imports.py +0 -0
  115. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/job_types.py +0 -0
  116. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/logs.py +0 -0
  117. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/projects.py +0 -0
  118. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/run_admin.py +0 -0
  119. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/sequences.py +0 -0
  120. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/tool_configs.py +0 -0
  121. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/resources/uploads.py +0 -0
  122. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/schema_cache.py +0 -0
  123. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/src/phi/waiters.py +0 -0
  124. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/__init__.py +0 -0
  125. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/__init__.py +0 -0
  126. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/conftest.py +0 -0
  127. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_artifacts.py +0 -0
  128. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_completion.py +0 -0
  129. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_datasets.py +0 -0
  130. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_documents.py +0 -0
  131. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_export.py +0 -0
  132. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_logs.py +0 -0
  133. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_params.py +0 -0
  134. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_projects.py +0 -0
  135. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/cli/test_quickstart.py +0 -0
  136. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/conftest.py +0 -0
  137. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/live/__init__.py +0 -0
  138. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/live/conftest.py +0 -0
  139. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/live/test_live_artifacts_logs.py +0 -0
  140. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/live/test_live_auth_discovery.py +0 -0
  141. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/live/test_live_datasets_export.py +0 -0
  142. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/live/test_live_errors.py +0 -0
  143. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/live/test_live_jobs.py +0 -0
  144. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/live/test_live_pipelines.py +0 -0
  145. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/live/test_live_quickstart_sdk.py +0 -0
  146. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_artifacts.py +0 -0
  147. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_config.py +0 -0
  148. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_datasets.py +0 -0
  149. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_designs.py +0 -0
  150. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_docs_examples.py +0 -0
  151. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_documents.py +0 -0
  152. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_files.py +0 -0
  153. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_imports.py +0 -0
  154. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_job_types.py +0 -0
  155. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_jobs.py +0 -0
  156. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_logs.py +0 -0
  157. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_parity.py +0 -0
  158. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_projects.py +0 -0
  159. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_run_admin.py +0 -0
  160. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_schema_validation.py +0 -0
  161. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_sequences.py +0 -0
  162. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_tool_configs.py +0 -0
  163. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_transport.py +0 -0
  164. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_uploads.py +0 -0
  165. {dyno_phi-0.3.0 → dyno_phi-0.3.2}/tests/test_waiters.py +0 -0
@@ -92,6 +92,7 @@ cloudbuild/*.log
92
92
  *.pdb
93
93
  !tests/fixtures/metrics/ipsae_nonzero_multisubchain.pdb
94
94
  !examples/bindcraft2/*.pdb
95
+ !experiments/campaigns/y4_agent_designs/assets/*.pdb
95
96
  *.cif
96
97
  !examples/bindcraft2/*.cif
97
98
  *.csv
@@ -122,4 +123,6 @@ experiments/cxcr3_vhh_design/data/scored/refold_manifest*.tsv
122
123
  # Agent-development guide is account/app-specific (DynoPhiAgent). Other
123
124
  # contributors run different agents/apps, so keep it local-only, not in the
124
125
  # shared repo.
125
- docs/agent-development.md
126
+ docs/agent-development.md
127
+ # The tag ruleset the docs tell an admin to apply; *.json is ignored above.
128
+ !docs/github-rulesets/tags.json
@@ -0,0 +1,134 @@
1
+ # Changelog
2
+
3
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
4
+ package follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The
5
+ version is read from `src/phi/_version.py`; a release is the tag `client-v<version>`
6
+ on `dynotx/phi-api`, which `.github/workflows/publish-client.yml` builds and publishes.
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.3.2] - 2026-10-27
11
+
12
+ ### Changed
13
+
14
+ - `phi auth whoami`'s `credits_available`, `phi quota`'s available line and the
15
+ `insufficient credits` message count purchased credits as well as the
16
+ allowance (`total_available`, falling back to `credits_available` on an older
17
+ server). `Quota.spendable` is that number; `phi quota` adds an allowance line.
18
+ - `runs.estimate` sends `params` and `inputs`, and the estimate breakdown is a
19
+ list of `{stage, unit, count, cents}`. `phi pipeline estimate` asks
20
+ `POST /v1/jobs/estimate` for the price a submit would reserve.
21
+ - `phi auth login` no longer prompts for a base URL and a key by default; that
22
+ is `phi auth login --interactive --key`.
23
+
24
+ ### Added
25
+
26
+ - `JobTypeSchema.typical_seconds`, `PipelineTypeResponse.typical_seconds`,
27
+ `JobSubmitResponse.estimated_seconds` and `Job.typical_seconds`: how long a
28
+ job type usually takes from submit to completion, from the API's completed
29
+ runs. `phi models list` shows it as a Time column ("about 4 min", "40 s") and
30
+ `phi jobs wait` and `phi jobs submit --wait` count the elapsed time against
31
+ it ("[30s of about 240s] RUNNING fold").
32
+ - `Job`, `JobStage`, `RunResponse`, `StageResponse` and `DagNode` read the
33
+ optional `error_code`, `exit_code` and `recommended_action` a failed run
34
+ carries; `phi jobs wait`, `phi pipeline run` and `phi quickstart` name the
35
+ code before the error when a failing job has one.
36
+ - `phi.billing`: `get()` (the balance, auto-reload, the load presets and packs),
37
+ `load(amount_cents)` or `load(pack=...)` (starts a load and returns the URL
38
+ to pay at), `loads()`, `auto_reload(...)` and `portal()`; `phi billing`,
39
+ `phi billing load <dollars>`, `phi billing loads`, `phi billing auto-reload
40
+ --on|--off [--threshold] [--amount] [--cap]` and `phi billing portal`. A
41
+ `Load` carries its `provider_ref`.
42
+ - `Quota` carries `purchased_balance`, `total_available`, `plan` and
43
+ `plan_source`, and `LedgerEntry` its `pool`; `phi quota` prints the plan and
44
+ the purchased balance. An older server that omits them still parses.
45
+ - `Billing.payment_method` (brand, last four and expiry of the saved card) and
46
+ `LedgerEntry.run_name`, `pipeline_type` and `note`, as the server sends them
47
+ from 0.2.1; `phi billing` prints the card. `quota.history(reason=[...])` keeps
48
+ the reasons asked for, and `quota.months()` is the ledger by calendar month.
49
+ - `phi skills list`, `phi skills show <name>` and `phi skills install [name]...
50
+ [--global] [--agent claude-code|cursor|codex|all]`: the agent skills Phi
51
+ publishes from `dynotx/phi-skills`, read from the site's index
52
+ (`https://phi.dynotx.com/skills.json`, or `PHI_SKILLS_INDEX`), installed
53
+ through `npx skills add` when `npx` is on `PATH` and otherwise written as
54
+ `SKILL.md` files into the agents' skill directories. `phi doctor` says how
55
+ many are installed and where.
56
+ - `phi docs sync` publishes a checkout that has no retrieval guard when every
57
+ document declares `access: general` in its frontmatter (`dynotx/phi-skills`);
58
+ a document that does not is named in the refusal.
59
+ - `phi auth login` signs in through the browser: Clerk's OAuth with PKCE and a
60
+ loopback redirect on a random port, the organization chosen at sign-in, the
61
+ access and refresh tokens saved in the profile (`auth = "oauth"`) and
62
+ refreshed when within sixty seconds of expiry or once when the API refuses the
63
+ token; a refresh the server declines ends with "Signed out. Run phi auth
64
+ login." and exit code 1. `--device` is the device authorization grant for a
65
+ machine with no browser; `--key [KEY]` is the key path, non-interactive when
66
+ the key or `PHI_API_KEY` is given. The base URL is `--api`, then the profile's,
67
+ then production; `--interactive` prompts. The CLI's OAuth client id is the
68
+ metadata document the API serves at `/.well-known/phi-cli-client.json`.
69
+ - `phi auth logout` revokes the refresh token at Clerk; `phi auth whoami` and
70
+ `phi auth profiles` say how a profile is authenticated and when its token
71
+ expires. `phi doctor` checks the base URL, the credential, the installed
72
+ version against PyPI and whether another `phi` is first on `PATH`.
73
+ - `PhiClient` and `AsyncPhiClient` take `user_agent=`; the CLI sends
74
+ `phi-cli/<version>`. `PhiSignedOutError` is exported.
75
+
76
+ ### Fixed
77
+
78
+ - `Billing.presets` and `Billing.packs` use independent default lists for each
79
+ model instance.
80
+
81
+ ## [0.3.1] - 2026-10-17
82
+
83
+ ### Changed
84
+
85
+ - The CLI is installed with the package: `pip install dyno-phi` puts `phi` on the
86
+ path. The `[cli]` extra is kept as a no-op alias, so `dyno-phi[cli]` still
87
+ installs.
88
+
89
+ ## [0.3.0] - 2026-10-17
90
+
91
+ The first release of the Phi SDK and CLI on PyPI, published from `clients/phi` in
92
+ `dynotx/phi-api`. It was distributed as `phi-client` 0.0.1 from this repository
93
+ over git during the internal preview.
94
+
95
+ `dyno-phi` 0.2.x on PyPI was an earlier, unrelated client of a retired API. 0.3.0
96
+ replaces it under the same name; nothing from 0.2.x carries over.
97
+
98
+ ### Added
99
+
100
+ - `PhiClient` and `AsyncPhiClient` with the same resources and signatures:
101
+ `projects`, `datasets`, `sequences`, `designs`, `jobs`, `job_types`, `runs`,
102
+ `run_admin`, `artifacts`, `files`, `uploads`, `logs`, `quota`, `tool_configs`,
103
+ `imports`, `documents`.
104
+ - Typed request and response models, local JSON Schema validation of `params` and
105
+ `inputs` for every job type (`client.job_types.validate_params`), waiters for
106
+ jobs and runs, and a typed error taxonomy (`PhiError` and its subclasses).
107
+ - The `phi` CLI behind the `[cli]` extra, every command taking `--json`:
108
+ `auth`, `projects`, `datasets`, `docs`, `models`, `jobs`, `runs`, `pipeline`,
109
+ `artifacts`, `export`, `logs`, `quota`, `use`, `quickstart`.
110
+ - Profiles in `~/.config/phi/config.toml`, selected with `--profile` or
111
+ `PHI_PROFILE`.
112
+ - A Claude Code skill (`skills/phi/SKILL.md`) that drives the CLI.
113
+
114
+ ### Changed from `dyno-phi` 0.2.x
115
+
116
+ - Environment: `PHI_API` is the base URL and `PHI_KEY` or `PHI_API_KEY` the key
117
+ (`DYNO_API_BASE_URL` and `DYNO_API_KEY` are not read). `PHI_ORG` names the
118
+ organization for a static key; a Clerk key carries its own.
119
+ - Command tree: `phi <group> <command>` as listed above. `phi upload`, `phi filter`,
120
+ `phi research`, `phi notes`, `phi tutorial`, the filter presets and the
121
+ per-model commands (`phi esmfold`, `phi alphafold`, ...) are gone; a model runs
122
+ as `phi jobs submit <JOB_TYPE>` and a chain of models as `phi runs submit` or
123
+ `phi pipeline run`. `phi quickstart` is the walkthrough.
124
+ - State: `.phi/state.json` holds the active project and dataset and the last job
125
+ and run, nothing else; the API key is never written there.
126
+ - Python 3.12 or newer. The SDK depends on `httpx` and `pydantic` alone; the CLI's
127
+ dependencies install only with the `[cli]` extra.
128
+ - Web app: results open at `design.dynotx.com`; the dashboard URLs the old client
129
+ printed no longer exist.
130
+
131
+ Versioning note: the client stays below 1.0 while the API it speaks to is itself
132
+ pre-1.0 (0.1.x) and the request shapes can still change between releases. 0.3.0
133
+ is the first release on this line only because 0.2.x was already taken on PyPI
134
+ by the earlier client; it is not a continuation of it.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dyno-phi
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Phi — Python SDK and CLI for the Phi protein design API
5
5
  Project-URL: Homepage, https://design.dynotx.com
6
6
  Project-URL: Documentation, https://design.dynotx.com/docs
@@ -20,21 +20,21 @@ Classifier: Programming Language :: Python :: 3.14
20
20
  Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
21
21
  Classifier: Typing :: Typed
22
22
  Requires-Python: >=3.12
23
+ Requires-Dist: click>=8.1
23
24
  Requires-Dist: httpx>=0.27
24
25
  Requires-Dist: pydantic>=2.9
26
+ Requires-Dist: pyyaml>=6
27
+ Requires-Dist: rich>=13
28
+ Requires-Dist: typer>=0.12
25
29
  Provides-Extra: cli
26
- Requires-Dist: click>=8.1; extra == 'cli'
27
- Requires-Dist: pyyaml>=6; extra == 'cli'
28
- Requires-Dist: rich>=13; extra == 'cli'
29
- Requires-Dist: typer>=0.12; extra == 'cli'
30
30
  Description-Content-Type: text/markdown
31
31
 
32
32
  # dyno-phi
33
33
 
34
34
  Python SDK and CLI for the [Phi](https://design.dynotx.com) protein design API.
35
35
  The `phi` package exposes synchronous (`PhiClient`) and asynchronous
36
- (`AsyncPhiClient`) clients; the CLI ships behind the `[cli]` extra so the SDK
37
- stays light. It is published to PyPI from `clients/phi` in
36
+ (`AsyncPhiClient`) clients and the `phi` CLI, installed together. It is
37
+ published to PyPI from `clients/phi` in
38
38
  [`dynotx/phi-api`](https://github.com/dynotx/phi-api).
39
39
 
40
40
  ## Install
@@ -42,16 +42,17 @@ stays light. It is published to PyPI from `clients/phi` in
42
42
  The CLI as an isolated tool on your `PATH`:
43
43
 
44
44
  ```bash
45
- uv tool install "dyno-phi[cli]"
45
+ uv tool install dyno-phi
46
46
  ```
47
47
 
48
48
  `uv tool` provisions its own Python, which covers the client's **Python 3.12+**
49
49
  requirement on stock macOS, where the system Python is 3.9. Re-run with
50
- `--upgrade` to upgrade. `pip install "dyno-phi[cli]"` into a Python 3.12+ of
51
- your own does the same. From a `phi-api` checkout, `just install-cli` runs the
52
- first form and `just install-cli-dev` installs the working tree.
50
+ `--upgrade` to upgrade. `pip install dyno-phi` into a Python 3.12+ of your own
51
+ does the same. From a `phi-api` checkout, `just install-cli` runs the first
52
+ form and `just install-cli-dev` installs the working tree.
53
53
 
54
- The SDK inside an existing project, without the CLI's dependencies:
54
+ The SDK inside an existing project (the CLI comes with it; the `[cli]` extra
55
+ from 0.3.0 still installs and adds nothing):
55
56
 
56
57
  ```bash
57
58
  uv add dyno-phi # or: pip install dyno-phi
@@ -60,7 +61,7 @@ uv add dyno-phi # or: pip install dyno-phi
60
61
  Check the install with `phi --version`. `dyno-phi` 0.2.x was an earlier,
61
62
  unrelated client of a retired API; if `phi --version` prints `0.2.x`, that
62
63
  client is first on your `PATH` (`which -a phi` lists every match) and
63
- `pip install --upgrade "dyno-phi[cli]"` in the Python that owns it replaces it.
64
+ `pip install --upgrade dyno-phi` in the Python that owns it replaces it.
64
65
  Coming from 0.2.x: the key is `PHI_API_KEY` (or `PHI_KEY`) and the base URL
65
66
  `PHI_API`, the commands are `phi <group> <command>` as listed under
66
67
  [CLI](#cli), and Python 3.12+ is required; see
@@ -79,13 +80,31 @@ path=("$HOME/.local/bin" $path)
79
80
 
80
81
  ## Authentication
81
82
 
82
- Both the SDK and the CLI read connection settings from the environment, falling
83
- back to `~/.config/phi/config.toml`:
83
+ Sign in through your browser:
84
+
85
+ ```bash
86
+ phi auth login
87
+ ```
88
+
89
+ It opens Dyno's sign-in, you pick the organization, and the CLI saves the sign-in
90
+ to `~/.config/phi/config.toml`, refreshing it as it runs out. On a machine with
91
+ no browser, `phi auth login --device` prints a URL and a code to enter on another
92
+ device. `phi auth whoami` prints who you are and which organization and
93
+ environment you are on; `phi doctor` checks the install when something is off.
94
+
95
+ An agent, CI or a service account uses an API key from
96
+ [design.dynotx.com](https://design.dynotx.com) instead, either saved as a profile
97
+ or read from the environment:
98
+
99
+ ```bash
100
+ phi auth login --key ak_... # saved to the profile
101
+ export PHI_API_KEY=ak_... # or per process
102
+ ```
84
103
 
85
104
  | Variable | Purpose |
86
105
  |---|---|
87
- | `PHI_API` | Base URL of the API. Its host selects the environment class — `hosted`, `staging`, or `local`. |
88
- | `PHI_KEY` / `PHI_API_KEY` | API key. |
106
+ | `PHI_API` | Base URL of the API. Its host selects the environment class: `hosted`, `staging`, or `local`. |
107
+ | `PHI_KEY` / `PHI_API_KEY` | API key. Wins over a stored sign-in. |
89
108
  | `PHI_ORG` | Organization ID. Required with a static key. |
90
109
 
91
110
  ```bash
@@ -98,7 +117,8 @@ With a Clerk key (`ak_...`) the tenant comes from the key, so `PHI_ORG` is
98
117
  ignored; it is required only for a static key.
99
118
 
100
119
  Pass `base_url=`, `api_key=`, and `org=` to the client constructor to override
101
- the environment.
120
+ the environment. `api_key=` takes a sign-in's access token as readily as a key.
121
+ The three ways and when to use each: [`docs/sdk-and-cli.md`](https://github.com/dynotx/phi-api/blob/main/docs/sdk-and-cli.md#authentication).
102
122
 
103
123
  ## 60-second quickstart
104
124
 
@@ -180,7 +200,7 @@ Every command reads the same environment and accepts `--json` for scripting.
180
200
 
181
201
  | Group | Commands |
182
202
  |---|---|
183
- | `auth` | `login`, `whoami`, `logout`, `profiles` |
203
+ | `auth` | `login [--device] [--key [KEY]]`, `whoami`, `logout`, `profiles` |
184
204
  | `projects` | `list`, `create`, `show`, `update`, `archive` |
185
205
  | `datasets` | `list`, `create`, `show`, `update`, `delete`, `copy`, `export`, `sequences` |
186
206
  | `docs` | `list`, `read`, `search`, `add`, `update`, `history`, `propose`, `activate`, `sync` |
@@ -192,8 +212,10 @@ Every command reads the same environment and accepts `--json` for scripting.
192
212
  | `export` | `sequences`, `designs`, `structure`, `pae` |
193
213
  | `logs` | `show`, `download`, `settings` |
194
214
  | `quota` | (summary), `history` |
215
+ | `billing` | (summary), `load <dollars>`, `loads`, `auto-reload`, `portal` |
195
216
  | `use` | Set the active project |
196
217
  | `quickstart` | Run the end-to-end ESMFOLD example |
218
+ | `doctor` | Check the base URL, the credential, the version and the `PATH` |
197
219
 
198
220
  `phi docs sync <dir> --dry-run` safely plans publication from a clean knowledge
199
221
  checkout. Without `--dry-run` it publishes only Git-tracked files in the
@@ -2,8 +2,8 @@
2
2
 
3
3
  Python SDK and CLI for the [Phi](https://design.dynotx.com) protein design API.
4
4
  The `phi` package exposes synchronous (`PhiClient`) and asynchronous
5
- (`AsyncPhiClient`) clients; the CLI ships behind the `[cli]` extra so the SDK
6
- stays light. It is published to PyPI from `clients/phi` in
5
+ (`AsyncPhiClient`) clients and the `phi` CLI, installed together. It is
6
+ published to PyPI from `clients/phi` in
7
7
  [`dynotx/phi-api`](https://github.com/dynotx/phi-api).
8
8
 
9
9
  ## Install
@@ -11,16 +11,17 @@ stays light. It is published to PyPI from `clients/phi` in
11
11
  The CLI as an isolated tool on your `PATH`:
12
12
 
13
13
  ```bash
14
- uv tool install "dyno-phi[cli]"
14
+ uv tool install dyno-phi
15
15
  ```
16
16
 
17
17
  `uv tool` provisions its own Python, which covers the client's **Python 3.12+**
18
18
  requirement on stock macOS, where the system Python is 3.9. Re-run with
19
- `--upgrade` to upgrade. `pip install "dyno-phi[cli]"` into a Python 3.12+ of
20
- your own does the same. From a `phi-api` checkout, `just install-cli` runs the
21
- first form and `just install-cli-dev` installs the working tree.
19
+ `--upgrade` to upgrade. `pip install dyno-phi` into a Python 3.12+ of your own
20
+ does the same. From a `phi-api` checkout, `just install-cli` runs the first
21
+ form and `just install-cli-dev` installs the working tree.
22
22
 
23
- The SDK inside an existing project, without the CLI's dependencies:
23
+ The SDK inside an existing project (the CLI comes with it; the `[cli]` extra
24
+ from 0.3.0 still installs and adds nothing):
24
25
 
25
26
  ```bash
26
27
  uv add dyno-phi # or: pip install dyno-phi
@@ -29,7 +30,7 @@ uv add dyno-phi # or: pip install dyno-phi
29
30
  Check the install with `phi --version`. `dyno-phi` 0.2.x was an earlier,
30
31
  unrelated client of a retired API; if `phi --version` prints `0.2.x`, that
31
32
  client is first on your `PATH` (`which -a phi` lists every match) and
32
- `pip install --upgrade "dyno-phi[cli]"` in the Python that owns it replaces it.
33
+ `pip install --upgrade dyno-phi` in the Python that owns it replaces it.
33
34
  Coming from 0.2.x: the key is `PHI_API_KEY` (or `PHI_KEY`) and the base URL
34
35
  `PHI_API`, the commands are `phi <group> <command>` as listed under
35
36
  [CLI](#cli), and Python 3.12+ is required; see
@@ -48,13 +49,31 @@ path=("$HOME/.local/bin" $path)
48
49
 
49
50
  ## Authentication
50
51
 
51
- Both the SDK and the CLI read connection settings from the environment, falling
52
- back to `~/.config/phi/config.toml`:
52
+ Sign in through your browser:
53
+
54
+ ```bash
55
+ phi auth login
56
+ ```
57
+
58
+ It opens Dyno's sign-in, you pick the organization, and the CLI saves the sign-in
59
+ to `~/.config/phi/config.toml`, refreshing it as it runs out. On a machine with
60
+ no browser, `phi auth login --device` prints a URL and a code to enter on another
61
+ device. `phi auth whoami` prints who you are and which organization and
62
+ environment you are on; `phi doctor` checks the install when something is off.
63
+
64
+ An agent, CI or a service account uses an API key from
65
+ [design.dynotx.com](https://design.dynotx.com) instead, either saved as a profile
66
+ or read from the environment:
67
+
68
+ ```bash
69
+ phi auth login --key ak_... # saved to the profile
70
+ export PHI_API_KEY=ak_... # or per process
71
+ ```
53
72
 
54
73
  | Variable | Purpose |
55
74
  |---|---|
56
- | `PHI_API` | Base URL of the API. Its host selects the environment class — `hosted`, `staging`, or `local`. |
57
- | `PHI_KEY` / `PHI_API_KEY` | API key. |
75
+ | `PHI_API` | Base URL of the API. Its host selects the environment class: `hosted`, `staging`, or `local`. |
76
+ | `PHI_KEY` / `PHI_API_KEY` | API key. Wins over a stored sign-in. |
58
77
  | `PHI_ORG` | Organization ID. Required with a static key. |
59
78
 
60
79
  ```bash
@@ -67,7 +86,8 @@ With a Clerk key (`ak_...`) the tenant comes from the key, so `PHI_ORG` is
67
86
  ignored; it is required only for a static key.
68
87
 
69
88
  Pass `base_url=`, `api_key=`, and `org=` to the client constructor to override
70
- the environment.
89
+ the environment. `api_key=` takes a sign-in's access token as readily as a key.
90
+ The three ways and when to use each: [`docs/sdk-and-cli.md`](https://github.com/dynotx/phi-api/blob/main/docs/sdk-and-cli.md#authentication).
71
91
 
72
92
  ## 60-second quickstart
73
93
 
@@ -149,7 +169,7 @@ Every command reads the same environment and accepts `--json` for scripting.
149
169
 
150
170
  | Group | Commands |
151
171
  |---|---|
152
- | `auth` | `login`, `whoami`, `logout`, `profiles` |
172
+ | `auth` | `login [--device] [--key [KEY]]`, `whoami`, `logout`, `profiles` |
153
173
  | `projects` | `list`, `create`, `show`, `update`, `archive` |
154
174
  | `datasets` | `list`, `create`, `show`, `update`, `delete`, `copy`, `export`, `sequences` |
155
175
  | `docs` | `list`, `read`, `search`, `add`, `update`, `history`, `propose`, `activate`, `sync` |
@@ -161,8 +181,10 @@ Every command reads the same environment and accepts `--json` for scripting.
161
181
  | `export` | `sequences`, `designs`, `structure`, `pae` |
162
182
  | `logs` | `show`, `download`, `settings` |
163
183
  | `quota` | (summary), `history` |
184
+ | `billing` | (summary), `load <dollars>`, `loads`, `auto-reload`, `portal` |
164
185
  | `use` | Set the active project |
165
186
  | `quickstart` | Run the end-to-end ESMFOLD example |
187
+ | `doctor` | Check the base URL, the credential, the version and the `PATH` |
166
188
 
167
189
  `phi docs sync <dir> --dry-run` safely plans publication from a clean knowledge
168
190
  checkout. Without `--dry-run` it publishes only Git-tracked files in the
@@ -33,10 +33,6 @@ classifiers = [
33
33
  dependencies = [
34
34
  "httpx>=0.27",
35
35
  "pydantic>=2.9",
36
- ]
37
-
38
- [project.optional-dependencies]
39
- cli = [
40
36
  "typer>=0.12",
41
37
  # Typer vendors Click from 0.27 on, so it is no longer a transitive
42
38
  # dependency. The CLI imports click directly for UsageError and confirm.
@@ -45,6 +41,10 @@ cli = [
45
41
  "pyyaml>=6",
46
42
  ]
47
43
 
44
+ [project.optional-dependencies]
45
+ # The CLI ships with the package since 0.3.1; `dyno-phi[cli]` stays installable.
46
+ cli = []
47
+
48
48
  [project.scripts]
49
49
  phi = "phi.cli:main"
50
50
 
@@ -57,7 +57,7 @@ Issues = "https://github.com/dynotx/phi-api/issues"
57
57
 
58
58
  [dependency-groups]
59
59
  dev = [
60
- "dyno-phi[cli]",
60
+ "dyno-phi",
61
61
  "pytest>=8.3.0",
62
62
  "pytest-asyncio>=0.24.0",
63
63
  "respx>=0.21.0",
@@ -18,6 +18,7 @@ from .errors import (
18
18
  )
19
19
  from .graph import Node, PipelineGraph, PortRef
20
20
  from .models import JobTypeSchema
21
+ from .oauth import PhiSignedOutError
21
22
 
22
23
  __all__ = [
23
24
  "AsyncPhiClient",
@@ -37,5 +38,6 @@ __all__ = [
37
38
  "PhiInsufficientCreditsError",
38
39
  "PhiRateLimitError",
39
40
  "PhiServerError",
41
+ "PhiSignedOutError",
40
42
  "__version__",
41
43
  ]
@@ -0,0 +1 @@
1
+ __version__ = "0.3.2"
@@ -13,8 +13,11 @@ from ._transport import (
13
13
  async_send_with_retry,
14
14
  parse_response,
15
15
  )
16
+ from ._version import __version__
16
17
  from .config import Profile
17
18
  from .resources.artifacts import AsyncArtifactsResource
19
+ from .resources.audit import AsyncAuditResource
20
+ from .resources.billing import AsyncBillingResource
18
21
  from .resources.datasets import AsyncDatasetsResource
19
22
  from .resources.designs import AsyncDesignsResource
20
23
  from .resources.documents import AsyncDocumentsResource
@@ -43,16 +46,24 @@ class AsyncPhiClient:
43
46
  timeout: float = 30.0,
44
47
  max_retries: int = 3,
45
48
  http_client: httpx.AsyncClient | None = None,
49
+ user_agent: str | None = None,
46
50
  ) -> None:
51
+ """`api_key` is an API key or the access token of a sign-in; with neither, the
52
+ profile's stored credential is used and refreshed as it runs out."""
47
53
  self.profile = Profile(base_url=base_url, api_key=api_key, org=org, profile=profile)
48
54
  self.retry_policy = RetryPolicy(max_retries=max_retries)
49
55
  self._http = http_client or httpx.AsyncClient(
50
56
  base_url=self.profile.base_url,
51
- headers=self.profile.auth_headers(),
57
+ headers={
58
+ **self.profile.auth_headers(),
59
+ "User-Agent": user_agent or f"dyno-phi/{__version__}",
60
+ },
52
61
  timeout=timeout,
53
62
  )
54
63
 
55
64
  self.quota = AsyncQuotaResource(self)
65
+ self.audit = AsyncAuditResource(self)
66
+ self.billing = AsyncBillingResource(self)
56
67
  self.projects = AsyncProjectsResource(self)
57
68
  self.datasets = AsyncDatasetsResource(self)
58
69
  self.jobs = AsyncJobsResource(self)
@@ -78,7 +89,15 @@ class AsyncPhiClient:
78
89
  return body
79
90
 
80
91
  async def send_with_headers(self, spec: RequestSpec) -> tuple[Any, httpx.Headers]:
81
- response = await async_send_with_retry(
92
+ await self._refresh_if_expiring()
93
+ response = await self._send(spec)
94
+ # A token the server refuses is refreshed once; a second refusal is the answer.
95
+ if response.status_code == 401 and await self._refresh_token():
96
+ response = await self._send(spec)
97
+ return parse_response(response, method=spec.method, path=spec.path), response.headers
98
+
99
+ async def _send(self, spec: RequestSpec) -> httpx.Response:
100
+ return await async_send_with_retry(
82
101
  lambda: self._http.request(
83
102
  spec.method,
84
103
  spec.path,
@@ -90,10 +109,23 @@ class AsyncPhiClient:
90
109
  path=spec.path,
91
110
  policy=self.retry_policy,
92
111
  )
93
- return parse_response(response, method=spec.method, path=spec.path), response.headers
94
112
 
95
- def stream_lines(self, spec: RequestSpec) -> AsyncIterator[str]:
96
- return async_iter_lines_with_retry(
113
+ async def _refresh_if_expiring(self) -> None:
114
+ if self.profile.needs_refresh():
115
+ await self._refresh_token()
116
+
117
+ async def _refresh_token(self) -> bool:
118
+ """Refresh a stored sign-in and carry the new token on every later request."""
119
+ if not await self.profile.arefresh():
120
+ return False
121
+ self._http.headers["X-API-Key"] = self.profile.api_key
122
+ return True
123
+
124
+ async def stream_lines(self, spec: RequestSpec) -> AsyncIterator[str]:
125
+ # The refresh is the iterator's first step, so it is awaited on the loop
126
+ # rather than run blocking before the iterator is handed back.
127
+ await self._refresh_if_expiring()
128
+ async for line in async_iter_lines_with_retry(
97
129
  lambda: self._http.stream(
98
130
  spec.method,
99
131
  spec.path,
@@ -104,7 +136,8 @@ class AsyncPhiClient:
104
136
  method=spec.method,
105
137
  path=spec.path,
106
138
  policy=self.retry_policy,
107
- )
139
+ ):
140
+ yield line
108
141
 
109
142
  async def aclose(self) -> None:
110
143
  await self._http.aclose()
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from datetime import datetime
5
+ from enum import StrEnum
6
+ from pathlib import Path
7
+
8
+ import typer
9
+
10
+ from .. import render
11
+ from ..state import CliState
12
+
13
+ app = typer.Typer(no_args_is_help=True, help="Inspect and export organization audit events.")
14
+
15
+
16
+ class AuditFormat(StrEnum):
17
+ csv = "csv"
18
+ jsonl = "jsonl"
19
+
20
+
21
+ def _datetime(value: str | None, option: str) -> datetime | None:
22
+ if value is None:
23
+ return None
24
+ return _required_datetime(value, option)
25
+
26
+
27
+ def _required_datetime(value: str, option: str) -> datetime:
28
+ try:
29
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
30
+ except ValueError as exc:
31
+ raise typer.BadParameter("must be an ISO 8601 timestamp", param_hint=option) from exc
32
+ if parsed.tzinfo is None or parsed.utcoffset() is None:
33
+ raise typer.BadParameter("must include a timezone", param_hint=option)
34
+ return parsed
35
+
36
+
37
+ @app.command("list")
38
+ def list_events(
39
+ ctx: typer.Context,
40
+ limit: int = typer.Option(50, "--limit", min=1, max=200),
41
+ from_: str | None = typer.Option(None, "--from"),
42
+ to: str | None = typer.Option(None, "--to"),
43
+ action: list[str] | None = typer.Option(None, "--action"),
44
+ category: list[str] | None = typer.Option(None, "--category"),
45
+ actor: str | None = typer.Option(None, "--actor"),
46
+ subject_type: str | None = typer.Option(None, "--subject-type"),
47
+ subject_id: str | None = typer.Option(None, "--subject-id"),
48
+ source: list[str] | None = typer.Option(None, "--source"),
49
+ outcome: list[str] | None = typer.Option(None, "--outcome"),
50
+ text: str | None = typer.Option(None, "--text"),
51
+ ) -> None:
52
+ state: CliState = ctx.obj
53
+ events = state.client.audit.list(
54
+ page_size=limit,
55
+ from_=_datetime(from_, "--from"),
56
+ to=_datetime(to, "--to"),
57
+ action=action,
58
+ category=category,
59
+ actor=actor,
60
+ subject_type=subject_type,
61
+ subject_id=subject_id,
62
+ source=source,
63
+ outcome=outcome,
64
+ text=text,
65
+ )
66
+ if state.json_output:
67
+ render.print_json([event.model_dump(mode="json") for event in events])
68
+ return
69
+ render.table(
70
+ ["id", "occurred_at", "action", "actor", "subject", "outcome", "source"],
71
+ [
72
+ (
73
+ event.id,
74
+ event.occurred_at.isoformat(),
75
+ event.action,
76
+ event.actor.label,
77
+ event.subject.label or event.subject.id or event.subject.type,
78
+ event.outcome,
79
+ event.source,
80
+ )
81
+ for event in events
82
+ ],
83
+ title="audit events",
84
+ no_color=state.no_color,
85
+ )
86
+
87
+
88
+ @app.command("show")
89
+ def show_event(
90
+ ctx: typer.Context,
91
+ event_id: int = typer.Argument(..., min=1),
92
+ ) -> None:
93
+ state: CliState = ctx.obj
94
+ render.model(state, state.client.audit.show(event_id), title="audit event")
95
+
96
+
97
+ @app.command("export")
98
+ def export_events(
99
+ ctx: typer.Context,
100
+ from_: str = typer.Option(..., "--from"),
101
+ to: str = typer.Option(..., "--to"),
102
+ format: AuditFormat = typer.Option(AuditFormat.csv, "--format"),
103
+ out: str = typer.Option("-", "--out", help="Output file, or - for stdout."),
104
+ action: list[str] | None = typer.Option(None, "--action"),
105
+ category: list[str] | None = typer.Option(None, "--category"),
106
+ actor: str | None = typer.Option(None, "--actor"),
107
+ subject_type: str | None = typer.Option(None, "--subject-type"),
108
+ subject_id: str | None = typer.Option(None, "--subject-id"),
109
+ source: list[str] | None = typer.Option(None, "--source"),
110
+ outcome: list[str] | None = typer.Option(None, "--outcome"),
111
+ text: str | None = typer.Option(None, "--text"),
112
+ ) -> None:
113
+ state: CliState = ctx.obj
114
+ lines = state.client.audit.export(
115
+ format=format.value,
116
+ from_=_required_datetime(from_, "--from"),
117
+ to=_required_datetime(to, "--to"),
118
+ action=action,
119
+ category=category,
120
+ actor=actor,
121
+ subject_type=subject_type,
122
+ subject_id=subject_id,
123
+ source=source,
124
+ outcome=outcome,
125
+ text=text,
126
+ )
127
+ if out == "-":
128
+ for line in lines:
129
+ sys.stdout.write(line + "\n")
130
+ return
131
+ with Path(out).open("w") as handle:
132
+ for line in lines:
133
+ handle.write(line + "\n")