pdt-cli 0.1.2__tar.gz → 0.1.3__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 (122) hide show
  1. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/.gitignore +1 -0
  2. pdt_cli-0.1.3/LICENSE +9 -0
  3. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/PKG-INFO +19 -9
  4. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/README.md +16 -7
  5. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/pyproject.toml +3 -3
  6. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/cli.py +133 -4
  7. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/completion.py +1 -1
  8. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/config.py +111 -24
  9. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/console.py +23 -6
  10. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/deploy.py +66 -6
  11. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/deploy_aws.py +200 -53
  12. pdt_cli-0.1.3/src/pdt/deploy_aws_batch.py +796 -0
  13. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/deploy_azure.py +57 -8
  14. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/deploy_azure_container_apps.py +199 -28
  15. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/deploy_common.py +27 -8
  16. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/deploy_google_cloud.py +204 -36
  17. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/deploy_windows.py +205 -41
  18. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/gcloud_sdk.py +7 -0
  19. pdt_cli-0.1.3/src/pdt/run_windows_task.py +59 -0
  20. pdt_cli-0.1.3/src/pdt/runs_cli.py +349 -0
  21. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/scaffold.py +8 -1
  22. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/utils/email_auth.py +3 -1
  23. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/utils/env_secret.py +9 -1
  24. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/utils/storage.py +4 -3
  25. pdt_cli-0.1.3/tests/test_app_name.py +41 -0
  26. pdt_cli-0.1.3/tests/test_aws_cost_estimate.py +20 -0
  27. pdt_cli-0.1.3/tests/test_aws_deployer_policy.py +148 -0
  28. pdt_cli-0.1.3/tests/test_aws_destroy.py +251 -0
  29. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_aws_inventory.py +25 -0
  30. pdt_cli-0.1.3/tests/test_aws_job_definition.py +127 -0
  31. pdt_cli-0.1.3/tests/test_aws_names.py +20 -0
  32. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_aws_not_found.py +4 -0
  33. pdt_cli-0.1.3/tests/test_aws_runs.py +121 -0
  34. pdt_cli-0.1.3/tests/test_aws_shared_deletes.py +143 -0
  35. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_azure_deleted_vault.py +5 -1
  36. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_azure_environment.py +3 -3
  37. pdt_cli-0.1.3/tests/test_azure_runs.py +117 -0
  38. pdt_cli-0.1.3/tests/test_azure_shared_locks.py +187 -0
  39. pdt_cli-0.1.3/tests/test_build_context.py +62 -0
  40. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_ci_azure.py +33 -0
  41. pdt_cli-0.1.3/tests/test_ci_google_cloud.py +200 -0
  42. pdt_cli-0.1.3/tests/test_cli.py +325 -0
  43. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_completion.py +35 -1
  44. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_config_merge.py +61 -0
  45. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_deploy_state.py +51 -0
  46. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_dockerfile.py +18 -0
  47. pdt_cli-0.1.3/tests/test_gcloud_sdk.py +52 -0
  48. pdt_cli-0.1.3/tests/test_google_cloud_retries.py +124 -0
  49. pdt_cli-0.1.3/tests/test_google_cloud_runs.py +154 -0
  50. pdt_cli-0.1.3/tests/test_google_cloud_storage.py +124 -0
  51. pdt_cli-0.1.3/tests/test_private_file.py +73 -0
  52. pdt_cli-0.1.3/tests/test_runs_cli.py +387 -0
  53. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_save_platform_key.py +60 -1
  54. pdt_cli-0.1.3/tests/test_set_app_enabled.py +49 -0
  55. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_storage.py +1 -2
  56. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_verify.py +20 -3
  57. pdt_cli-0.1.3/tests/test_windows_data.py +101 -0
  58. pdt_cli-0.1.3/tests/test_windows_runner.py +96 -0
  59. pdt_cli-0.1.3/tests/test_windows_runs.py +159 -0
  60. pdt_cli-0.1.3/tests/test_windows_storage.py +144 -0
  61. pdt_cli-0.1.3/tests/test_winget_launcher.py +66 -0
  62. pdt_cli-0.1.3/tests/test_winget_manifests.py +48 -0
  63. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/verify/README.md +1 -1
  64. pdt_cli-0.1.2/src/pdt/deploy_aws_fargate.py +0 -508
  65. pdt_cli-0.1.2/tests/test_aws_names.py +0 -9
  66. pdt_cli-0.1.2/tests/test_ci_google_cloud.py +0 -93
  67. pdt_cli-0.1.2/tests/test_cli.py +0 -104
  68. pdt_cli-0.1.2/tests/test_gcloud_sdk.py +0 -24
  69. pdt_cli-0.1.2/tests/test_google_cloud_retries.py +0 -41
  70. pdt_cli-0.1.2/tests/test_google_cloud_storage.py +0 -25
  71. pdt_cli-0.1.2/tests/test_windows_storage.py +0 -50
  72. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/ci/tests/test_claude_task.py +0 -0
  73. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/ci/tests/test_close_stale_drafts.py +0 -0
  74. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/ci/tests/test_rebase_task.py +0 -0
  75. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/ci/tests/test_score_task.py +0 -0
  76. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/__init__.py +0 -0
  77. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/examples/hello-world/config.yml +0 -0
  78. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/examples/hello-world/run.py +0 -0
  79. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/examples/impossible-travel-report/config.yml +0 -0
  80. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/examples/impossible-travel-report/env.template +0 -0
  81. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/examples/impossible-travel-report/run.py +0 -0
  82. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/examples/monday-orphaned-account-report/config.yml +0 -0
  83. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/examples/monday-orphaned-account-report/env.template +0 -0
  84. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/examples/monday-orphaned-account-report/run.py +0 -0
  85. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/storage_cli.py +0 -0
  86. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/utils/__init__.py +0 -0
  87. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/utils/entra.py +0 -0
  88. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/utils/log.py +0 -0
  89. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/utils/send_email.py +0 -0
  90. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/utils/send_email_graph.py +0 -0
  91. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/src/pdt/utils/web.py +0 -0
  92. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/conftest.py +0 -0
  93. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_aws_account.py +0 -0
  94. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_aws_profile.py +0 -0
  95. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_aws_storage.py +0 -0
  96. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_azure_build.py +0 -0
  97. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_azure_environment_key.py +0 -0
  98. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_azure_group_ownership.py +0 -0
  99. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_azure_job_diagnostics.py +0 -0
  100. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_azure_names.py +0 -0
  101. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_azure_retries.py +0 -0
  102. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_azure_side_resources.py +0 -0
  103. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_azure_storage.py +0 -0
  104. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_azure_subscription_state.py +0 -0
  105. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_console.py +0 -0
  106. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_cost_estimate.py +0 -0
  107. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_env_secret.py +0 -0
  108. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_env_sources.py +0 -0
  109. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_fetch_json.py +0 -0
  110. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_google_cloud_inventory.py +0 -0
  111. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_google_cloud_not_found.py +0 -0
  112. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_google_cloud_service_account.py +0 -0
  113. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_init_questions.py +0 -0
  114. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_log_colour.py +0 -0
  115. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_project_discovery.py +0 -0
  116. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_run_log_links.py +0 -0
  117. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_scaffold.py +0 -0
  118. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_schedule.py +0 -0
  119. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_secrets.py +0 -0
  120. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_starter_app.py +0 -0
  121. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_storage_cli.py +0 -0
  122. {pdt_cli-0.1.2 → pdt_cli-0.1.3}/tests/test_strict_validation.py +0 -0
@@ -16,6 +16,7 @@ dist/
16
16
  .pytest_cache/
17
17
  .uv-cache/
18
18
  claude-task-report/
19
+ winget/rendered/
19
20
 
20
21
  # OS / editor noise
21
22
  .DS_Store
pdt_cli-0.1.3/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AutoIDM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pdt-cli
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Easily deploy your process to AWS, Azure, Google Cloud, or Windows.
5
5
  License-Expression: MIT
6
+ License-File: LICENSE
6
7
  Requires-Python: >=3.12
7
8
  Requires-Dist: argcomplete
8
9
  Requires-Dist: backoff
@@ -26,7 +27,7 @@ Requires-Dist: gcsfs; extra == 'apps'
26
27
  Requires-Dist: google-cloud-secret-manager; extra == 'apps'
27
28
  Requires-Dist: google-cloud-storage; extra == 'apps'
28
29
  Requires-Dist: pyjwt; extra == 'apps'
29
- Requires-Dist: s3fs; extra == 'apps'
30
+ Requires-Dist: s3fs>=2024; extra == 'apps'
30
31
  Description-Content-Type: text/markdown
31
32
 
32
33
  # pdt
@@ -41,7 +42,7 @@ uv tool install pdt-cli
41
42
 
42
43
  That puts a `pdt` command on your PATH. To update it later, run `uv tool upgrade pdt-cli`.
43
44
 
44
- The first interactive `pdt` command turns on tab completion without a prompt. It works in every new terminal: Tab finishes pdt commands, their options, your app names, and example names for `new --from`. It supports bash, zsh, fish, and PowerShell. `pdt completion [SHELL]` sets up a second shell, and `pdt completion --script bash` prints the script instead of writing it, if you keep your own dotfiles.
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.
45
46
 
46
47
  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.
47
48
 
@@ -121,6 +122,7 @@ pdt login my-report
121
122
  | `pdt validate` | check the config files and the required env vars |
122
123
  | `pdt run APP` | run an app on this machine |
123
124
  | `pdt deploy APP` | deploy an app to its configured platform |
125
+ | `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 |
124
126
  | `pdt destroy APP` | remove everything deploy created |
125
127
  | `pdt secrets APP` | show which `.env` values differ from the deployed app |
126
128
  | `pdt secrets APP save` | send your `.env` values to the deployed app; the next run uses them |
@@ -128,6 +130,9 @@ pdt login my-report
128
130
  | `pdt secrets APP set NAME` | put one value, read from stdin, into the deployed app's secrets |
129
131
  | `pdt login APP` | sign in again to the app's cloud provider |
130
132
  | `pdt storage APP ls|get|query|destroy` | look at, fetch, query, or delete the app's stored files |
133
+ | `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 |
135
+ | `pdt health [APP]` | show whether each app's last run succeeded; exits 1 when one failed |
131
136
  | `pdt az ...` | run the Azure CLI that pdt installs |
132
137
  | `pdt gcloud ...` | run the Google Cloud CLI that pdt installs |
133
138
  | `pdt completion [SHELL]` | turn on tab completion for a shell |
@@ -140,7 +145,7 @@ Leave `APP` off `run`, `deploy`, `destroy`, `secrets`, or `login`, or mistype it
140
145
 
141
146
  Your app runs, writes some files, and stops. Then the computer it ran on is thrown away, and the files go with it. So pdt gives every app a folder in your cloud account that stays. Deploy the app, destroy it, deploy it again: the folder and everything in it is still there.
142
147
 
143
- pdt creates one bucket per cloud account, named `pdt-data-` plus a short code, and gives each app its own folder inside it. When your app runs in the cloud, `PDT_STORAGE_URL` points at that folder. When you run `pdt run APP` on your own computer, it points at `.pdt/storage/APP/` inside your project instead. The app code is the same in both places.
148
+ pdt creates one bucket per cloud account, named `pdt-data-` plus a short code, and gives each app its own folder inside it. When your app runs in the cloud, `PDT_STORAGE_URL` points at that folder. A Windows scheduled task points it at `%ProgramData%\pdt\APP\storage\`. When you run `pdt run APP` on your own computer, it points at `.pdt/storage/APP/` inside your project instead. The app code is the same in every place.
144
149
 
145
150
  Write and read files with `pdt.utils.storage`:
146
151
 
@@ -183,7 +188,7 @@ Every command reads its values from the environment it runs in. A build server s
183
188
 
184
189
  ## Choosing where jobs run
185
190
 
186
- 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.
191
+ 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.
187
192
 
188
193
  ### Azure
189
194
 
@@ -228,7 +233,7 @@ platform:
228
233
 
229
234
  ### Bringing your own Dockerfile
230
235
 
231
- Every cloud provider runs a job as a container: AWS on Fargate, Azure on Container Apps Jobs, Google Cloud on Cloud Run Jobs. The deploy builds an image for each app from a generated Dockerfile. It copies the app folder and `pdt.yml` into `/workspace`, installs the script-header dependencies of `run.py` with `uv sync --script`, and runs `run.py` as the entrypoint.
236
+ Every cloud provider runs a job as a container: AWS on Batch (Fargate), Azure on Container Apps Jobs, Google Cloud on Cloud Run Jobs. The deploy builds an image for each app from a generated Dockerfile. It copies the app folder and `pdt.yml` into `/workspace`, installs the script-header dependencies of `run.py` with `uv sync --script`, and runs `run.py` as the entrypoint.
232
237
 
233
238
  Put a `Dockerfile` in the app folder to build the image your own way, for example to add system packages or to install a heavy tool at build time instead of on every run. The build context is the same as the generated one: the app folder under its own name, next to `pdt.yml`. Start from the generated file:
234
239
 
@@ -236,7 +241,7 @@ Put a `Dockerfile` in the app folder to build the image your own way, for exampl
236
241
  FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
237
242
  COPY . /workspace
238
243
  WORKDIR /workspace/my-app
239
- ENV PDT_PROJECT=/workspace
244
+ ENV PDT_PROJECT=/workspace NO_COLOR=1 DBT_USE_COLORS=false
240
245
  RUN uv sync --script run.py
241
246
  ENTRYPOINT ["uv", "run", "--script", "run.py"]
242
247
  ```
@@ -253,10 +258,11 @@ platform:
253
258
 
254
259
  apps:
255
260
  - name: my-report
256
- timezone: local
257
261
  schedule: daily
258
262
  ```
259
263
 
264
+ The Windows provider runs each job on the PC's local time, so `timezone` defaults to `local` there, the only value it accepts.
265
+
260
266
  The job runs as the SYSTEM account. Windows accepts these schedules:
261
267
 
262
268
  - `hourly`, `daily`, `weekly`, `monthly`, and `yearly`
@@ -267,6 +273,8 @@ The job runs as the SYSTEM account. Windows accepts these schedules:
267
273
 
268
274
  Other cron forms are rejected, because they do not translate to Windows Task Scheduler.
269
275
 
276
+ Each app keeps its run data in `%ProgramData%\pdt\<app>\`. The task writes one log file per run to the `logs\` folder in it, named by the run's UTC start time, and `pdt runs <app>` and `pdt logs <app>` read those files. The app's own files (see [Keeping files between runs](#keeping-files-between-runs)) live in the `storage\` folder next to it. Deploy creates the folder and gives the SYSTEM account full control and you modify rights, so `pdt storage <app> destroy` works without an administrator. `pdt destroy <app>` removes the task and the `logs\` folder and keeps `storage\`. A run is stopped after 30 minutes, the same limit as the cloud providers, and a run that is still going when the next one is due is left alone; the new start is skipped.
277
+
270
278
  ## The bundled examples
271
279
 
272
280
  Run `pdt examples` to list them, then `pdt new <name> --from <example>` to copy one.
@@ -290,9 +298,11 @@ Reports active Monday users whose email address does not match the `userPrincipa
290
298
  | Item | Where |
291
299
  | --- | --- |
292
300
  | the `pdt` command and its code | the tool's own environment, from `uv tool install` |
301
+ | the `pdt` launcher from winget | `%LOCALAPPDATA%\Microsoft\WinGet\Packages`, linked from `%LOCALAPPDATA%\Microsoft\WinGet\Links` |
293
302
  | the example apps | inside the pdt package, copied out by `pdt new` |
294
303
  | your apps, `pdt.yml`, `.env` | your project folder, under version control |
295
304
  | the Google Cloud CLI pdt downloads | `~/.local/share/pdt/gcloud`, or `%LOCALAPPDATA%\pdt\gcloud` |
305
+ | a Windows scheduled task's run logs and files | `%ProgramData%\pdt\<app>\logs` and `%ProgramData%\pdt\<app>\storage` |
296
306
  | cloud sign-in state | `~/.azure` and `~/.config/gcloud`, as usual |
297
307
 
298
308
  Set `PDT_PROJECT` to name the project folder directly, instead of letting pdt search upward. Deployed jobs get it set for them.
@@ -304,7 +314,7 @@ An app is a folder with a `run.py` that has a `main()` function. It declares its
304
314
  ```python
305
315
  # /// script
306
316
  # requires-python = ">=3.12"
307
- # dependencies = ["pdt-cli[apps]==0.1.2"]
317
+ # dependencies = ["pdt-cli[apps]==0.1.3"]
308
318
  # ///
309
319
  from pathlib import Path
310
320
 
@@ -10,7 +10,7 @@ uv tool install pdt-cli
10
10
 
11
11
  That puts a `pdt` command on your PATH. To update it later, run `uv tool upgrade pdt-cli`.
12
12
 
13
- The first interactive `pdt` command turns on tab completion without a prompt. It works in every new terminal: Tab finishes pdt commands, their options, your app names, and example names for `new --from`. It supports bash, zsh, fish, and PowerShell. `pdt completion [SHELL]` sets up a second shell, and `pdt completion --script bash` prints the script instead of writing it, if you keep your own dotfiles.
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
14
 
15
15
  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
16
 
@@ -90,6 +90,7 @@ pdt login my-report
90
90
  | `pdt validate` | check the config files and the required env vars |
91
91
  | `pdt run APP` | run an app on this machine |
92
92
  | `pdt deploy APP` | deploy an app to its configured platform |
93
+ | `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 |
93
94
  | `pdt destroy APP` | remove everything deploy created |
94
95
  | `pdt secrets APP` | show which `.env` values differ from the deployed app |
95
96
  | `pdt secrets APP save` | send your `.env` values to the deployed app; the next run uses them |
@@ -97,6 +98,9 @@ pdt login my-report
97
98
  | `pdt secrets APP set NAME` | put one value, read from stdin, into the deployed app's secrets |
98
99
  | `pdt login APP` | sign in again to the app's cloud provider |
99
100
  | `pdt storage APP ls|get|query|destroy` | look at, fetch, query, or delete the app's stored files |
101
+ | `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 |
103
+ | `pdt health [APP]` | show whether each app's last run succeeded; exits 1 when one failed |
100
104
  | `pdt az ...` | run the Azure CLI that pdt installs |
101
105
  | `pdt gcloud ...` | run the Google Cloud CLI that pdt installs |
102
106
  | `pdt completion [SHELL]` | turn on tab completion for a shell |
@@ -109,7 +113,7 @@ Leave `APP` off `run`, `deploy`, `destroy`, `secrets`, or `login`, or mistype it
109
113
 
110
114
  Your app runs, writes some files, and stops. Then the computer it ran on is thrown away, and the files go with it. So pdt gives every app a folder in your cloud account that stays. Deploy the app, destroy it, deploy it again: the folder and everything in it is still there.
111
115
 
112
- pdt creates one bucket per cloud account, named `pdt-data-` plus a short code, and gives each app its own folder inside it. When your app runs in the cloud, `PDT_STORAGE_URL` points at that folder. When you run `pdt run APP` on your own computer, it points at `.pdt/storage/APP/` inside your project instead. The app code is the same in both places.
116
+ pdt creates one bucket per cloud account, named `pdt-data-` plus a short code, and gives each app its own folder inside it. When your app runs in the cloud, `PDT_STORAGE_URL` points at that folder. A Windows scheduled task points it at `%ProgramData%\pdt\APP\storage\`. When you run `pdt run APP` on your own computer, it points at `.pdt/storage/APP/` inside your project instead. The app code is the same in every place.
113
117
 
114
118
  Write and read files with `pdt.utils.storage`:
115
119
 
@@ -152,7 +156,7 @@ Every command reads its values from the environment it runs in. A build server s
152
156
 
153
157
  ## Choosing where jobs run
154
158
 
155
- 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.
159
+ 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.
156
160
 
157
161
  ### Azure
158
162
 
@@ -197,7 +201,7 @@ platform:
197
201
 
198
202
  ### Bringing your own Dockerfile
199
203
 
200
- Every cloud provider runs a job as a container: AWS on Fargate, Azure on Container Apps Jobs, Google Cloud on Cloud Run Jobs. The deploy builds an image for each app from a generated Dockerfile. It copies the app folder and `pdt.yml` into `/workspace`, installs the script-header dependencies of `run.py` with `uv sync --script`, and runs `run.py` as the entrypoint.
204
+ Every cloud provider runs a job as a container: AWS on Batch (Fargate), Azure on Container Apps Jobs, Google Cloud on Cloud Run Jobs. The deploy builds an image for each app from a generated Dockerfile. It copies the app folder and `pdt.yml` into `/workspace`, installs the script-header dependencies of `run.py` with `uv sync --script`, and runs `run.py` as the entrypoint.
201
205
 
202
206
  Put a `Dockerfile` in the app folder to build the image your own way, for example to add system packages or to install a heavy tool at build time instead of on every run. The build context is the same as the generated one: the app folder under its own name, next to `pdt.yml`. Start from the generated file:
203
207
 
@@ -205,7 +209,7 @@ Put a `Dockerfile` in the app folder to build the image your own way, for exampl
205
209
  FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
206
210
  COPY . /workspace
207
211
  WORKDIR /workspace/my-app
208
- ENV PDT_PROJECT=/workspace
212
+ ENV PDT_PROJECT=/workspace NO_COLOR=1 DBT_USE_COLORS=false
209
213
  RUN uv sync --script run.py
210
214
  ENTRYPOINT ["uv", "run", "--script", "run.py"]
211
215
  ```
@@ -222,10 +226,11 @@ platform:
222
226
 
223
227
  apps:
224
228
  - name: my-report
225
- timezone: local
226
229
  schedule: daily
227
230
  ```
228
231
 
232
+ The Windows provider runs each job on the PC's local time, so `timezone` defaults to `local` there, the only value it accepts.
233
+
229
234
  The job runs as the SYSTEM account. Windows accepts these schedules:
230
235
 
231
236
  - `hourly`, `daily`, `weekly`, `monthly`, and `yearly`
@@ -236,6 +241,8 @@ The job runs as the SYSTEM account. Windows accepts these schedules:
236
241
 
237
242
  Other cron forms are rejected, because they do not translate to Windows Task Scheduler.
238
243
 
244
+ Each app keeps its run data in `%ProgramData%\pdt\<app>\`. The task writes one log file per run to the `logs\` folder in it, named by the run's UTC start time, and `pdt runs <app>` and `pdt logs <app>` read those files. The app's own files (see [Keeping files between runs](#keeping-files-between-runs)) live in the `storage\` folder next to it. Deploy creates the folder and gives the SYSTEM account full control and you modify rights, so `pdt storage <app> destroy` works without an administrator. `pdt destroy <app>` removes the task and the `logs\` folder and keeps `storage\`. A run is stopped after 30 minutes, the same limit as the cloud providers, and a run that is still going when the next one is due is left alone; the new start is skipped.
245
+
239
246
  ## The bundled examples
240
247
 
241
248
  Run `pdt examples` to list them, then `pdt new <name> --from <example>` to copy one.
@@ -259,9 +266,11 @@ Reports active Monday users whose email address does not match the `userPrincipa
259
266
  | Item | Where |
260
267
  | --- | --- |
261
268
  | the `pdt` command and its code | the tool's own environment, from `uv tool install` |
269
+ | the `pdt` launcher from winget | `%LOCALAPPDATA%\Microsoft\WinGet\Packages`, linked from `%LOCALAPPDATA%\Microsoft\WinGet\Links` |
262
270
  | the example apps | inside the pdt package, copied out by `pdt new` |
263
271
  | your apps, `pdt.yml`, `.env` | your project folder, under version control |
264
272
  | the Google Cloud CLI pdt downloads | `~/.local/share/pdt/gcloud`, or `%LOCALAPPDATA%\pdt\gcloud` |
273
+ | a Windows scheduled task's run logs and files | `%ProgramData%\pdt\<app>\logs` and `%ProgramData%\pdt\<app>\storage` |
265
274
  | cloud sign-in state | `~/.azure` and `~/.config/gcloud`, as usual |
266
275
 
267
276
  Set `PDT_PROJECT` to name the project folder directly, instead of letting pdt search upward. Deployed jobs get it set for them.
@@ -273,7 +282,7 @@ An app is a folder with a `run.py` that has a `main()` function. It declares its
273
282
  ```python
274
283
  # /// script
275
284
  # requires-python = ">=3.12"
276
- # dependencies = ["pdt-cli[apps]==0.1.2"]
285
+ # dependencies = ["pdt-cli[apps]==0.1.3"]
277
286
  # ///
278
287
  from pathlib import Path
279
288
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pdt-cli"
3
- version = "0.1.2"
3
+ version = "0.1.3"
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"
@@ -25,7 +25,7 @@ apps = [
25
25
  "boto3",
26
26
  "google-cloud-secret-manager",
27
27
  "fsspec",
28
- "s3fs",
28
+ "s3fs>=2024",
29
29
  "gcsfs",
30
30
  "adlfs",
31
31
  "google-cloud-storage",
@@ -58,7 +58,7 @@ include = ["src/**/*.py"]
58
58
  extend-exclude = ["src/pdt/examples"]
59
59
 
60
60
  [tool.ruff.lint]
61
- select = ["T20"]
61
+ select = ["T20", "F821"]
62
62
 
63
63
  [tool.ruff.lint.per-file-ignores]
64
64
  "src/pdt/utils/log.py" = ["T20"]
@@ -18,6 +18,7 @@ import rich_argparse
18
18
 
19
19
  from pdt import __version__, completion, config, console, deploy, deploy_common, scaffold
20
20
  from pdt.config import ConfigError
21
+ from pdt.utils.email_auth import can_prompt
21
22
  from pdt.utils.send_email import auth_env_file, email_problems, prepare_email_auth
22
23
 
23
24
  CLOUD_CLIS = {
@@ -52,8 +53,17 @@ APP_QUESTIONS = {
52
53
  "login": "Which app's cloud provider do you want to sign in to?",
53
54
  "destroy": "Which app do you want to destroy?",
54
55
  "secrets": "Which app's secrets?",
56
+ "storage": "Which app's files do you want to manage?",
57
+ "runs": "Which app's runs do you want to see?",
58
+ "logs": "Which app's log do you want to read?",
59
+ "health": "Which app do you want to check?",
55
60
  }
56
61
 
62
+ SINCE_HELP = ("show every run that started at or after this: 12h, 3d, 2w, 2026-09-20, "
63
+ "or 2026-09-20T14:00 (default: the 10 newest runs)")
64
+ SPAN_HELP = "with --since, show only the runs that started within this long after it: 12h, 3d, 2w"
65
+ COUNT_HELP = "show at most this many runs (default: 10 without --since, else every run)"
66
+
57
67
 
58
68
  def choose_app(name: str | None, command: str) -> str | None:
59
69
  """Return the app `pdt <command>` should act on, or None after guiding the user.
@@ -156,12 +166,51 @@ def cmd_run(args) -> int:
156
166
 
157
167
 
158
168
  def cmd_deploy(args) -> int:
169
+ if args.skip_failures and not args.all:
170
+ console.error("--skip-failures only works with --all")
171
+ return 1
172
+ if args.all:
173
+ if args.app is not None:
174
+ console.error("pick an app or --all, not both")
175
+ return 1
176
+ return deploy_all(args.yes, args.skip_failures)
159
177
  name = choose_app(args.app, "deploy")
160
178
  if name is None:
161
179
  return 1
162
180
  return deploy.deploy(name, assume_yes=args.yes)
163
181
 
164
182
 
183
+ def deploy_all(assume_yes: bool, skip_failures: bool) -> int:
184
+ names = config.find_apps()
185
+ if not names:
186
+ say_no_apps()
187
+ return 1
188
+ ask = not skip_failures and can_prompt(None)
189
+ failed: list[str] = []
190
+ for index, name in enumerate(names, 1):
191
+ console.heading(f"Deploying {name} ({index} of {len(names)})")
192
+ code = deploy.deploy(name, assume_yes=assume_yes)
193
+ if code == 0:
194
+ continue
195
+ failed.append(name)
196
+ console.error(f"{name} did not deploy.")
197
+ if index < len(names) and not skip_failures and not (
198
+ ask and console.confirm(
199
+ f"Skip the failing app {name} and deploy the rest?")):
200
+ console.say("Fix the problem above and run pdt deploy --all again, "
201
+ "or add --skip-failures to go on past it.")
202
+ return code
203
+ if ask and not assume_yes and console.confirm(
204
+ f"Disable the failing app {name}?"):
205
+ path = config.set_app_enabled(name, False)
206
+ console.done(f"Disabled {name} in {path.relative_to(config.find_project())}. "
207
+ "Set enabled: true there to bring it back.")
208
+ if failed:
209
+ console.warn(f"Not deployed: {', '.join(failed)}")
210
+ return 1
211
+ return 0
212
+
213
+
165
214
  def cmd_login(args) -> int:
166
215
  name = choose_app(args.app, "login")
167
216
  if name is None:
@@ -184,7 +233,48 @@ def cmd_secrets(args) -> int:
184
233
 
185
234
 
186
235
  def cmd_storage(args) -> int:
187
- return deploy.storage(args.app, args.rest)
236
+ name = choose_app(args.app, "storage")
237
+ if name is None:
238
+ return 1
239
+ return deploy.storage(name, args.rest)
240
+
241
+
242
+ def window_options(args) -> list[str]:
243
+ return [part for option, value in (("--since", args.since), ("--span", args.span),
244
+ ("--count", args.count)) if value is not None
245
+ for part in (option, str(value))]
246
+
247
+
248
+ def cmd_runs(args) -> int:
249
+ name = choose_app(args.app, "runs")
250
+ if name is None:
251
+ return 1
252
+ return deploy.runs(name, [*window_options(args), *(["--json"] if args.json else [])])
253
+
254
+
255
+ def cmd_logs(args) -> int:
256
+ name = choose_app(args.app, "logs")
257
+ if name is None:
258
+ return 1
259
+ flags = [flag for flag, on in (("--failed", args.failed), ("--errors", args.errors),
260
+ ("--head", args.head), ("--full", args.full),
261
+ ("--json", args.json)) if on]
262
+ lines = [] if args.lines is None else ["--lines", str(args.lines)]
263
+ return deploy.logs(name, [str(args.number), *window_options(args), *lines, *flags])
264
+
265
+
266
+ def cmd_health(args) -> int:
267
+ if args.app is None:
268
+ names = config.find_apps()
269
+ if not names:
270
+ say_no_apps()
271
+ return 0
272
+ else:
273
+ name = choose_app(args.app, "health")
274
+ if name is None:
275
+ return 1
276
+ names = [name]
277
+ return deploy.health(names, args.json)
188
278
 
189
279
 
190
280
  def cmd_completion(args) -> int:
@@ -232,6 +322,9 @@ def build_parser() -> argparse.ArgumentParser:
232
322
  help="the app's folder name; omit to see the choices")
233
323
  app.completer = completion.apps
234
324
  p.add_argument("--yes", action="store_true", help="skip the confirmation prompt")
325
+ p.add_argument("--all", action="store_true", help="deploy every enabled app, in order")
326
+ p.add_argument("--skip-failures", action="store_true",
327
+ help="with --all, go on past an app that fails to deploy instead of asking")
235
328
  p.set_defaults(func=cmd_deploy)
236
329
  p = add_parser("login", help="sign in again to an app's cloud provider")
237
330
  app = p.add_argument("app", nargs="?",
@@ -257,10 +350,44 @@ def build_parser() -> argparse.ArgumentParser:
257
350
  p.add_argument("--yes", action="store_true", help="skip the confirmation prompt")
258
351
  p.set_defaults(func=cmd_secrets)
259
352
  p = add_parser("storage", help="read or manage an app's data store")
260
- app = p.add_argument("app", help="the app's folder name")
353
+ app = p.add_argument("app", nargs="?",
354
+ help="the app's folder name; omit to see the choices")
261
355
  app.completer = completion.apps
262
356
  p.add_argument("rest", nargs=argparse.REMAINDER, help="ls|get|query|destroy [args...]")
263
357
  p.set_defaults(func=cmd_storage)
358
+ p = add_parser("runs", help="list a deployed app's recent runs")
359
+ app = p.add_argument("app", nargs="?",
360
+ help="the app's folder name; omit to see the choices")
361
+ app.completer = completion.apps
362
+ p.add_argument("--since", help=SINCE_HELP)
363
+ p.add_argument("--span", help=SPAN_HELP)
364
+ p.add_argument("--count", type=int, help=COUNT_HELP)
365
+ p.add_argument("--json", action="store_true", help="print JSON for a script or an agent")
366
+ p.set_defaults(func=cmd_runs)
367
+ p = add_parser("logs", help="read the log of one of a deployed app's runs")
368
+ app = p.add_argument("app", nargs="?",
369
+ help="the app's folder name; omit to see the choices")
370
+ app.completer = completion.apps
371
+ p.add_argument("number", nargs="?", type=int, default=1,
372
+ help="which run, as `pdt runs` numbers them (default: 1, the newest)")
373
+ p.add_argument("--since", help=SINCE_HELP)
374
+ p.add_argument("--span", help=SPAN_HELP)
375
+ p.add_argument("--count", type=int, help=COUNT_HELP)
376
+ p.add_argument("--failed", action="store_true", help="read the newest failed run")
377
+ p.add_argument("--errors", action="store_true",
378
+ help="leave out DEBUG and INFO lines")
379
+ p.add_argument("--lines", type=int, help="print this many lines (default: 20)")
380
+ p.add_argument("--head", action="store_true",
381
+ help="print the first lines, not the last")
382
+ p.add_argument("--full", action="store_true",
383
+ help="print every line, not only the last 20")
384
+ p.add_argument("--json", action="store_true", help="print JSON for a script or an agent")
385
+ p.set_defaults(func=cmd_logs)
386
+ p = add_parser("health", help="show whether each deployed app's last run succeeded")
387
+ app = p.add_argument("app", nargs="?", help="one app's folder name; omit to check every app")
388
+ app.completer = completion.apps
389
+ p.add_argument("--json", action="store_true", help="print JSON for a script or an agent")
390
+ p.set_defaults(func=cmd_health)
264
391
  p = add_parser("completion", help="turn on tab completion in your shell")
265
392
  p.add_argument("shell", nargs="?", choices=completion.SHELLS,
266
393
  help="which shell; pdt works it out when left off")
@@ -277,10 +404,12 @@ def main() -> int:
277
404
  parser = build_parser()
278
405
  completion.configure(parser)
279
406
  if len(sys.argv) > 1 and sys.argv[1] in CLOUD_CLIS:
280
- # Before argparse, so the cloud CLI parses its own flags.
407
+ # Before argparse, so the cloud CLI parses its own flags. --project keeps
408
+ # `uvx pdt aws` working; deploy.provider_command explains why.
281
409
  script = Path(__file__).with_name(CLOUD_CLIS[sys.argv[1]])
282
410
  return subprocess.run(
283
- ["uv", "run", "--script", str(script), *sys.argv[1:]]).returncode
411
+ ["uv", "run", "--project", str(Path.cwd()), "--script", str(script),
412
+ *sys.argv[1:]]).returncode
284
413
  args = parser.parse_args()
285
414
  if args.command is None:
286
415
  parser.print_help()
@@ -132,7 +132,7 @@ def setup(shell: str | None = None) -> None:
132
132
 
133
133
 
134
134
  def configure(parser) -> None:
135
- argcomplete.autocomplete(parser)
135
+ argcomplete.autocomplete(parser, always_complete_options=False)
136
136
  if not (sys.stdin.isatty() and sys.stderr.isatty()):
137
137
  return
138
138
  try: