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.
- pdt_cli-0.1.0/.gitignore +23 -0
- pdt_cli-0.1.0/PKG-INFO +325 -0
- pdt_cli-0.1.0/README.md +307 -0
- pdt_cli-0.1.0/pyproject.toml +41 -0
- pdt_cli-0.1.0/src/pdt/__init__.py +6 -0
- pdt_cli-0.1.0/src/pdt/cli.py +177 -0
- pdt_cli-0.1.0/src/pdt/config.py +394 -0
- pdt_cli-0.1.0/src/pdt/deploy.py +106 -0
- pdt_cli-0.1.0/src/pdt/deploy_aws.py +613 -0
- pdt_cli-0.1.0/src/pdt/deploy_aws_fargate.py +449 -0
- pdt_cli-0.1.0/src/pdt/deploy_aws_lambda.py +359 -0
- pdt_cli-0.1.0/src/pdt/deploy_azure.py +531 -0
- pdt_cli-0.1.0/src/pdt/deploy_azure_container_apps.py +328 -0
- pdt_cli-0.1.0/src/pdt/deploy_azure_functions.py +307 -0
- pdt_cli-0.1.0/src/pdt/deploy_common.py +94 -0
- pdt_cli-0.1.0/src/pdt/deploy_google_cloud.py +756 -0
- pdt_cli-0.1.0/src/pdt/deploy_windows.py +376 -0
- pdt_cli-0.1.0/src/pdt/examples/hello-world/config.yml +20 -0
- pdt_cli-0.1.0/src/pdt/examples/hello-world/run.py +41 -0
- pdt_cli-0.1.0/src/pdt/examples/impossible-travel-report/config.yml +48 -0
- pdt_cli-0.1.0/src/pdt/examples/impossible-travel-report/env.template +57 -0
- pdt_cli-0.1.0/src/pdt/examples/impossible-travel-report/run.py +232 -0
- pdt_cli-0.1.0/src/pdt/examples/monday-orphaned-account-report/config.yml +47 -0
- pdt_cli-0.1.0/src/pdt/examples/monday-orphaned-account-report/env.template +57 -0
- pdt_cli-0.1.0/src/pdt/examples/monday-orphaned-account-report/run.py +333 -0
- pdt_cli-0.1.0/src/pdt/gcloud_sdk.py +170 -0
- pdt_cli-0.1.0/src/pdt/scaffold.py +278 -0
- pdt_cli-0.1.0/src/pdt/utils/__init__.py +0 -0
- pdt_cli-0.1.0/src/pdt/utils/email_auth.py +406 -0
- pdt_cli-0.1.0/src/pdt/utils/entra.py +114 -0
- pdt_cli-0.1.0/src/pdt/utils/log.py +34 -0
- pdt_cli-0.1.0/src/pdt/utils/send_email.py +467 -0
- pdt_cli-0.1.0/src/pdt/utils/send_email_graph.py +266 -0
- pdt_cli-0.1.0/src/pdt/utils/web.py +28 -0
- pdt_cli-0.1.0/tests/conftest.py +18 -0
- pdt_cli-0.1.0/tests/test_aws_account.py +38 -0
- pdt_cli-0.1.0/tests/test_config_merge.py +75 -0
- pdt_cli-0.1.0/tests/test_init_questions.py +58 -0
- pdt_cli-0.1.0/tests/test_project_discovery.py +52 -0
- pdt_cli-0.1.0/tests/test_save_platform_key.py +79 -0
- pdt_cli-0.1.0/tests/test_scaffold.py +124 -0
- pdt_cli-0.1.0/tests/test_schedule.py +45 -0
- pdt_cli-0.1.0/tests/test_starter_app.py +59 -0
pdt_cli-0.1.0/.gitignore
ADDED
|
@@ -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
|
pdt_cli-0.1.0/README.md
ADDED
|
@@ -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"]
|