pdt-cli 0.1.0__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 (43) hide show
  1. pdt_cli-0.1.0/.gitignore +23 -0
  2. pdt_cli-0.1.0/PKG-INFO +325 -0
  3. pdt_cli-0.1.0/README.md +307 -0
  4. pdt_cli-0.1.0/pyproject.toml +41 -0
  5. pdt_cli-0.1.0/src/pdt/__init__.py +6 -0
  6. pdt_cli-0.1.0/src/pdt/cli.py +177 -0
  7. pdt_cli-0.1.0/src/pdt/config.py +394 -0
  8. pdt_cli-0.1.0/src/pdt/deploy.py +106 -0
  9. pdt_cli-0.1.0/src/pdt/deploy_aws.py +613 -0
  10. pdt_cli-0.1.0/src/pdt/deploy_aws_fargate.py +449 -0
  11. pdt_cli-0.1.0/src/pdt/deploy_aws_lambda.py +359 -0
  12. pdt_cli-0.1.0/src/pdt/deploy_azure.py +531 -0
  13. pdt_cli-0.1.0/src/pdt/deploy_azure_container_apps.py +328 -0
  14. pdt_cli-0.1.0/src/pdt/deploy_azure_functions.py +307 -0
  15. pdt_cli-0.1.0/src/pdt/deploy_common.py +94 -0
  16. pdt_cli-0.1.0/src/pdt/deploy_google_cloud.py +756 -0
  17. pdt_cli-0.1.0/src/pdt/deploy_windows.py +376 -0
  18. pdt_cli-0.1.0/src/pdt/examples/hello-world/config.yml +20 -0
  19. pdt_cli-0.1.0/src/pdt/examples/hello-world/run.py +41 -0
  20. pdt_cli-0.1.0/src/pdt/examples/impossible-travel-report/config.yml +48 -0
  21. pdt_cli-0.1.0/src/pdt/examples/impossible-travel-report/env.template +57 -0
  22. pdt_cli-0.1.0/src/pdt/examples/impossible-travel-report/run.py +232 -0
  23. pdt_cli-0.1.0/src/pdt/examples/monday-orphaned-account-report/config.yml +47 -0
  24. pdt_cli-0.1.0/src/pdt/examples/monday-orphaned-account-report/env.template +57 -0
  25. pdt_cli-0.1.0/src/pdt/examples/monday-orphaned-account-report/run.py +333 -0
  26. pdt_cli-0.1.0/src/pdt/gcloud_sdk.py +170 -0
  27. pdt_cli-0.1.0/src/pdt/scaffold.py +278 -0
  28. pdt_cli-0.1.0/src/pdt/utils/__init__.py +0 -0
  29. pdt_cli-0.1.0/src/pdt/utils/email_auth.py +406 -0
  30. pdt_cli-0.1.0/src/pdt/utils/entra.py +114 -0
  31. pdt_cli-0.1.0/src/pdt/utils/log.py +34 -0
  32. pdt_cli-0.1.0/src/pdt/utils/send_email.py +467 -0
  33. pdt_cli-0.1.0/src/pdt/utils/send_email_graph.py +266 -0
  34. pdt_cli-0.1.0/src/pdt/utils/web.py +28 -0
  35. pdt_cli-0.1.0/tests/conftest.py +18 -0
  36. pdt_cli-0.1.0/tests/test_aws_account.py +38 -0
  37. pdt_cli-0.1.0/tests/test_config_merge.py +75 -0
  38. pdt_cli-0.1.0/tests/test_init_questions.py +58 -0
  39. pdt_cli-0.1.0/tests/test_project_discovery.py +52 -0
  40. pdt_cli-0.1.0/tests/test_save_platform_key.py +79 -0
  41. pdt_cli-0.1.0/tests/test_scaffold.py +124 -0
  42. pdt_cli-0.1.0/tests/test_schedule.py +45 -0
  43. pdt_cli-0.1.0/tests/test_starter_app.py +59 -0
@@ -0,0 +1,23 @@
1
+ # Secrets — never commit these
2
+ .env
3
+ .env.*
4
+ *.json.key
5
+ *-key.json
6
+ service-account*.json
7
+
8
+ # Local state
9
+ .secrets/*
10
+ !.secrets/.gitkeep
11
+ .venv/
12
+ __pycache__/
13
+ .uv/
14
+ dist/
15
+ *.egg-info/
16
+ .pytest_cache/
17
+ .uv-cache/
18
+
19
+ # OS / editor noise
20
+ .DS_Store
21
+ .vscode/
22
+ .idea/
23
+ .worktrees
pdt_cli-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,325 @@
1
+ Metadata-Version: 2.5
2
+ Name: pdt-cli
3
+ Version: 0.1.0
4
+ Summary: Validate, run, and deploy small scheduled jobs to AWS, Azure, Google Cloud, or Windows.
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: google-auth
8
+ Requires-Dist: google-auth-oauthlib
9
+ Requires-Dist: msal
10
+ Requires-Dist: python-dotenv
11
+ Requires-Dist: pyyaml
12
+ Provides-Extra: apps
13
+ Requires-Dist: boto3; extra == 'apps'
14
+ Requires-Dist: cryptography; extra == 'apps'
15
+ Requires-Dist: google-cloud-secret-manager; extra == 'apps'
16
+ Requires-Dist: pyjwt; extra == 'apps'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # pdt
20
+
21
+ Run scheduled jobs — reports, integrations, automations — and deploy them to AWS, Azure, Google Cloud, or Windows Task Scheduler with one command.
22
+
23
+ ## Install
24
+
25
+ ```
26
+ uv tool install pdt-cli
27
+ ```
28
+
29
+ That puts a `pdt` command on your PATH. To update it later, run `uv tool upgrade pdt-cli`.
30
+
31
+ 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.
32
+
33
+ ## Set up a project
34
+
35
+ 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.
36
+
37
+ ```
38
+ pdt init my-jobs
39
+ ```
40
+
41
+ `pdt init` asks where the project should live, which cloud you want, and which region. It warns you if you are about to create a project somewhere unwise, such as your home folder. Run `pdt init` with no folder name to use the current folder, or add `--yes` to take the defaults and answer nothing.
42
+
43
+ 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.
44
+
45
+ Starting in an empty folder also gives you a working app called `hello-world`. Run it straight away:
46
+
47
+ ```
48
+ cd my-jobs
49
+ pdt run hello-world
50
+ ```
51
+
52
+ It writes one log line and nothing else. Edit `hello-world/run.py` to make it do real work, or delete the folder if you would rather start from one of the examples below. A folder that already holds your own files gets no starter app.
53
+
54
+ ## Add an app
55
+
56
+ ```
57
+ pdt examples
58
+ pdt new my-report --from impossible-travel-report
59
+ ```
60
+
61
+ `pdt new` copies one of the examples bundled with pdt into your project. Open `my-report/env.template`, copy the names you need into `.env`, then:
62
+
63
+ ```
64
+ pdt validate
65
+ pdt run my-report
66
+ ```
67
+
68
+ A project ends up looking like this:
69
+
70
+ ```
71
+ my-jobs/
72
+ pdt.yml settings shared by every app
73
+ .env secrets, never committed
74
+ my-report/
75
+ run.py the job
76
+ config.yml schedule, settings, and the env vars it needs
77
+ env.template
78
+ ```
79
+
80
+ ## Deploy
81
+
82
+ ```
83
+ pdt deploy my-report
84
+ ```
85
+
86
+ 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:
87
+
88
+ ```
89
+ pdt destroy my-report
90
+ ```
91
+
92
+ To sign in again, or to switch to a different cloud account:
93
+
94
+ ```
95
+ pdt login my-report
96
+ ```
97
+
98
+ ## Commands
99
+
100
+ | Command | What it does |
101
+ | --- | --- |
102
+ | `pdt init [DIR]` | create a project here, or in DIR |
103
+ | `pdt examples` | list the example apps bundled with pdt |
104
+ | `pdt new APP --from EXAMPLE` | add an app to the project |
105
+ | `pdt list` | show every app with its schedule and provider |
106
+ | `pdt validate` | check the config files and the required env vars |
107
+ | `pdt run APP` | run an app on this machine |
108
+ | `pdt deploy APP` | deploy an app to its configured platform |
109
+ | `pdt destroy APP` | remove everything deploy created |
110
+ | `pdt login APP` | sign in again to the app's cloud provider |
111
+ | `pdt az ...` | run the Azure CLI that pdt installs |
112
+ | `pdt gcloud ...` | run the Google Cloud CLI that pdt installs |
113
+
114
+ `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`.
115
+
116
+ ## Choosing where jobs run
117
+
118
+ 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.
119
+
120
+ ### Azure
121
+
122
+ ```yaml
123
+ platform:
124
+ provider: azure
125
+ subscription: 00000000-0000-0000-0000-000000000000
126
+ region: eastus
127
+ resource_group: pdt
128
+ # Options: functions (default), container_apps
129
+ runtime: functions
130
+ ```
131
+
132
+ `subscription` is optional. When it is missing or wrong, the deploy asks you to choose one.
133
+
134
+ ### AWS
135
+
136
+ ```yaml
137
+ platform:
138
+ provider: aws
139
+ region: us-east-1
140
+ # Written for you on the first deploy, from your credentials.
141
+ account: "123456789012"
142
+ # lambda (default) or fargate
143
+ runtime: lambda
144
+ ```
145
+
146
+ ### Google Cloud
147
+
148
+ ```yaml
149
+ platform:
150
+ provider: google-cloud
151
+ region: us-central1
152
+ project: my-starter-project
153
+ ```
154
+
155
+ `project` is optional. When it is missing or wrong, the deploy lists your projects and asks you to choose one, then writes your answer here.
156
+
157
+ ### Windows Task Scheduler
158
+
159
+ ```yaml
160
+ platform:
161
+ provider: windows
162
+
163
+ apps:
164
+ - name: my-report
165
+ timezone: local
166
+ schedule: daily
167
+ ```
168
+
169
+ The job runs as the SYSTEM account. Windows accepts these schedules:
170
+
171
+ - `hourly`, `daily`, `weekly`, `monthly`, and `yearly`
172
+ - `*/N * * * *` minute intervals where `N` divides 60
173
+ - one fixed time daily
174
+ - one fixed time on selected weekdays
175
+ - one fixed time on selected days of selected months
176
+
177
+ Other cron forms are rejected, because they do not translate to Windows Task Scheduler.
178
+
179
+ ## The bundled examples
180
+
181
+ Run `pdt examples` to list them, then `pdt new <name> --from <example>` to copy one.
182
+
183
+ ### hello-world
184
+
185
+ 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.
186
+
187
+ ### impossible-travel-report
188
+
189
+ 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.
190
+
191
+ Settings for the lookback window, the minimum distance, and the email addresses live in the app's `config.yml`. The env vars it needs are listed there too, and in its `env.template`.
192
+
193
+ ### monday-orphaned-account-report
194
+
195
+ Reports active Monday users whose email address does not match the `userPrincipalName` of an active Entra ID user.
196
+
197
+ ## Where things live
198
+
199
+ | Item | Where |
200
+ | --- | --- |
201
+ | the `pdt` command and its code | the tool's own environment, from `uv tool install` |
202
+ | the example apps | inside the pdt package, copied out by `pdt new` |
203
+ | your apps, `pdt.yml`, `.env` | your project folder, under version control |
204
+ | the Google Cloud CLI pdt downloads | `~/.local/share/pdt/gcloud`, or `%LOCALAPPDATA%\pdt\gcloud` |
205
+ | cloud sign-in state | `~/.azure` and `~/.config/gcloud`, as usual |
206
+
207
+ Set `PDT_PROJECT` to name the project folder directly, instead of letting pdt search upward. Deployed jobs get it set for them.
208
+
209
+ ## Writing your own app
210
+
211
+ An app is a folder with a `run.py` that has a `main()` function. It declares its own dependencies in a script header, and pdt is one of them:
212
+
213
+ ```python
214
+ # /// script
215
+ # requires-python = ">=3.12"
216
+ # dependencies = ["pdt-cli[apps]==0.1.0"]
217
+ # ///
218
+ from pathlib import Path
219
+
220
+ from pdt.config import check_env, load_env, merged_app
221
+ from pdt.utils.log import log
222
+
223
+
224
+ def main() -> int:
225
+ app = merged_app(Path(__file__).resolve().parent.name)
226
+ load_env(app["dir"])
227
+ ...
228
+ ```
229
+
230
+ 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.
231
+
232
+ # Utilities
233
+
234
+ `pdt.utils` provides built-in support for common functionality:
235
+
236
+ - Send emails
237
+ - Logging
238
+
239
+ ## Send Email
240
+
241
+ `pdt.utils.send_email` sends plain-text mail.
242
+
243
+ Supports:
244
+
245
+ - SMTP + password
246
+ - SMTP + OAuth for Microsoft or Google
247
+ - Microsoft Graph delegated Mail.Send
248
+ - SES (Amazon Simple Email Service)
249
+ - Resend
250
+
251
+ ### Configuration
252
+
253
+ #### SMTP
254
+
255
+ Required environment variables:
256
+
257
+ - PDT_SMTP_HOST
258
+ - PDT_SMTP_USER
259
+
260
+ When using password auth:
261
+
262
+ - PDT_SMTP_PASSWORD
263
+
264
+ When using OAuth for Google or Microsoft:
265
+
266
+ - PDT_SMTP_OAUTH_CLIENT_ID
267
+
268
+ Optional:
269
+
270
+ - PDT_SMTP_AUTH = oauth2
271
+ - PDT_SMTP_PORT: Port 587 is the default and uses STARTTLS; port 465 uses implicit TLS.
272
+
273
+ Google also requires:
274
+
275
+ - PDT_SMTP_OAUTH_CLIENT_SECRET
276
+
277
+ Microsoft optionally accepts:
278
+
279
+ - PDT_SMTP_OAUTH_TENANT_ID: Defaults to `common`.
280
+
281
+ PDT infers OAuth when the host is `smtp.gmail.com`, `smtp.office365.com`, or `smtp-mail.outlook.com` and no password is set. If a provider revokes authorization, run or deploy the app from a terminal again.
282
+
283
+ ##### Google OAuth
284
+
285
+ Create a Desktop app OAuth client in the [Google Auth Platform](https://console.cloud.google.com/auth/clients). Set its client ID and client secret in the variables above. The OAuth consent screen must include `https://mail.google.com/` because Google requires that scope for SMTP OAuth.
286
+
287
+ ##### Microsoft OAuth
288
+
289
+ Create an app registration in the [Microsoft Entra admin center](https://entra.microsoft.com/). Add a Mobile and desktop application platform with `http://localhost` as a redirect URI. Add the delegated Office 365 Exchange Online permission `https://outlook.office.com/SMTP.Send` and allow public client flows.
290
+
291
+ Microsoft 365 must also have Authenticated SMTP enabled for the sending mailbox. Use `PDT_SMTP_OAUTH_TENANT_ID` for a tenant-specific registration, or leave it empty to use `common`.
292
+
293
+ #### Microsoft Graph
294
+
295
+ Required environment variables:
296
+
297
+ - PDT_GRAPH_MAIL_USER
298
+ - PDT_GRAPH_MAIL_CLIENT_ID
299
+
300
+ Optional:
301
+
302
+ - PDT_GRAPH_MAIL_TENANT_ID: Defaults to `common`.
303
+
304
+ PDT sends as `PDT_GRAPH_MAIL_USER`, so `email_from` in config.yml must match it. Graph sends the HTML part alone; the other transports send both parts. If a provider revokes authorization, run or deploy the app from a terminal again.
305
+
306
+ Create an app registration in the [Microsoft Entra admin center](https://entra.microsoft.com/). Add a Mobile and desktop application platform with `http://localhost` as a redirect URI. Add the Microsoft Graph delegated permission `Mail.Send` and allow public client flows. Do not add a client secret.
307
+
308
+ #### SES
309
+
310
+ Required environment variables:
311
+
312
+ - PDT_SES_REGION
313
+
314
+ Optional:
315
+
316
+ - PDT_SES_ACCESS_KEY_ID
317
+ - PDT_SES_SECRET_ACCESS_KEY
318
+
319
+ Leave the two SES key variables empty to use the standard AWS credential chain (for example, `AWS_PROFILE`, environment credentials, or a workload role).
320
+
321
+ #### Resend
322
+
323
+ Required environment variables:
324
+
325
+ - PDT_RESEND_API_KEY
@@ -0,0 +1,307 @@
1
+ # pdt
2
+
3
+ Run scheduled jobs — reports, integrations, automations — and deploy them to AWS, Azure, Google Cloud, or Windows Task Scheduler with one command.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ uv tool install pdt-cli
9
+ ```
10
+
11
+ That puts a `pdt` command on your PATH. To update it later, run `uv tool upgrade pdt-cli`.
12
+
13
+ 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.
14
+
15
+ ## Set up a project
16
+
17
+ 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.
18
+
19
+ ```
20
+ pdt init my-jobs
21
+ ```
22
+
23
+ `pdt init` asks where the project should live, which cloud you want, and which region. It warns you if you are about to create a project somewhere unwise, such as your home folder. Run `pdt init` with no folder name to use the current folder, or add `--yes` to take the defaults and answer nothing.
24
+
25
+ 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.
26
+
27
+ Starting in an empty folder also gives you a working app called `hello-world`. Run it straight away:
28
+
29
+ ```
30
+ cd my-jobs
31
+ pdt run hello-world
32
+ ```
33
+
34
+ It writes one log line and nothing else. Edit `hello-world/run.py` to make it do real work, or delete the folder if you would rather start from one of the examples below. A folder that already holds your own files gets no starter app.
35
+
36
+ ## Add an app
37
+
38
+ ```
39
+ pdt examples
40
+ pdt new my-report --from impossible-travel-report
41
+ ```
42
+
43
+ `pdt new` copies one of the examples bundled with pdt into your project. Open `my-report/env.template`, copy the names you need into `.env`, then:
44
+
45
+ ```
46
+ pdt validate
47
+ pdt run my-report
48
+ ```
49
+
50
+ A project ends up looking like this:
51
+
52
+ ```
53
+ my-jobs/
54
+ pdt.yml settings shared by every app
55
+ .env secrets, never committed
56
+ my-report/
57
+ run.py the job
58
+ config.yml schedule, settings, and the env vars it needs
59
+ env.template
60
+ ```
61
+
62
+ ## Deploy
63
+
64
+ ```
65
+ pdt deploy my-report
66
+ ```
67
+
68
+ 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:
69
+
70
+ ```
71
+ pdt destroy my-report
72
+ ```
73
+
74
+ To sign in again, or to switch to a different cloud account:
75
+
76
+ ```
77
+ pdt login my-report
78
+ ```
79
+
80
+ ## Commands
81
+
82
+ | Command | What it does |
83
+ | --- | --- |
84
+ | `pdt init [DIR]` | create a project here, or in DIR |
85
+ | `pdt examples` | list the example apps bundled with pdt |
86
+ | `pdt new APP --from EXAMPLE` | add an app to the project |
87
+ | `pdt list` | show every app with its schedule and provider |
88
+ | `pdt validate` | check the config files and the required env vars |
89
+ | `pdt run APP` | run an app on this machine |
90
+ | `pdt deploy APP` | deploy an app to its configured platform |
91
+ | `pdt destroy APP` | remove everything deploy created |
92
+ | `pdt login APP` | sign in again to the app's cloud provider |
93
+ | `pdt az ...` | run the Azure CLI that pdt installs |
94
+ | `pdt gcloud ...` | run the Google Cloud CLI that pdt installs |
95
+
96
+ `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`.
97
+
98
+ ## Choosing where jobs run
99
+
100
+ 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.
101
+
102
+ ### Azure
103
+
104
+ ```yaml
105
+ platform:
106
+ provider: azure
107
+ subscription: 00000000-0000-0000-0000-000000000000
108
+ region: eastus
109
+ resource_group: pdt
110
+ # Options: functions (default), container_apps
111
+ runtime: functions
112
+ ```
113
+
114
+ `subscription` is optional. When it is missing or wrong, the deploy asks you to choose one.
115
+
116
+ ### AWS
117
+
118
+ ```yaml
119
+ platform:
120
+ provider: aws
121
+ region: us-east-1
122
+ # Written for you on the first deploy, from your credentials.
123
+ account: "123456789012"
124
+ # lambda (default) or fargate
125
+ runtime: lambda
126
+ ```
127
+
128
+ ### Google Cloud
129
+
130
+ ```yaml
131
+ platform:
132
+ provider: google-cloud
133
+ region: us-central1
134
+ project: my-starter-project
135
+ ```
136
+
137
+ `project` is optional. When it is missing or wrong, the deploy lists your projects and asks you to choose one, then writes your answer here.
138
+
139
+ ### Windows Task Scheduler
140
+
141
+ ```yaml
142
+ platform:
143
+ provider: windows
144
+
145
+ apps:
146
+ - name: my-report
147
+ timezone: local
148
+ schedule: daily
149
+ ```
150
+
151
+ The job runs as the SYSTEM account. Windows accepts these schedules:
152
+
153
+ - `hourly`, `daily`, `weekly`, `monthly`, and `yearly`
154
+ - `*/N * * * *` minute intervals where `N` divides 60
155
+ - one fixed time daily
156
+ - one fixed time on selected weekdays
157
+ - one fixed time on selected days of selected months
158
+
159
+ Other cron forms are rejected, because they do not translate to Windows Task Scheduler.
160
+
161
+ ## The bundled examples
162
+
163
+ Run `pdt examples` to list them, then `pdt new <name> --from <example>` to copy one.
164
+
165
+ ### hello-world
166
+
167
+ 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.
168
+
169
+ ### impossible-travel-report
170
+
171
+ 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.
172
+
173
+ Settings for the lookback window, the minimum distance, and the email addresses live in the app's `config.yml`. The env vars it needs are listed there too, and in its `env.template`.
174
+
175
+ ### monday-orphaned-account-report
176
+
177
+ Reports active Monday users whose email address does not match the `userPrincipalName` of an active Entra ID user.
178
+
179
+ ## Where things live
180
+
181
+ | Item | Where |
182
+ | --- | --- |
183
+ | the `pdt` command and its code | the tool's own environment, from `uv tool install` |
184
+ | the example apps | inside the pdt package, copied out by `pdt new` |
185
+ | your apps, `pdt.yml`, `.env` | your project folder, under version control |
186
+ | the Google Cloud CLI pdt downloads | `~/.local/share/pdt/gcloud`, or `%LOCALAPPDATA%\pdt\gcloud` |
187
+ | cloud sign-in state | `~/.azure` and `~/.config/gcloud`, as usual |
188
+
189
+ Set `PDT_PROJECT` to name the project folder directly, instead of letting pdt search upward. Deployed jobs get it set for them.
190
+
191
+ ## Writing your own app
192
+
193
+ An app is a folder with a `run.py` that has a `main()` function. It declares its own dependencies in a script header, and pdt is one of them:
194
+
195
+ ```python
196
+ # /// script
197
+ # requires-python = ">=3.12"
198
+ # dependencies = ["pdt-cli[apps]==0.1.0"]
199
+ # ///
200
+ from pathlib import Path
201
+
202
+ from pdt.config import check_env, load_env, merged_app
203
+ from pdt.utils.log import log
204
+
205
+
206
+ def main() -> int:
207
+ app = merged_app(Path(__file__).resolve().parent.name)
208
+ load_env(app["dir"])
209
+ ...
210
+ ```
211
+
212
+ 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.
213
+
214
+ # Utilities
215
+
216
+ `pdt.utils` provides built-in support for common functionality:
217
+
218
+ - Send emails
219
+ - Logging
220
+
221
+ ## Send Email
222
+
223
+ `pdt.utils.send_email` sends plain-text mail.
224
+
225
+ Supports:
226
+
227
+ - SMTP + password
228
+ - SMTP + OAuth for Microsoft or Google
229
+ - Microsoft Graph delegated Mail.Send
230
+ - SES (Amazon Simple Email Service)
231
+ - Resend
232
+
233
+ ### Configuration
234
+
235
+ #### SMTP
236
+
237
+ Required environment variables:
238
+
239
+ - PDT_SMTP_HOST
240
+ - PDT_SMTP_USER
241
+
242
+ When using password auth:
243
+
244
+ - PDT_SMTP_PASSWORD
245
+
246
+ When using OAuth for Google or Microsoft:
247
+
248
+ - PDT_SMTP_OAUTH_CLIENT_ID
249
+
250
+ Optional:
251
+
252
+ - PDT_SMTP_AUTH = oauth2
253
+ - PDT_SMTP_PORT: Port 587 is the default and uses STARTTLS; port 465 uses implicit TLS.
254
+
255
+ Google also requires:
256
+
257
+ - PDT_SMTP_OAUTH_CLIENT_SECRET
258
+
259
+ Microsoft optionally accepts:
260
+
261
+ - PDT_SMTP_OAUTH_TENANT_ID: Defaults to `common`.
262
+
263
+ PDT infers OAuth when the host is `smtp.gmail.com`, `smtp.office365.com`, or `smtp-mail.outlook.com` and no password is set. If a provider revokes authorization, run or deploy the app from a terminal again.
264
+
265
+ ##### Google OAuth
266
+
267
+ Create a Desktop app OAuth client in the [Google Auth Platform](https://console.cloud.google.com/auth/clients). Set its client ID and client secret in the variables above. The OAuth consent screen must include `https://mail.google.com/` because Google requires that scope for SMTP OAuth.
268
+
269
+ ##### Microsoft OAuth
270
+
271
+ Create an app registration in the [Microsoft Entra admin center](https://entra.microsoft.com/). Add a Mobile and desktop application platform with `http://localhost` as a redirect URI. Add the delegated Office 365 Exchange Online permission `https://outlook.office.com/SMTP.Send` and allow public client flows.
272
+
273
+ Microsoft 365 must also have Authenticated SMTP enabled for the sending mailbox. Use `PDT_SMTP_OAUTH_TENANT_ID` for a tenant-specific registration, or leave it empty to use `common`.
274
+
275
+ #### Microsoft Graph
276
+
277
+ Required environment variables:
278
+
279
+ - PDT_GRAPH_MAIL_USER
280
+ - PDT_GRAPH_MAIL_CLIENT_ID
281
+
282
+ Optional:
283
+
284
+ - PDT_GRAPH_MAIL_TENANT_ID: Defaults to `common`.
285
+
286
+ PDT sends as `PDT_GRAPH_MAIL_USER`, so `email_from` in config.yml must match it. Graph sends the HTML part alone; the other transports send both parts. If a provider revokes authorization, run or deploy the app from a terminal again.
287
+
288
+ Create an app registration in the [Microsoft Entra admin center](https://entra.microsoft.com/). Add a Mobile and desktop application platform with `http://localhost` as a redirect URI. Add the Microsoft Graph delegated permission `Mail.Send` and allow public client flows. Do not add a client secret.
289
+
290
+ #### SES
291
+
292
+ Required environment variables:
293
+
294
+ - PDT_SES_REGION
295
+
296
+ Optional:
297
+
298
+ - PDT_SES_ACCESS_KEY_ID
299
+ - PDT_SES_SECRET_ACCESS_KEY
300
+
301
+ Leave the two SES key variables empty to use the standard AWS credential chain (for example, `AWS_PROFILE`, environment credentials, or a workload role).
302
+
303
+ #### Resend
304
+
305
+ Required environment variables:
306
+
307
+ - PDT_RESEND_API_KEY
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "pdt-cli"
3
+ version = "0.1.0"
4
+ description = "Validate, run, and deploy small scheduled jobs to AWS, Azure, Google Cloud, or Windows."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ dependencies = [
9
+ "pyyaml",
10
+ "python-dotenv",
11
+ "msal",
12
+ "google-auth",
13
+ "google-auth-oauthlib",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ apps = [
18
+ "cryptography",
19
+ "pyjwt",
20
+ "boto3",
21
+ "google-cloud-secret-manager",
22
+ ]
23
+
24
+ [project.scripts]
25
+ pdt = "pdt.cli:main"
26
+
27
+ [build-system]
28
+ requires = ["hatchling"]
29
+ build-backend = "hatchling.build"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/pdt"]
33
+
34
+ [tool.hatch.build.targets.sdist]
35
+ include = ["src/pdt", "README.md", "tests"]
36
+
37
+ [dependency-groups]
38
+ dev = ["pytest"]
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("pdt-cli")
5
+ except PackageNotFoundError:
6
+ __version__ = "0.0.0.dev0"