pdt-cli 0.1.4__tar.gz → 0.1.7__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 (202) hide show
  1. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/.gitignore +1 -0
  2. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/PKG-INFO +90 -26
  3. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/README.md +89 -25
  4. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/ci/tests/test_score_task.py +2 -9
  5. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/pyproject.toml +2 -2
  6. pdt_cli-0.1.7/src/pdt/__init__.py +9 -0
  7. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/cli.py +246 -58
  8. pdt_cli-0.1.7/src/pdt/completion.py +255 -0
  9. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/config.py +253 -19
  10. pdt_cli-0.1.7/src/pdt/console.py +334 -0
  11. pdt_cli-0.1.7/src/pdt/deploy.py +357 -0
  12. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/deploy_aws.py +85 -41
  13. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/deploy_aws_batch.py +253 -106
  14. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/deploy_azure.py +166 -96
  15. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/deploy_azure_container_apps.py +379 -156
  16. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/deploy_common.py +161 -39
  17. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/deploy_google_cloud.py +245 -124
  18. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/deploy_windows.py +95 -37
  19. pdt_cli-0.1.7/src/pdt/docker_setup.py +598 -0
  20. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/download.py +15 -10
  21. pdt_cli-0.1.7/src/pdt/duckdb_wasm.py +100 -0
  22. pdt_cli-0.1.7/src/pdt/examples/azure-license-waste/LicenseWasteAudit.ps1 +233 -0
  23. pdt_cli-0.1.7/src/pdt/examples/azure-license-waste/config.yml +21 -0
  24. pdt_cli-0.1.7/src/pdt/examples/azure-license-waste/env.template +14 -0
  25. pdt_cli-0.1.7/src/pdt/examples/azure-license-waste/license-prices.csv +33 -0
  26. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/examples/hello-world/run.py +5 -5
  27. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/examples/impossible-travel-report/run.py +4 -4
  28. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/examples/monday-orphaned-account-report/run.py +4 -4
  29. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/gcloud_sdk.py +4 -4
  30. pdt_cli-0.1.7/src/pdt/gui/__init__.py +12 -0
  31. pdt_cli-0.1.7/src/pdt/gui/apps.py +6 -0
  32. pdt_cli-0.1.7/src/pdt/gui/health.py +580 -0
  33. pdt_cli-0.1.7/src/pdt/gui/migrations/0001_initial.py +81 -0
  34. pdt_cli-0.1.7/src/pdt/gui/migrations/0002_timing_and_artifacts_by_time.py +33 -0
  35. pdt_cli-0.1.7/src/pdt/gui/migrations/0003_app_history_synced_at.py +18 -0
  36. pdt_cli-0.1.7/src/pdt/gui/migrations/0004_reread_time_matched_files.py +22 -0
  37. pdt_cli-0.1.7/src/pdt/gui/models.py +90 -0
  38. pdt_cli-0.1.7/src/pdt/gui/pdt_cmd.py +55 -0
  39. pdt_cli-0.1.7/src/pdt/gui/settings.py +63 -0
  40. pdt_cli-0.1.7/src/pdt/gui/static/arrow.mjs +4 -0
  41. pdt_cli-0.1.7/src/pdt/gui/static/pdt.css +175 -0
  42. pdt_cli-0.1.7/src/pdt/gui/static/workbench.js +284 -0
  43. pdt_cli-0.1.7/src/pdt/gui/sync.py +230 -0
  44. pdt_cli-0.1.7/src/pdt/gui/templates/gui/_actions.html +24 -0
  45. pdt_cli-0.1.7/src/pdt/gui/templates/gui/_health.html +319 -0
  46. pdt_cli-0.1.7/src/pdt/gui/templates/gui/_health_grid.html +57 -0
  47. pdt_cli-0.1.7/src/pdt/gui/templates/gui/app.html +50 -0
  48. pdt_cli-0.1.7/src/pdt/gui/templates/gui/apps.html +39 -0
  49. pdt_cli-0.1.7/src/pdt/gui/templates/gui/base.html +40 -0
  50. pdt_cli-0.1.7/src/pdt/gui/templates/gui/explore.html +57 -0
  51. pdt_cli-0.1.7/src/pdt/gui/templates/gui/run.html +78 -0
  52. pdt_cli-0.1.7/src/pdt/gui/templates/gui/stats.html +79 -0
  53. pdt_cli-0.1.7/src/pdt/gui/timing.py +46 -0
  54. pdt_cli-0.1.7/src/pdt/gui/urls.py +17 -0
  55. pdt_cli-0.1.7/src/pdt/gui/views.py +293 -0
  56. pdt_cli-0.1.7/src/pdt/gui/worker.py +117 -0
  57. pdt_cli-0.1.7/src/pdt/gui_cli.py +162 -0
  58. pdt_cli-0.1.7/src/pdt/gui_server.py +68 -0
  59. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/powershell.py +196 -21
  60. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/pwsh.py +2 -2
  61. pdt_cli-0.1.7/src/pdt/python_env.py +98 -0
  62. pdt_cli-0.1.7/src/pdt/regions.py +412 -0
  63. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/run_powershell.py +5 -0
  64. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/run_windows_task.py +11 -5
  65. pdt_cli-0.1.7/src/pdt/runs_cli.py +657 -0
  66. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/scaffold.py +51 -40
  67. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/scan_powershell.ps1 +11 -3
  68. pdt_cli-0.1.7/src/pdt/storage_cli.py +133 -0
  69. pdt_cli-0.1.7/src/pdt/utils/__init__.py +0 -0
  70. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/utils/email_auth.py +7 -6
  71. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/utils/send_email_graph.py +4 -4
  72. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/utils/storage.py +202 -21
  73. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/conftest.py +15 -0
  74. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_aws_cli.py +12 -0
  75. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_aws_cost_estimate.py +1 -1
  76. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_aws_deployer_policy.py +19 -2
  77. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_aws_destroy.py +21 -16
  78. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_aws_inventory.py +9 -0
  79. pdt_cli-0.1.7/tests/test_aws_pause.py +118 -0
  80. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_aws_runs.py +13 -1
  81. pdt_cli-0.1.7/tests/test_aws_security_group.py +149 -0
  82. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_aws_shared_deletes.py +67 -4
  83. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_azure_build.py +3 -18
  84. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_azure_environment.py +24 -17
  85. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_azure_job_diagnostics.py +2 -1
  86. pdt_cli-0.1.7/tests/test_azure_pause.py +63 -0
  87. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_azure_retries.py +49 -12
  88. pdt_cli-0.1.7/tests/test_azure_runs.py +352 -0
  89. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_azure_shared_locks.py +10 -8
  90. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_azure_storage.py +2 -1
  91. pdt_cli-0.1.7/tests/test_build_ssh.py +82 -0
  92. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_cli.py +281 -11
  93. pdt_cli-0.1.7/tests/test_command_mentions.py +165 -0
  94. pdt_cli-0.1.7/tests/test_completion.py +466 -0
  95. pdt_cli-0.1.7/tests/test_console.py +74 -0
  96. pdt_cli-0.1.7/tests/test_console_values.py +127 -0
  97. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_cost_estimate.py +1 -1
  98. pdt_cli-0.1.7/tests/test_currency.py +211 -0
  99. pdt_cli-0.1.7/tests/test_deploy_first_run.py +217 -0
  100. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_deploy_state.py +15 -1
  101. pdt_cli-0.1.7/tests/test_discovery_lines.py +220 -0
  102. pdt_cli-0.1.7/tests/test_docker_preflight.py +921 -0
  103. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_dockerfile.py +9 -4
  104. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_download.py +2 -1
  105. pdt_cli-0.1.7/tests/test_duckdb_wasm.py +58 -0
  106. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_env_sources.py +38 -0
  107. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_gcloud_sdk.py +2 -1
  108. pdt_cli-0.1.7/tests/test_google_cloud_build.py +56 -0
  109. pdt_cli-0.1.7/tests/test_google_cloud_pause.py +64 -0
  110. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_google_cloud_runs.py +41 -2
  111. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_google_cloud_storage.py +2 -1
  112. pdt_cli-0.1.7/tests/test_gui.py +525 -0
  113. pdt_cli-0.1.7/tests/test_gui_cli.py +104 -0
  114. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_init_questions.py +2 -2
  115. pdt_cli-0.1.7/tests/test_install_notice.py +35 -0
  116. pdt_cli-0.1.7/tests/test_log_delay.py +38 -0
  117. pdt_cli-0.1.7/tests/test_next_steps.py +122 -0
  118. pdt_cli-0.1.7/tests/test_pause_key.py +61 -0
  119. pdt_cli-0.1.7/tests/test_powershell_env.py +152 -0
  120. pdt_cli-0.1.7/tests/test_powershell_local_modules.py +156 -0
  121. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_powershell_rules.py +92 -12
  122. pdt_cli-0.1.7/tests/test_python_env.py +158 -0
  123. pdt_cli-0.1.7/tests/test_regions.py +175 -0
  124. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_run_log_links.py +3 -3
  125. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_run_powershell.py +17 -1
  126. pdt_cli-0.1.7/tests/test_runs_cli.py +750 -0
  127. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_scaffold.py +18 -0
  128. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_schedule.py +24 -1
  129. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_set_app_enabled.py +46 -1
  130. pdt_cli-0.1.7/tests/test_sign_in_status.py +25 -0
  131. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_starter_app.py +2 -2
  132. pdt_cli-0.1.7/tests/test_storage.py +348 -0
  133. pdt_cli-0.1.7/tests/test_storage_cli.py +98 -0
  134. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_windows_data.py +8 -7
  135. pdt_cli-0.1.7/tests/test_windows_pause.py +77 -0
  136. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_windows_runner.py +12 -0
  137. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_windows_runs.py +2 -2
  138. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_winget_launcher.py +3 -1
  139. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/verify/README.md +8 -4
  140. pdt_cli-0.1.4/src/pdt/__init__.py +0 -6
  141. pdt_cli-0.1.4/src/pdt/completion.py +0 -161
  142. pdt_cli-0.1.4/src/pdt/console.py +0 -195
  143. pdt_cli-0.1.4/src/pdt/deploy.py +0 -217
  144. pdt_cli-0.1.4/src/pdt/runs_cli.py +0 -349
  145. pdt_cli-0.1.4/src/pdt/storage_cli.py +0 -94
  146. pdt_cli-0.1.4/tests/test_azure_runs.py +0 -117
  147. pdt_cli-0.1.4/tests/test_completion.py +0 -219
  148. pdt_cli-0.1.4/tests/test_console.py +0 -35
  149. pdt_cli-0.1.4/tests/test_runs_cli.py +0 -387
  150. pdt_cli-0.1.4/tests/test_storage.py +0 -149
  151. pdt_cli-0.1.4/tests/test_storage_cli.py +0 -40
  152. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/LICENSE +0 -0
  153. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/ci/tests/test_claude_task.py +0 -0
  154. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/ci/tests/test_close_stale_drafts.py +0 -0
  155. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/examples/hello-world/config.yml +0 -0
  156. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/examples/impossible-travel-report/config.yml +0 -0
  157. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/examples/impossible-travel-report/env.template +0 -0
  158. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/examples/monday-orphaned-account-report/config.yml +0 -0
  159. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/examples/monday-orphaned-account-report/env.template +0 -0
  160. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/examples/powershell-report/config.yml +0 -0
  161. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/examples/powershell-report/report.ps1 +0 -0
  162. {pdt_cli-0.1.4/src/pdt/utils → pdt_cli-0.1.7/src/pdt/gui/migrations}/__init__.py +0 -0
  163. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/utils/entra.py +0 -0
  164. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/utils/env_secret.py +0 -0
  165. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/utils/log.py +0 -0
  166. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/utils/send_email.py +0 -0
  167. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/src/pdt/utils/web.py +0 -0
  168. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_app_name.py +0 -0
  169. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_aws_account.py +0 -0
  170. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_aws_job_definition.py +0 -0
  171. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_aws_names.py +0 -0
  172. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_aws_not_found.py +0 -0
  173. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_aws_profile.py +0 -0
  174. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_aws_storage.py +0 -0
  175. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_azure_deleted_vault.py +0 -0
  176. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_azure_environment_key.py +0 -0
  177. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_azure_group_ownership.py +0 -0
  178. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_azure_job_name.py +0 -0
  179. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_azure_names.py +0 -0
  180. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_azure_side_resources.py +0 -0
  181. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_azure_subscription_state.py +0 -0
  182. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_build_context.py +0 -0
  183. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_ci_azure.py +0 -0
  184. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_ci_google_cloud.py +0 -0
  185. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_config_merge.py +0 -0
  186. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_env_secret.py +0 -0
  187. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_fetch_json.py +0 -0
  188. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_google_cloud_inventory.py +0 -0
  189. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_google_cloud_not_found.py +0 -0
  190. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_google_cloud_retries.py +0 -0
  191. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_google_cloud_service_account.py +0 -0
  192. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_log_colour.py +0 -0
  193. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_powershell_app_config.py +0 -0
  194. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_private_file.py +0 -0
  195. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_project_discovery.py +0 -0
  196. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_pwsh.py +0 -0
  197. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_save_platform_key.py +0 -0
  198. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_secrets.py +0 -0
  199. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_strict_validation.py +0 -0
  200. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_verify.py +0 -0
  201. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_windows_storage.py +0 -0
  202. {pdt_cli-0.1.4 → pdt_cli-0.1.7}/tests/test_winget_manifests.py +0 -0
@@ -23,3 +23,4 @@ winget/rendered/
23
23
  .vscode/
24
24
  .idea/
25
25
  .worktrees
26
+ verify/*/pdt_cli-*.whl
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pdt-cli
3
- Version: 0.1.4
3
+ Version: 0.1.7
4
4
  Summary: Easily deploy your process to AWS, Azure, Google Cloud, or Windows.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -38,17 +38,31 @@ Run scheduled jobs — reports, integrations, automations — and deploy them to
38
38
 
39
39
  ```
40
40
  uv tool install pdt-cli
41
+ uv tool update-shell
41
42
  ```
42
43
 
43
- That puts a `pdt` command on your PATH. To update it later, run `uv tool upgrade pdt-cli`.
44
+ `uv tool install` puts the `pdt` command in uv's tool folder. `uv tool update-shell` adds that folder to your PATH if it is not there yet. A terminal window that was already open does not see a PATH change, so open a new window before you type `pdt`. To update pdt later, run `uv tool upgrade pdt-cli`.
44
45
 
45
- On Windows you can run `winget install AutoIDM.pdt` instead, which also installs `uv` if you do not have it and puts the same `pdt` command on your PATH. `winget upgrade AutoIDM.pdt` moves pdt to the latest release, and the first `pdt` command after an install or upgrade finishes setting up that version.
46
+ On Windows, paste these lines into PowerShell. They install `uv` and pdt, and they work in the window you type them in:
47
+
48
+ ```powershell
49
+ winget install --id astral-sh.uv -e
50
+ $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')
51
+ uv tool install pdt-cli
52
+ uv tool update-shell
53
+ $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')
54
+ pdt --version
55
+ ```
56
+
57
+ `winget` and `uv tool update-shell` change the PATH for new windows only. Each `$env:Path = ...` line reads the new PATH into the current window. Another PowerShell window that was open before the install does not find `uv` or `pdt` until you paste the same line into it or open a new window.
58
+
59
+ On Windows you can run `winget install AutoIDM.pdt` instead, which also installs `uv` if you do not have it and puts the same `pdt` command on your PATH. Open a new PowerShell window after it finishes. `winget upgrade AutoIDM.pdt` moves pdt to the latest release, and the first `pdt` command after an install or upgrade finishes setting up that version.
46
60
 
47
61
  You can also clone this repository and run `./pdt` (or `.\pdt.bat` on Windows) instead. It installs `uv` for you if you do not have it. Both ways give you the same commands.
48
62
 
49
63
  ## Set up a project
50
64
 
51
- A project is a folder holding `pdt.yml`. Each app is a folder inside it that contains a `run.py`. Every command except `init` finds the project by looking in the current folder, then each folder above it.
65
+ A project is a folder holding `pdt.yml`. Each app is a folder inside it that contains a `run.py`, or the `.ps1` scripts of a PowerShell app. Every command except `init` finds the project by looking in the current folder, then each folder above it.
52
66
 
53
67
  ```
54
68
  pdt init my-jobs
@@ -58,6 +72,8 @@ pdt init my-jobs
58
72
 
59
73
  Region is the only setting it asks for. Your AWS account, Azure subscription, and Google Cloud project all come from your credentials the first time you deploy, and pdt writes the answer into `pdt.yml` so every later deploy checks against it.
60
74
 
75
+ pdt suggests the region nearest to you from your computer's time zone. If `pdt.yml` has no region when you first deploy, the deploy suggests one the same way, asks you to confirm it or type another, and saves your answer. With `--yes` it takes the suggestion and tells you which region it used.
76
+
61
77
  Starting in an empty folder also gives you a working app called `hello-world`. Run it straight away:
62
78
 
63
79
  ```
@@ -99,7 +115,18 @@ my-jobs/
99
115
  pdt deploy my-report
100
116
  ```
101
117
 
102
- The command prints a resource plan and a monthly cost estimate before it changes anything. Add `--yes` to skip the question. To remove everything it created:
118
+ A deploy to AWS, Azure, or Google Cloud builds the app's image with Docker on your computer. The deploy starts Docker when it is installed but not running, and installs it after one question when it is missing (see [Choosing where jobs run](#choosing-where-jobs-run)). The command prints a resource plan and a monthly cost estimate before it changes anything. Add `--yes` to skip the question.
119
+
120
+ When the app has no successful run yet (it never ran, or every run failed), the deploy then asks `Start a run now and show its log? [y/N]`. A yes starts one run, as `pdt run my-report --deployed` does, and prints the run's log lines as they arrive, as `pdt logs my-report --follow` does. The deploy then exits with the run's result: 0 when the run succeeded, 1 when it failed. A run can have side effects, for example an email to real people, so the answer is no unless you type `y`. With `--yes`, or with no terminal (a build server), pdt does not ask and starts no run. Add `--run` to start the run and follow its log with no question. `pdt deploy --all` does not offer a run.
121
+
122
+ To start one run of the deployed job now, and then read its log:
123
+
124
+ ```
125
+ pdt run my-report --deployed
126
+ pdt logs my-report
127
+ ```
128
+
129
+ To remove everything the deploy created:
103
130
 
104
131
  ```
105
132
  pdt destroy my-report
@@ -118,26 +145,34 @@ pdt login my-report
118
145
  | `pdt init [DIR]` | create a project here, or in DIR |
119
146
  | `pdt examples` | list the example apps bundled with pdt |
120
147
  | `pdt new APP --from EXAMPLE` | add an app to the project |
121
- | `pdt list` | show every app with its schedule and provider; `--names` prints only the enabled app names |
122
- | `pdt validate` | check the config files and the required env vars |
148
+ | `pdt list` | show every app with its schedule and platform; `--names` prints only the enabled app names; `--json` prints one object per app with `name`, `schedule`, `platform`, `enabled`, `paused`, and `error` (a config problem, or `null`) |
149
+ | `pdt validate` | check the config files and the required env vars; exits 1 when it finds a problem; `--json` prints `{"ok": ..., "problems": [{"app": ..., "message": ...}]}`, where `app` is `null` for a problem of the project or of more than one app |
123
150
  | `pdt run APP` | run an app on this machine |
124
- | `pdt deploy APP` | deploy an app to its configured platform |
151
+ | `pdt run APP --deployed` | start one run of the deployed job now, on its platform |
152
+ | `pdt deploy APP` | deploy an app to its configured platform; when the app has no successful run yet, asks whether to start one and follow its log |
153
+ | `pdt deploy APP --run` | deploy, then start the app's first run and follow its log with no question; starts no run when the app already has a successful run |
125
154
  | `pdt deploy --all` | deploy every enabled app, in order; asks whether to skip and disable an app that fails; add `--yes --skip-failures` to run unattended |
126
155
  | `pdt destroy APP` | remove everything deploy created |
156
+ | `pdt pause APP` | stop the deployed schedule from starting runs; writes `pause: true` into the app's `config.yml` |
157
+ | `pdt unpause APP` | let the schedule start runs again; writes `pause: false` |
127
158
  | `pdt secrets APP` | show which `.env` values differ from the deployed app |
128
159
  | `pdt secrets APP save` | send your `.env` values to the deployed app; the next run uses them |
129
160
  | `pdt secrets APP get` | copy the deployed values into a `.env.<provider>` file |
130
161
  | `pdt secrets APP set NAME` | put one value, read from stdin, into the deployed app's secrets |
131
- | `pdt login APP` | sign in again to the app's cloud provider |
132
- | `pdt storage APP ls|get|query|destroy` | look at, fetch, query, or delete the app's stored files |
162
+ | `pdt login APP` | sign in again to the app's platform |
163
+ | `pdt storage APP ls\|get\|query\|unlock\|destroy` | look at, fetch, query, unlock, or delete the app's stored files; `ls PATH --recursive` lists every file under a folder, and `ls --json` prints the list as JSON |
133
164
  | `pdt runs APP` | list the deployed app's runs with each run's exit code: the 10 newest, or with `--since 3d` (or `12h`, `2w`, `2026-09-20`, `2026-09-20T14:00`) every run since then, `--span 1d` keeping only the runs within that long after `--since` and `--count N` keeping only the N newest; a run's number is its place among every run pdt can still find, so it stays the same whichever runs print |
134
- | `pdt logs APP [N]` | read the log of run N as `pdt runs` numbers it; with no N, the newest run, and `--failed` picks the newest failed run instead; `--since`, `--span`, and `--count` limit which runs those two choose from, `--errors` leaves out DEBUG and INFO lines; the last 20 lines print, `--lines N` prints N instead, `--head` prints the first lines instead of the last, and `--full` prints every line |
165
+ | `pdt logs APP [N]` | read the log of run N as `pdt runs` numbers it; with no N, the newest run, `--failed` picks the newest failed run instead, and `--id ID` picks a run by the id `pdt runs` shows (repeat it to read several runs; their `--json` is then an object keyed by id); `--since`, `--span`, and `--count` limit which runs those two choose from, `--errors` leaves out DEBUG and INFO lines; the last 20 lines print, `--lines N` prints N instead, `--head` prints the first lines instead of the last, `--full` prints every line, and `--follow` keeps printing new lines, in time order, until the run ends and its log has the run's exit line; on Azure, a running run's lines come from its container's live log, because Azure Log Analytics shows a line a minute or more after the job writes it; a run that is still running, or whose lines have not reached the log store yet, says so |
135
166
  | `pdt health [APP] [--all]` | show whether each app's last run succeeded; exits 1 when one failed; inside an app folder it checks only that app, and `--all` checks every app |
167
+ | `pdt gui` | open the project's dashboard in your browser: every app's health, each app's run history, and each run's log and files, with buttons for pause, unpause, and run now; `--background` keeps the server up after the command returns and `pdt gui --stop` ends it; `--port N` and `--no-browser` are there for the rare case |
168
+ | `pdt aws ...` | run the AWS CLI that pdt installs |
136
169
  | `pdt az ...` | run the Azure CLI that pdt installs |
137
170
  | `pdt gcloud ...` | run the Google Cloud CLI that pdt installs |
138
171
  | `pdt completion [SHELL]` | turn on tab completion for a shell |
139
172
 
140
- Inside an app folder, or any folder below it, leave `APP` off any command that takes one and pdt uses that app. For example, `pdt logs 3` in the `hello-world` folder reads run 3 of `hello-world`. Anywhere else, leave `APP` off or mistype it and pdt lists the apps in the project so you can pick one.
173
+ Inside an app folder, or any folder below it, leave `APP` off any command that takes one and pdt uses that app. For example, `pdt logs 3` in the `hello-world` folder reads run 3 of `hello-world`. Anywhere else, leave `APP` off or mistype it and pdt lists the apps in the project so you can pick one; a mistyped name also gets the closest app name. A mistyped command or option gets the closest one too, for example `pdt lgos` asks "Did you mean `pdt logs`?".
174
+
175
+ With `--json`, `pdt list`, `pdt validate`, `pdt runs`, `pdt logs`, `pdt health`, and `pdt storage APP ls` print only JSON on stdout, and every other line, such as an error or a sign-in question, on stderr. In a project with no enabled apps, `pdt runs`, `pdt logs`, `pdt health`, and `pdt storage APP ls` print `[]`. A command that has no JSON to print exits with a code other than 0 and prints nothing on stdout.
141
176
 
142
177
  `pdt az` and `pdt gcloud` hand your arguments straight to the cloud tool, and install it first if it is missing. For example, `pdt az account list`.
143
178
 
@@ -159,23 +194,31 @@ with store.open("report.csv", "wb") as f:
159
194
 
160
195
  `store.fs()` gives you the full [fsspec](https://filesystem-spec.readthedocs.io/) filesystem, rooted at your app's folder. Output from one run goes under `store.run_folder()`, which names a new `runs/<time>-<id>/` folder each run.
161
196
 
162
- Some apps need files from the last run: a Meltano bookmark, a database. Keep those in one local folder, and copy it down before the work and up after:
197
+ Some apps need files from the last run: a Meltano bookmark, a database. Keep those in the `state` folder that `storage.sync()` hands you, and put anything a person may want to look at afterwards in its `output` folder:
163
198
 
164
199
  ```python
165
- from pathlib import Path
166
200
  from pdt.utils import storage
167
201
 
168
- store = storage.store()
169
- lease = store.pull("state/", Path(".pdt-state"))
170
- # do the work; keep anything the next run needs inside .pdt-state/
171
- store.push(Path(".pdt-state"), "state/", lease)
202
+ with storage.sync() as run:
203
+ # do the work; keep anything the next run needs inside run.state/
204
+ (run.output / "report.csv").write_text("name,count\n")
172
205
  ```
173
206
 
174
- `pull` locks the folder, so a second copy of your app cannot run at the same time and mix up the files. `push` checks that nobody else changed them, saves them, and unlocks. If a run crashes, the next one takes over the lock after 30 minutes.
207
+ `sync` copies `state/` down from your app's folder and locks it, so a second copy of your app cannot run at the same time and mix up the files. When the block ends, it uploads `run.output` to this run's `runs/` folder, checks that nobody else changed the state files, saves them, and unlocks. When the block raises, or the run is interrupted, it still uploads `run.output`, unlocks, and leaves `state/` as it was, so the next run starts from the last good state. An app whose state is safe to keep at any point, such as a Meltano bookmark or an OAuth token that rotates on every login, passes `storage.sync(push_state_on_error=True)` so a failed run keeps what it saved. Both local folders live under `.pdt/runs/` in the app folder and stay there for inspection.
208
+
209
+ The same steps by hand are `store.pull("state/", folder)`, which returns a lease, then `store.push(folder, "state/", lease)` on success or `store.abort(lease)` on failure.
210
+
211
+ A run that dies without reaching either call leaves the lock behind. The next run takes it over right away when it can tell the holder is gone: the holder ran on the same computer and its process has ended, or it carries the same `PDT_RUN_ID`. Otherwise it waits 30 minutes, or you release the lock yourself with `pdt storage APP unlock`, which asks first.
175
212
 
176
213
  From your own computer, `pdt storage APP ls`, `get`, and `query` read the files with your own cloud sign-in. `pdt storage APP destroy` is the only command that deletes them, and it asks first. An app that needs none of this sets `storage: false` in its `config.yml`.
177
214
 
178
- An app that is not ready sets `enabled: false` in its `config.yml`. `pdt list` still shows it, and every other command acts as if the app is not there. `uv run run.py` in the app folder still runs it.
215
+ An app that is not ready sets `enabled: false` in its `config.yml`. `pdt list` still shows it, and every other command acts as if the app is not there. `uv run run.py` in the app folder still runs it. `enabled: false` does not stop a schedule that is already deployed, so run `pdt pause APP` or `pdt destroy APP` before you set it.
216
+
217
+ ## The dashboard
218
+
219
+ `pdt gui` starts a small web server on your own computer and opens it in your browser. The first page is `pdt health` as a table, with a grid of every run by hour, day, week, or month underneath it. Click an app for its run history, and click a run for its log and the files it kept in storage. A CSV file a run kept opens in a SQL workbench in the browser: a table you can sort, filter, and page, and a SQL view where every CSV of the run is a view you can query and join, with the matching `pdt storage APP query` command shown under it. The workbench runs on DuckDB, which the first `pdt gui` downloads once into the pdt data folder (`~/.local/share/pdt`); a Download link next to each file still saves it. Every number on a page came out of a pdt command (`pdt runs`, `pdt logs`, `pdt storage ls`), and every button runs one (`pdt pause`, `pdt unpause`, `pdt run --deployed`); hover a button to see which. The pages keep what they fetched in `.pdt/gui.sqlite3` inside the project and fetch again when it is older than five minutes or when you press Refresh, so a run stays in the history after the cloud has forgotten it. A worker inside the server checks for work every two minutes: it refreshes each app's runs, asks the provider once for every run it still keeps, and then fetches the log and files of each run that lacks them, newest first, until every run has them. A page shows what is there; it only waits when it asks for something the worker has not reached yet. The Stats link shows how long each page and each pdt command took. The server listens on localhost only and has no login. `pdt gui --background` leaves it running after the command returns; `pdt gui --stop` ends it.
220
+
221
+ An app that should stay deployed but not run for a while sets `pause: true`. The schedule stays in place and does not fire until the key goes back to `false`. `pdt pause APP` and `pdt unpause APP` write the key into the app's `config.yml` and apply it to the deployed schedule in the same step; `pdt deploy` also honors it. A paused app still runs when you ask for it with `pdt run APP --deployed`.
179
222
 
180
223
  ## Running pdt from CI
181
224
 
@@ -184,12 +227,16 @@ Every command reads its values from the environment it runs in. A build server s
184
227
  - A `.env` file is a convenience for a person working on their own machine. A value that is already in the environment wins over the same name in a `.env` file.
185
228
  - `PDT_ENV_JSON` holds every value as one JSON object, for a CI system that keeps one secret instead of many. For example, `PDT_ENV_JSON={"PDT_TOKEN": "abc", "PDT_SMTP_USER": "reports@example.com"}`.
186
229
  - pdt creates no `.env` file on a build server. It looks for the `CI` variable that build systems set, and for a missing terminal. Email authorization must therefore be done first: run `pdt run APP` once on a machine with a browser, then copy `PDT_SMTP_OAUTH_CACHE_B64` (or `PDT_GRAPH_MAIL_CACHE_B64` for Microsoft Graph) from your `.env` into the CI variables.
187
- - `pdt deploy APP` and `pdt destroy APP` ask before they change anything. Add `--yes` so they proceed without asking.
230
+ - `pdt deploy APP` and `pdt destroy APP` ask before they change anything. Add `--yes` so they proceed without asking. On a build server, `pdt deploy APP` starts no run after the deploy; add `--run` to start the app's first run and follow its log.
188
231
 
189
232
  ## Choosing where jobs run
190
233
 
191
234
  Set `platform:` in `pdt.yml` for every app, or in an app's own `config.yml` for one app. An app's own file wins. `timezone` may live under `platform:` as the default for every app, and an app's own `timezone` overrides it.
192
235
 
236
+ The cost estimate before each deploy shows prices in the currency of your computer's regional setting, or in US dollars when pdt cannot read it. pdt takes the price in that currency from the cloud when the cloud lists one. Otherwise it converts from US dollars at the cloud's own exchange rate, or at the European Central Bank's daily reference rate, and the estimate names the rate it used.
237
+
238
+ A deploy to AWS, Azure, or Google Cloud builds each app's image with Docker on your computer and uploads it to that cloud's image registry. When Docker is installed but not running, the deploy starts it. When Docker is not installed, the deploy shows the steps that install it and asks once. On macOS with Homebrew, it installs Colima through Homebrew and starts it with `brew services start colima`, which also starts it at each login. On macOS without Homebrew, it downloads Colima, Lima, the docker CLI, and docker-buildx to the `docker` folder in the pdt data folder (`$XDG_DATA_HOME/pdt`, by default `~/.local/share/pdt`), checks each file against its pinned sha256, and starts Colima; each later deploy starts Colima when it is stopped. To remove it, run `~/.local/share/pdt/docker/bin/colima delete`, delete that `docker` folder, and delete the link `~/.docker/cli-plugins/docker-buildx`. On Windows x64, it installs Rancher Desktop through winget. On Linux, it downloads Docker's install script at a pinned docker/docker-install commit, checks it against its pinned sha256, runs it with `sudo sh`, and adds you to the docker group. When Docker is installed and only `sudo` reaches it, the deploy shows the two commands that add you to the docker group and asks once. Without winget or sudo, and on Windows ARM64, the deploy names an install page for your system. When Docker runs without the buildx plugin, the deploy links Homebrew's docker-buildx or downloads the pinned one to the same `docker` folder, and links it into `~/.docker/cli-plugins`. On Linux and on Windows, when the image is for another CPU type than your computer's and the emulator is missing, the deploy first adds the QEMU emulator with `docker run --privileged --rm tonistiigi/binfmt:qemu-v10.2.3@sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0 --install <type>`. The Windows provider does not use Docker.
239
+
193
240
  ### Azure
194
241
 
195
242
  ```yaml
@@ -243,11 +290,15 @@ COPY . /workspace
243
290
  WORKDIR /workspace/my-app
244
291
  ENV PDT_PROJECT=/workspace NO_COLOR=1 DBT_USE_COLORS=false
245
292
  RUN uv sync --script run.py
246
- ENTRYPOINT ["uv", "run", "--script", "run.py"]
293
+ ENTRYPOINT ["sh", "-c", "uv run --script run.py; code=$?; echo \"pdt: exit $code\"; exit $code"]
247
294
  ```
248
295
 
249
296
  A `.dockerignore` in the app folder keeps files out of the image. Write its patterns relative to the app folder (`.meltano`, `output`, `*.csv`); pdt moves them to the root of the build context for you. `.env` and the other secret files never reach the context, with or without a `.dockerignore`.
250
297
 
298
+ The `ENTRYPOINT` ends each run's output with the line `pdt: exit N`, which `pdt runs` reads for the run's exit code. Keep that line when you change the rest of the file.
299
+
300
+ The build forwards your computer's SSH agent when one is running, so a `RUN --mount=type=ssh` step can install a private git dependency.
301
+
251
302
  The Windows provider never builds an image, so `pdt validate` reports a Dockerfile in an app that uses it.
252
303
 
253
304
  ### Windows Task Scheduler
@@ -283,6 +334,12 @@ Run `pdt examples` to list them, then `pdt new <name> --from <example>` to copy
283
334
 
284
335
  Small app that logs "Hello world." Shows how to write a config.yml and useful as an empty starting project so you can write your own. `pdt init` puts a copy of this in every new empty project.
285
336
 
337
+ ### azure-license-waste
338
+
339
+ A PowerShell app. Finds the Microsoft 365 licenses a tenant pays for that nobody uses: paid licenses on disabled accounts, on accounts that have not signed in for 90 days (or never did), and seats bought but assigned to nobody. Each run keeps a `LicenseWaste-<date>.xlsx` workbook in the app's data store, with a Summary sheet of monthly and annual cost and one sheet per finding.
340
+
341
+ Monthly prices come from the app's `license-prices.csv`; edit it to match what your tenant pays. A license missing from it is listed on an Unpriced Licenses sheet so you can add it. The Graph permissions and env vars it needs are listed in its `config.yml` and `env.template`.
342
+
286
343
  ### impossible-travel-report
287
344
 
288
345
  Looks at recent Entra ID sign-ins. Sends an email when one person signs in from two far-apart places too quickly: Dallas an hour ago, Paris now.
@@ -293,6 +350,10 @@ Settings for the lookback window, the minimum distance, and the email addresses
293
350
 
294
351
  Reports active Monday users whose email address does not match the `userPrincipalName` of an active Entra ID user.
295
352
 
353
+ ### powershell-report
354
+
355
+ A PowerShell app with no `run.py`. Its script writes a small CSV report with `Export-Csv`, and pdt keeps the file in the app's data store.
356
+
296
357
  ## Where things live
297
358
 
298
359
  | Item | Where |
@@ -302,8 +363,11 @@ Reports active Monday users whose email address does not match the `userPrincipa
302
363
  | the example apps | inside the pdt package, copied out by `pdt new` |
303
364
  | your apps, `pdt.yml`, `.env` | your project folder, under version control |
304
365
  | the Google Cloud CLI pdt downloads | `~/.local/share/pdt/gcloud`, or `%LOCALAPPDATA%\pdt\gcloud` |
366
+ | the PowerShell 7 pdt downloads | `~/.local/share/pdt/pwsh`, or `%ProgramData%\pdt\pwsh` |
367
+ | the DuckDB files of the `pdt gui` SQL workbench | `~/.local/share/pdt/duckdb-wasm`, or `%LOCALAPPDATA%\pdt\duckdb-wasm` |
368
+ | what `pdt gui` fetched | `.pdt/gui.sqlite3` in your project folder |
305
369
  | a Windows scheduled task's run logs and files | `%ProgramData%\pdt\<app>\logs` and `%ProgramData%\pdt\<app>\storage` |
306
- | cloud sign-in state | `~/.azure` and `~/.config/gcloud`, as usual |
370
+ | cloud sign-in state | `~/.aws`, `~/.azure`, and `~/.config/gcloud`, as usual |
307
371
 
308
372
  Set `PDT_PROJECT` to name the project folder directly, instead of letting pdt search upward. Deployed jobs get it set for them.
309
373
 
@@ -314,7 +378,7 @@ An app is a folder with a `run.py` that has a `main()` function. It declares its
314
378
  ```python
315
379
  # /// script
316
380
  # requires-python = ">=3.12"
317
- # dependencies = ["pdt-cli[apps]==0.1.4"]
381
+ # dependencies = ["pdt-cli[apps]==0.1.7"]
318
382
  # ///
319
383
  from pathlib import Path
320
384
 
@@ -328,11 +392,11 @@ def main() -> int:
328
392
  ...
329
393
  ```
330
394
 
331
- The pinned version matters. A deployed job keeps using the version in its header, so upgrading pdt on your machine does not change a job already running in the cloud.
395
+ The pinned version is the pdt version the app runs with, on your computer and in a deployed job. Upgrading pdt on your computer does not change it. Keep the pin at the newest pdt release, and run `pdt deploy APP` after you raise it, so the deployed job uses it too.
332
396
 
333
397
  ### PowerShell apps
334
398
 
335
- A folder of `.ps1` files with no `run.py` is a PowerShell app: pdt reads the scripts to find the modules to install, then runs the entry scripts in name order through pwsh. A `run.ps1` file is the only entry script. Otherwise, use `run_scripts` in `config.yml` to choose entries and set their order. Do not use `run.ps1` and `run_scripts` together. By default, pdt stops after the first failed script. Set `continue_on_error: true` to run every entry script. The run still fails with the first script's exit code. To change either step, run `pdt new <app> --from-scripts`. It writes `requirements.psd1`, which lists the modules with pinned versions, and `run.py`, which runs the scripts. Once they exist, pdt installs exactly the modules in `requirements.psd1` (`pdt validate` warns about a module the scripts need that it leaves out), and `pdt run` and deploy run your `run.py`.
399
+ A folder of `.ps1` files with no `run.py` is a PowerShell app: pdt reads the scripts to find the modules to install, then runs the entry scripts in name order through pwsh. Before `pdt run` starts the scripts on your computer, pdt installs each of those modules that is missing or at another version into its own folder (`~/.local/share/pdt/powershell-modules`, or `%LOCALAPPDATA%\pdt\powershell-modules` on Windows), so a local run loads the same versions a deploy installs. Other PowerShell sessions do not see that folder, and deleting it removes the modules. A `run.ps1` file is the only entry script. Otherwise, use `run_scripts` in `config.yml` to choose entries and set their order. Do not use `run.ps1` and `run_scripts` together. By default, pdt stops after the first failed script. Set `continue_on_error: true` to run every entry script. The run still fails with the first script's exit code. To change either step, run `pdt new <app> --from-scripts`. It writes `requirements.psd1`, which lists the modules with pinned versions, and `run.py`, which runs the scripts. Once they exist, pdt installs exactly the modules in `requirements.psd1` (`pdt validate` warns about a module the scripts need that it leaves out), and `pdt run` and deploy run your `run.py`.
336
400
 
337
401
  ```
338
402
  pdt new my-report --from-scripts
@@ -6,17 +6,31 @@ Run scheduled jobs — reports, integrations, automations — and deploy them to
6
6
 
7
7
  ```
8
8
  uv tool install pdt-cli
9
+ uv tool update-shell
9
10
  ```
10
11
 
11
- That puts a `pdt` command on your PATH. To update it later, run `uv tool upgrade pdt-cli`.
12
+ `uv tool install` puts the `pdt` command in uv's tool folder. `uv tool update-shell` adds that folder to your PATH if it is not there yet. A terminal window that was already open does not see a PATH change, so open a new window before you type `pdt`. To update pdt later, run `uv tool upgrade pdt-cli`.
12
13
 
13
- On Windows you can run `winget install AutoIDM.pdt` instead, which also installs `uv` if you do not have it and puts the same `pdt` command on your PATH. `winget upgrade AutoIDM.pdt` moves pdt to the latest release, and the first `pdt` command after an install or upgrade finishes setting up that version.
14
+ On Windows, paste these lines into PowerShell. They install `uv` and pdt, and they work in the window you type them in:
15
+
16
+ ```powershell
17
+ winget install --id astral-sh.uv -e
18
+ $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')
19
+ uv tool install pdt-cli
20
+ uv tool update-shell
21
+ $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')
22
+ pdt --version
23
+ ```
24
+
25
+ `winget` and `uv tool update-shell` change the PATH for new windows only. Each `$env:Path = ...` line reads the new PATH into the current window. Another PowerShell window that was open before the install does not find `uv` or `pdt` until you paste the same line into it or open a new window.
26
+
27
+ On Windows you can run `winget install AutoIDM.pdt` instead, which also installs `uv` if you do not have it and puts the same `pdt` command on your PATH. Open a new PowerShell window after it finishes. `winget upgrade AutoIDM.pdt` moves pdt to the latest release, and the first `pdt` command after an install or upgrade finishes setting up that version.
14
28
 
15
29
  You can also clone this repository and run `./pdt` (or `.\pdt.bat` on Windows) instead. It installs `uv` for you if you do not have it. Both ways give you the same commands.
16
30
 
17
31
  ## Set up a project
18
32
 
19
- A project is a folder holding `pdt.yml`. Each app is a folder inside it that contains a `run.py`. Every command except `init` finds the project by looking in the current folder, then each folder above it.
33
+ A project is a folder holding `pdt.yml`. Each app is a folder inside it that contains a `run.py`, or the `.ps1` scripts of a PowerShell app. Every command except `init` finds the project by looking in the current folder, then each folder above it.
20
34
 
21
35
  ```
22
36
  pdt init my-jobs
@@ -26,6 +40,8 @@ pdt init my-jobs
26
40
 
27
41
  Region is the only setting it asks for. Your AWS account, Azure subscription, and Google Cloud project all come from your credentials the first time you deploy, and pdt writes the answer into `pdt.yml` so every later deploy checks against it.
28
42
 
43
+ pdt suggests the region nearest to you from your computer's time zone. If `pdt.yml` has no region when you first deploy, the deploy suggests one the same way, asks you to confirm it or type another, and saves your answer. With `--yes` it takes the suggestion and tells you which region it used.
44
+
29
45
  Starting in an empty folder also gives you a working app called `hello-world`. Run it straight away:
30
46
 
31
47
  ```
@@ -67,7 +83,18 @@ my-jobs/
67
83
  pdt deploy my-report
68
84
  ```
69
85
 
70
- The command prints a resource plan and a monthly cost estimate before it changes anything. Add `--yes` to skip the question. To remove everything it created:
86
+ A deploy to AWS, Azure, or Google Cloud builds the app's image with Docker on your computer. The deploy starts Docker when it is installed but not running, and installs it after one question when it is missing (see [Choosing where jobs run](#choosing-where-jobs-run)). The command prints a resource plan and a monthly cost estimate before it changes anything. Add `--yes` to skip the question.
87
+
88
+ When the app has no successful run yet (it never ran, or every run failed), the deploy then asks `Start a run now and show its log? [y/N]`. A yes starts one run, as `pdt run my-report --deployed` does, and prints the run's log lines as they arrive, as `pdt logs my-report --follow` does. The deploy then exits with the run's result: 0 when the run succeeded, 1 when it failed. A run can have side effects, for example an email to real people, so the answer is no unless you type `y`. With `--yes`, or with no terminal (a build server), pdt does not ask and starts no run. Add `--run` to start the run and follow its log with no question. `pdt deploy --all` does not offer a run.
89
+
90
+ To start one run of the deployed job now, and then read its log:
91
+
92
+ ```
93
+ pdt run my-report --deployed
94
+ pdt logs my-report
95
+ ```
96
+
97
+ To remove everything the deploy created:
71
98
 
72
99
  ```
73
100
  pdt destroy my-report
@@ -86,26 +113,34 @@ pdt login my-report
86
113
  | `pdt init [DIR]` | create a project here, or in DIR |
87
114
  | `pdt examples` | list the example apps bundled with pdt |
88
115
  | `pdt new APP --from EXAMPLE` | add an app to the project |
89
- | `pdt list` | show every app with its schedule and provider; `--names` prints only the enabled app names |
90
- | `pdt validate` | check the config files and the required env vars |
116
+ | `pdt list` | show every app with its schedule and platform; `--names` prints only the enabled app names; `--json` prints one object per app with `name`, `schedule`, `platform`, `enabled`, `paused`, and `error` (a config problem, or `null`) |
117
+ | `pdt validate` | check the config files and the required env vars; exits 1 when it finds a problem; `--json` prints `{"ok": ..., "problems": [{"app": ..., "message": ...}]}`, where `app` is `null` for a problem of the project or of more than one app |
91
118
  | `pdt run APP` | run an app on this machine |
92
- | `pdt deploy APP` | deploy an app to its configured platform |
119
+ | `pdt run APP --deployed` | start one run of the deployed job now, on its platform |
120
+ | `pdt deploy APP` | deploy an app to its configured platform; when the app has no successful run yet, asks whether to start one and follow its log |
121
+ | `pdt deploy APP --run` | deploy, then start the app's first run and follow its log with no question; starts no run when the app already has a successful run |
93
122
  | `pdt deploy --all` | deploy every enabled app, in order; asks whether to skip and disable an app that fails; add `--yes --skip-failures` to run unattended |
94
123
  | `pdt destroy APP` | remove everything deploy created |
124
+ | `pdt pause APP` | stop the deployed schedule from starting runs; writes `pause: true` into the app's `config.yml` |
125
+ | `pdt unpause APP` | let the schedule start runs again; writes `pause: false` |
95
126
  | `pdt secrets APP` | show which `.env` values differ from the deployed app |
96
127
  | `pdt secrets APP save` | send your `.env` values to the deployed app; the next run uses them |
97
128
  | `pdt secrets APP get` | copy the deployed values into a `.env.<provider>` file |
98
129
  | `pdt secrets APP set NAME` | put one value, read from stdin, into the deployed app's secrets |
99
- | `pdt login APP` | sign in again to the app's cloud provider |
100
- | `pdt storage APP ls|get|query|destroy` | look at, fetch, query, or delete the app's stored files |
130
+ | `pdt login APP` | sign in again to the app's platform |
131
+ | `pdt storage APP ls\|get\|query\|unlock\|destroy` | look at, fetch, query, unlock, or delete the app's stored files; `ls PATH --recursive` lists every file under a folder, and `ls --json` prints the list as JSON |
101
132
  | `pdt runs APP` | list the deployed app's runs with each run's exit code: the 10 newest, or with `--since 3d` (or `12h`, `2w`, `2026-09-20`, `2026-09-20T14:00`) every run since then, `--span 1d` keeping only the runs within that long after `--since` and `--count N` keeping only the N newest; a run's number is its place among every run pdt can still find, so it stays the same whichever runs print |
102
- | `pdt logs APP [N]` | read the log of run N as `pdt runs` numbers it; with no N, the newest run, and `--failed` picks the newest failed run instead; `--since`, `--span`, and `--count` limit which runs those two choose from, `--errors` leaves out DEBUG and INFO lines; the last 20 lines print, `--lines N` prints N instead, `--head` prints the first lines instead of the last, and `--full` prints every line |
133
+ | `pdt logs APP [N]` | read the log of run N as `pdt runs` numbers it; with no N, the newest run, `--failed` picks the newest failed run instead, and `--id ID` picks a run by the id `pdt runs` shows (repeat it to read several runs; their `--json` is then an object keyed by id); `--since`, `--span`, and `--count` limit which runs those two choose from, `--errors` leaves out DEBUG and INFO lines; the last 20 lines print, `--lines N` prints N instead, `--head` prints the first lines instead of the last, `--full` prints every line, and `--follow` keeps printing new lines, in time order, until the run ends and its log has the run's exit line; on Azure, a running run's lines come from its container's live log, because Azure Log Analytics shows a line a minute or more after the job writes it; a run that is still running, or whose lines have not reached the log store yet, says so |
103
134
  | `pdt health [APP] [--all]` | show whether each app's last run succeeded; exits 1 when one failed; inside an app folder it checks only that app, and `--all` checks every app |
135
+ | `pdt gui` | open the project's dashboard in your browser: every app's health, each app's run history, and each run's log and files, with buttons for pause, unpause, and run now; `--background` keeps the server up after the command returns and `pdt gui --stop` ends it; `--port N` and `--no-browser` are there for the rare case |
136
+ | `pdt aws ...` | run the AWS CLI that pdt installs |
104
137
  | `pdt az ...` | run the Azure CLI that pdt installs |
105
138
  | `pdt gcloud ...` | run the Google Cloud CLI that pdt installs |
106
139
  | `pdt completion [SHELL]` | turn on tab completion for a shell |
107
140
 
108
- Inside an app folder, or any folder below it, leave `APP` off any command that takes one and pdt uses that app. For example, `pdt logs 3` in the `hello-world` folder reads run 3 of `hello-world`. Anywhere else, leave `APP` off or mistype it and pdt lists the apps in the project so you can pick one.
141
+ Inside an app folder, or any folder below it, leave `APP` off any command that takes one and pdt uses that app. For example, `pdt logs 3` in the `hello-world` folder reads run 3 of `hello-world`. Anywhere else, leave `APP` off or mistype it and pdt lists the apps in the project so you can pick one; a mistyped name also gets the closest app name. A mistyped command or option gets the closest one too, for example `pdt lgos` asks "Did you mean `pdt logs`?".
142
+
143
+ With `--json`, `pdt list`, `pdt validate`, `pdt runs`, `pdt logs`, `pdt health`, and `pdt storage APP ls` print only JSON on stdout, and every other line, such as an error or a sign-in question, on stderr. In a project with no enabled apps, `pdt runs`, `pdt logs`, `pdt health`, and `pdt storage APP ls` print `[]`. A command that has no JSON to print exits with a code other than 0 and prints nothing on stdout.
109
144
 
110
145
  `pdt az` and `pdt gcloud` hand your arguments straight to the cloud tool, and install it first if it is missing. For example, `pdt az account list`.
111
146
 
@@ -127,23 +162,31 @@ with store.open("report.csv", "wb") as f:
127
162
 
128
163
  `store.fs()` gives you the full [fsspec](https://filesystem-spec.readthedocs.io/) filesystem, rooted at your app's folder. Output from one run goes under `store.run_folder()`, which names a new `runs/<time>-<id>/` folder each run.
129
164
 
130
- Some apps need files from the last run: a Meltano bookmark, a database. Keep those in one local folder, and copy it down before the work and up after:
165
+ Some apps need files from the last run: a Meltano bookmark, a database. Keep those in the `state` folder that `storage.sync()` hands you, and put anything a person may want to look at afterwards in its `output` folder:
131
166
 
132
167
  ```python
133
- from pathlib import Path
134
168
  from pdt.utils import storage
135
169
 
136
- store = storage.store()
137
- lease = store.pull("state/", Path(".pdt-state"))
138
- # do the work; keep anything the next run needs inside .pdt-state/
139
- store.push(Path(".pdt-state"), "state/", lease)
170
+ with storage.sync() as run:
171
+ # do the work; keep anything the next run needs inside run.state/
172
+ (run.output / "report.csv").write_text("name,count\n")
140
173
  ```
141
174
 
142
- `pull` locks the folder, so a second copy of your app cannot run at the same time and mix up the files. `push` checks that nobody else changed them, saves them, and unlocks. If a run crashes, the next one takes over the lock after 30 minutes.
175
+ `sync` copies `state/` down from your app's folder and locks it, so a second copy of your app cannot run at the same time and mix up the files. When the block ends, it uploads `run.output` to this run's `runs/` folder, checks that nobody else changed the state files, saves them, and unlocks. When the block raises, or the run is interrupted, it still uploads `run.output`, unlocks, and leaves `state/` as it was, so the next run starts from the last good state. An app whose state is safe to keep at any point, such as a Meltano bookmark or an OAuth token that rotates on every login, passes `storage.sync(push_state_on_error=True)` so a failed run keeps what it saved. Both local folders live under `.pdt/runs/` in the app folder and stay there for inspection.
176
+
177
+ The same steps by hand are `store.pull("state/", folder)`, which returns a lease, then `store.push(folder, "state/", lease)` on success or `store.abort(lease)` on failure.
178
+
179
+ A run that dies without reaching either call leaves the lock behind. The next run takes it over right away when it can tell the holder is gone: the holder ran on the same computer and its process has ended, or it carries the same `PDT_RUN_ID`. Otherwise it waits 30 minutes, or you release the lock yourself with `pdt storage APP unlock`, which asks first.
143
180
 
144
181
  From your own computer, `pdt storage APP ls`, `get`, and `query` read the files with your own cloud sign-in. `pdt storage APP destroy` is the only command that deletes them, and it asks first. An app that needs none of this sets `storage: false` in its `config.yml`.
145
182
 
146
- An app that is not ready sets `enabled: false` in its `config.yml`. `pdt list` still shows it, and every other command acts as if the app is not there. `uv run run.py` in the app folder still runs it.
183
+ An app that is not ready sets `enabled: false` in its `config.yml`. `pdt list` still shows it, and every other command acts as if the app is not there. `uv run run.py` in the app folder still runs it. `enabled: false` does not stop a schedule that is already deployed, so run `pdt pause APP` or `pdt destroy APP` before you set it.
184
+
185
+ ## The dashboard
186
+
187
+ `pdt gui` starts a small web server on your own computer and opens it in your browser. The first page is `pdt health` as a table, with a grid of every run by hour, day, week, or month underneath it. Click an app for its run history, and click a run for its log and the files it kept in storage. A CSV file a run kept opens in a SQL workbench in the browser: a table you can sort, filter, and page, and a SQL view where every CSV of the run is a view you can query and join, with the matching `pdt storage APP query` command shown under it. The workbench runs on DuckDB, which the first `pdt gui` downloads once into the pdt data folder (`~/.local/share/pdt`); a Download link next to each file still saves it. Every number on a page came out of a pdt command (`pdt runs`, `pdt logs`, `pdt storage ls`), and every button runs one (`pdt pause`, `pdt unpause`, `pdt run --deployed`); hover a button to see which. The pages keep what they fetched in `.pdt/gui.sqlite3` inside the project and fetch again when it is older than five minutes or when you press Refresh, so a run stays in the history after the cloud has forgotten it. A worker inside the server checks for work every two minutes: it refreshes each app's runs, asks the provider once for every run it still keeps, and then fetches the log and files of each run that lacks them, newest first, until every run has them. A page shows what is there; it only waits when it asks for something the worker has not reached yet. The Stats link shows how long each page and each pdt command took. The server listens on localhost only and has no login. `pdt gui --background` leaves it running after the command returns; `pdt gui --stop` ends it.
188
+
189
+ An app that should stay deployed but not run for a while sets `pause: true`. The schedule stays in place and does not fire until the key goes back to `false`. `pdt pause APP` and `pdt unpause APP` write the key into the app's `config.yml` and apply it to the deployed schedule in the same step; `pdt deploy` also honors it. A paused app still runs when you ask for it with `pdt run APP --deployed`.
147
190
 
148
191
  ## Running pdt from CI
149
192
 
@@ -152,12 +195,16 @@ Every command reads its values from the environment it runs in. A build server s
152
195
  - A `.env` file is a convenience for a person working on their own machine. A value that is already in the environment wins over the same name in a `.env` file.
153
196
  - `PDT_ENV_JSON` holds every value as one JSON object, for a CI system that keeps one secret instead of many. For example, `PDT_ENV_JSON={"PDT_TOKEN": "abc", "PDT_SMTP_USER": "reports@example.com"}`.
154
197
  - pdt creates no `.env` file on a build server. It looks for the `CI` variable that build systems set, and for a missing terminal. Email authorization must therefore be done first: run `pdt run APP` once on a machine with a browser, then copy `PDT_SMTP_OAUTH_CACHE_B64` (or `PDT_GRAPH_MAIL_CACHE_B64` for Microsoft Graph) from your `.env` into the CI variables.
155
- - `pdt deploy APP` and `pdt destroy APP` ask before they change anything. Add `--yes` so they proceed without asking.
198
+ - `pdt deploy APP` and `pdt destroy APP` ask before they change anything. Add `--yes` so they proceed without asking. On a build server, `pdt deploy APP` starts no run after the deploy; add `--run` to start the app's first run and follow its log.
156
199
 
157
200
  ## Choosing where jobs run
158
201
 
159
202
  Set `platform:` in `pdt.yml` for every app, or in an app's own `config.yml` for one app. An app's own file wins. `timezone` may live under `platform:` as the default for every app, and an app's own `timezone` overrides it.
160
203
 
204
+ The cost estimate before each deploy shows prices in the currency of your computer's regional setting, or in US dollars when pdt cannot read it. pdt takes the price in that currency from the cloud when the cloud lists one. Otherwise it converts from US dollars at the cloud's own exchange rate, or at the European Central Bank's daily reference rate, and the estimate names the rate it used.
205
+
206
+ A deploy to AWS, Azure, or Google Cloud builds each app's image with Docker on your computer and uploads it to that cloud's image registry. When Docker is installed but not running, the deploy starts it. When Docker is not installed, the deploy shows the steps that install it and asks once. On macOS with Homebrew, it installs Colima through Homebrew and starts it with `brew services start colima`, which also starts it at each login. On macOS without Homebrew, it downloads Colima, Lima, the docker CLI, and docker-buildx to the `docker` folder in the pdt data folder (`$XDG_DATA_HOME/pdt`, by default `~/.local/share/pdt`), checks each file against its pinned sha256, and starts Colima; each later deploy starts Colima when it is stopped. To remove it, run `~/.local/share/pdt/docker/bin/colima delete`, delete that `docker` folder, and delete the link `~/.docker/cli-plugins/docker-buildx`. On Windows x64, it installs Rancher Desktop through winget. On Linux, it downloads Docker's install script at a pinned docker/docker-install commit, checks it against its pinned sha256, runs it with `sudo sh`, and adds you to the docker group. When Docker is installed and only `sudo` reaches it, the deploy shows the two commands that add you to the docker group and asks once. Without winget or sudo, and on Windows ARM64, the deploy names an install page for your system. When Docker runs without the buildx plugin, the deploy links Homebrew's docker-buildx or downloads the pinned one to the same `docker` folder, and links it into `~/.docker/cli-plugins`. On Linux and on Windows, when the image is for another CPU type than your computer's and the emulator is missing, the deploy first adds the QEMU emulator with `docker run --privileged --rm tonistiigi/binfmt:qemu-v10.2.3@sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0 --install <type>`. The Windows provider does not use Docker.
207
+
161
208
  ### Azure
162
209
 
163
210
  ```yaml
@@ -211,11 +258,15 @@ COPY . /workspace
211
258
  WORKDIR /workspace/my-app
212
259
  ENV PDT_PROJECT=/workspace NO_COLOR=1 DBT_USE_COLORS=false
213
260
  RUN uv sync --script run.py
214
- ENTRYPOINT ["uv", "run", "--script", "run.py"]
261
+ ENTRYPOINT ["sh", "-c", "uv run --script run.py; code=$?; echo \"pdt: exit $code\"; exit $code"]
215
262
  ```
216
263
 
217
264
  A `.dockerignore` in the app folder keeps files out of the image. Write its patterns relative to the app folder (`.meltano`, `output`, `*.csv`); pdt moves them to the root of the build context for you. `.env` and the other secret files never reach the context, with or without a `.dockerignore`.
218
265
 
266
+ The `ENTRYPOINT` ends each run's output with the line `pdt: exit N`, which `pdt runs` reads for the run's exit code. Keep that line when you change the rest of the file.
267
+
268
+ The build forwards your computer's SSH agent when one is running, so a `RUN --mount=type=ssh` step can install a private git dependency.
269
+
219
270
  The Windows provider never builds an image, so `pdt validate` reports a Dockerfile in an app that uses it.
220
271
 
221
272
  ### Windows Task Scheduler
@@ -251,6 +302,12 @@ Run `pdt examples` to list them, then `pdt new <name> --from <example>` to copy
251
302
 
252
303
  Small app that logs "Hello world." Shows how to write a config.yml and useful as an empty starting project so you can write your own. `pdt init` puts a copy of this in every new empty project.
253
304
 
305
+ ### azure-license-waste
306
+
307
+ A PowerShell app. Finds the Microsoft 365 licenses a tenant pays for that nobody uses: paid licenses on disabled accounts, on accounts that have not signed in for 90 days (or never did), and seats bought but assigned to nobody. Each run keeps a `LicenseWaste-<date>.xlsx` workbook in the app's data store, with a Summary sheet of monthly and annual cost and one sheet per finding.
308
+
309
+ Monthly prices come from the app's `license-prices.csv`; edit it to match what your tenant pays. A license missing from it is listed on an Unpriced Licenses sheet so you can add it. The Graph permissions and env vars it needs are listed in its `config.yml` and `env.template`.
310
+
254
311
  ### impossible-travel-report
255
312
 
256
313
  Looks at recent Entra ID sign-ins. Sends an email when one person signs in from two far-apart places too quickly: Dallas an hour ago, Paris now.
@@ -261,6 +318,10 @@ Settings for the lookback window, the minimum distance, and the email addresses
261
318
 
262
319
  Reports active Monday users whose email address does not match the `userPrincipalName` of an active Entra ID user.
263
320
 
321
+ ### powershell-report
322
+
323
+ A PowerShell app with no `run.py`. Its script writes a small CSV report with `Export-Csv`, and pdt keeps the file in the app's data store.
324
+
264
325
  ## Where things live
265
326
 
266
327
  | Item | Where |
@@ -270,8 +331,11 @@ Reports active Monday users whose email address does not match the `userPrincipa
270
331
  | the example apps | inside the pdt package, copied out by `pdt new` |
271
332
  | your apps, `pdt.yml`, `.env` | your project folder, under version control |
272
333
  | the Google Cloud CLI pdt downloads | `~/.local/share/pdt/gcloud`, or `%LOCALAPPDATA%\pdt\gcloud` |
334
+ | the PowerShell 7 pdt downloads | `~/.local/share/pdt/pwsh`, or `%ProgramData%\pdt\pwsh` |
335
+ | the DuckDB files of the `pdt gui` SQL workbench | `~/.local/share/pdt/duckdb-wasm`, or `%LOCALAPPDATA%\pdt\duckdb-wasm` |
336
+ | what `pdt gui` fetched | `.pdt/gui.sqlite3` in your project folder |
273
337
  | a Windows scheduled task's run logs and files | `%ProgramData%\pdt\<app>\logs` and `%ProgramData%\pdt\<app>\storage` |
274
- | cloud sign-in state | `~/.azure` and `~/.config/gcloud`, as usual |
338
+ | cloud sign-in state | `~/.aws`, `~/.azure`, and `~/.config/gcloud`, as usual |
275
339
 
276
340
  Set `PDT_PROJECT` to name the project folder directly, instead of letting pdt search upward. Deployed jobs get it set for them.
277
341
 
@@ -282,7 +346,7 @@ An app is a folder with a `run.py` that has a `main()` function. It declares its
282
346
  ```python
283
347
  # /// script
284
348
  # requires-python = ">=3.12"
285
- # dependencies = ["pdt-cli[apps]==0.1.4"]
349
+ # dependencies = ["pdt-cli[apps]==0.1.7"]
286
350
  # ///
287
351
  from pathlib import Path
288
352
 
@@ -296,11 +360,11 @@ def main() -> int:
296
360
  ...
297
361
  ```
298
362
 
299
- The pinned version matters. A deployed job keeps using the version in its header, so upgrading pdt on your machine does not change a job already running in the cloud.
363
+ The pinned version is the pdt version the app runs with, on your computer and in a deployed job. Upgrading pdt on your computer does not change it. Keep the pin at the newest pdt release, and run `pdt deploy APP` after you raise it, so the deployed job uses it too.
300
364
 
301
365
  ### PowerShell apps
302
366
 
303
- A folder of `.ps1` files with no `run.py` is a PowerShell app: pdt reads the scripts to find the modules to install, then runs the entry scripts in name order through pwsh. A `run.ps1` file is the only entry script. Otherwise, use `run_scripts` in `config.yml` to choose entries and set their order. Do not use `run.ps1` and `run_scripts` together. By default, pdt stops after the first failed script. Set `continue_on_error: true` to run every entry script. The run still fails with the first script's exit code. To change either step, run `pdt new <app> --from-scripts`. It writes `requirements.psd1`, which lists the modules with pinned versions, and `run.py`, which runs the scripts. Once they exist, pdt installs exactly the modules in `requirements.psd1` (`pdt validate` warns about a module the scripts need that it leaves out), and `pdt run` and deploy run your `run.py`.
367
+ A folder of `.ps1` files with no `run.py` is a PowerShell app: pdt reads the scripts to find the modules to install, then runs the entry scripts in name order through pwsh. Before `pdt run` starts the scripts on your computer, pdt installs each of those modules that is missing or at another version into its own folder (`~/.local/share/pdt/powershell-modules`, or `%LOCALAPPDATA%\pdt\powershell-modules` on Windows), so a local run loads the same versions a deploy installs. Other PowerShell sessions do not see that folder, and deleting it removes the modules. A `run.ps1` file is the only entry script. Otherwise, use `run_scripts` in `config.yml` to choose entries and set their order. Do not use `run.ps1` and `run_scripts` together. By default, pdt stops after the first failed script. Set `continue_on_error: true` to run every entry script. The run still fails with the first script's exit code. To change either step, run `pdt new <app> --from-scripts`. It writes `requirements.psd1`, which lists the modules with pinned versions, and `run.py`, which runs the scripts. Once they exist, pdt installs exactly the modules in `requirements.psd1` (`pdt validate` warns about a module the scripts need that it leaves out), and `pdt run` and deploy run your `run.py`.
304
368
 
305
369
  ```
306
370
  pdt new my-report --from-scripts
@@ -119,7 +119,7 @@ def comment(marker: str, login: str = "pdt-bot") -> dict:
119
119
 
120
120
  def test_path_floors_follow_the_table():
121
121
  arch = ["src/pdt/utils/log.py", "src/pdt/deploy.py", "src/pdt/deploy_common.py",
122
- "src/pdt/config.py", "src/pdt/cli.py", "src/pdt/__init__.py", ".gitlab-ci.yml",
122
+ "src/pdt/config.py", "src/pdt/cli.py", "src/pdt/__init__.py",
123
123
  ".github/workflows/ci.yml", "ci/claude_task.py", "AGENTS.md", "CLAUDE.md",
124
124
  "pdt", "pdt.bat"]
125
125
  for path in arch:
@@ -172,7 +172,7 @@ def test_a_few_tested_lines_in_a_covered_file_floor_at_simple():
172
172
  assert select.rule_floor(pr(), [five, test])[0] == "architectural"
173
173
  gated = change("src/pdt/cli.py", extra="+token = 1\n")
174
174
  assert select.rule_floor(pr(), [gated, test])[0] == "architectural"
175
- for path in ("src/pdt/utils/log.py", ".gitlab-ci.yml", "AGENTS.md", "pdt"):
175
+ for path in ("src/pdt/utils/log.py", ".github/workflows/ci.yml", "AGENTS.md", "pdt"):
176
176
  assert select.rule_floor(pr(), [change(path), test])[0] == "architectural", path
177
177
 
178
178
 
@@ -670,10 +670,3 @@ def test_score_prs_workflow_runs_on_the_three_pr_events_a_schedule_and_by_hand()
670
670
  assert score["run"].rstrip().endswith("|| [ $? -eq 2 ]")
671
671
  upload = job["steps"][-1]
672
672
  assert upload["if"] == "always()" and upload["with"]["retention-days"] == 30
673
-
674
-
675
- def test_gitlab_ci_runs_no_claude_task():
676
- ci = yaml.safe_load((REPO / ".gitlab-ci.yml").read_text())
677
- for name in (".claude-task", "rebase-mrs", "score-mrs", "score-mrs-skip",
678
- "close_stale_drafts"):
679
- assert name not in ci, name
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pdt-cli"
3
- version = "0.1.4"
3
+ version = "0.1.7"
4
4
  description = "Easily deploy your process to AWS, Azure, Google Cloud, or Windows."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -48,7 +48,7 @@ packages = ["src/pdt"]
48
48
  include = ["src/pdt", "README.md", "tests"]
49
49
 
50
50
  [dependency-groups]
51
- dev = ["pytest", "ruff", "fsspec", "boto3"]
51
+ dev = ["pytest", "ruff", "fsspec", "boto3", "django>=5.2", "croniter", "tzlocal"]
52
52
 
53
53
  [tool.pytest.ini_options]
54
54
  testpaths = ["tests", "ci/tests"]
@@ -0,0 +1,9 @@
1
+ import os
2
+ from importlib.metadata import PackageNotFoundError, version
3
+
4
+ try:
5
+ __version__ = version("pdt-cli")
6
+ except PackageNotFoundError:
7
+ # A provider script run from a clone imports pdt from src/, which holds no
8
+ # package metadata; deploy.provider_env passes the parent's version down.
9
+ __version__ = os.environ.get("PDT_CLI_VERSION", "0.0.0.dev0")