cfn-typed 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.
- cfn_typed-0.1.0/.github/workflows/ci.yml +11 -0
- cfn_typed-0.1.0/.github/workflows/docs.yml +36 -0
- cfn_typed-0.1.0/.github/workflows/publish.yml +34 -0
- cfn_typed-0.1.0/.github/workflows/test.yml +36 -0
- cfn_typed-0.1.0/.gitignore +9 -0
- cfn_typed-0.1.0/LICENSE +21 -0
- cfn_typed-0.1.0/PKG-INFO +133 -0
- cfn_typed-0.1.0/README.md +109 -0
- cfn_typed-0.1.0/docs/gen_reference.py +100 -0
- cfn_typed-0.1.0/docs/generator.md +48 -0
- cfn_typed-0.1.0/docs/getting-started.md +98 -0
- cfn_typed-0.1.0/docs/index.md +54 -0
- cfn_typed-0.1.0/docs/patterns/complete-stack.md +39 -0
- cfn_typed-0.1.0/docs/patterns/composing.md +42 -0
- cfn_typed-0.1.0/docs/patterns/index.md +49 -0
- cfn_typed-0.1.0/docs/patterns/lambda-role.md +29 -0
- cfn_typed-0.1.0/docs/patterns/queue-with-dlq.md +39 -0
- cfn_typed-0.1.0/docs/patterns/sqs-trigger.md +24 -0
- cfn_typed-0.1.0/examples/minimal.py +28 -0
- cfn_typed-0.1.0/examples/patterns.py +79 -0
- cfn_typed-0.1.0/examples/sqs_worker.py +49 -0
- cfn_typed-0.1.0/examples/types.txt +5 -0
- cfn_typed-0.1.0/pyproject.toml +56 -0
- cfn_typed-0.1.0/scripts/build-docs.sh +7 -0
- cfn_typed-0.1.0/src/cfn_typed/__init__.py +15 -0
- cfn_typed-0.1.0/src/cfn_typed/base.py +75 -0
- cfn_typed-0.1.0/src/cfn_typed/helpers.py +36 -0
- cfn_typed-0.1.0/src/cfn_typed/intrinsics.py +48 -0
- cfn_typed-0.1.0/src/cfn_typed/py.typed +0 -0
- cfn_typed-0.1.0/src/cfn_typed/render.py +73 -0
- cfn_typed-0.1.0/src/cfn_typed/typegen.py +314 -0
- cfn_typed-0.1.0/tests/conftest.py +12 -0
- cfn_typed-0.1.0/tests/test_gen.py +47 -0
- cfn_typed-0.1.0/tests/test_helpers.py +13 -0
- cfn_typed-0.1.0/tests/test_patterns.py +30 -0
- cfn_typed-0.1.0/tests/test_render.py +66 -0
- cfn_typed-0.1.0/tests/test_sqs_worker.py +41 -0
- cfn_typed-0.1.0/zensical.toml +83 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Documentation
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: pages
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
pages: write
|
|
16
|
+
id-token: write
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
deploy:
|
|
20
|
+
environment:
|
|
21
|
+
name: github-pages
|
|
22
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/configure-pages@v6
|
|
26
|
+
- uses: actions/checkout@v7
|
|
27
|
+
- uses: actions/setup-python@v6
|
|
28
|
+
with:
|
|
29
|
+
python-version: "3.14"
|
|
30
|
+
- run: pip install zensical .
|
|
31
|
+
- run: scripts/build-docs.sh # downloads the schemas, generates types and reference pages, builds the site
|
|
32
|
+
- uses: actions/upload-pages-artifact@v5
|
|
33
|
+
with:
|
|
34
|
+
path: site
|
|
35
|
+
- uses: actions/deploy-pages@v5
|
|
36
|
+
id: deployment
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
uses: ./.github/workflows/test.yml
|
|
10
|
+
|
|
11
|
+
publish:
|
|
12
|
+
needs: test
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
environment: pypi
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v7
|
|
19
|
+
|
|
20
|
+
- name: Set version from release tag
|
|
21
|
+
run: |
|
|
22
|
+
VERSION="${{ github.ref_name }}"
|
|
23
|
+
VERSION="${VERSION#v}"
|
|
24
|
+
sed -i "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
|
|
25
|
+
echo "Set version to $VERSION"
|
|
26
|
+
|
|
27
|
+
- name: Install uv
|
|
28
|
+
uses: astral-sh/setup-uv@v7
|
|
29
|
+
|
|
30
|
+
- name: Build package
|
|
31
|
+
run: uv build
|
|
32
|
+
|
|
33
|
+
- name: Publish to PyPI
|
|
34
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
inputs:
|
|
6
|
+
python-versions:
|
|
7
|
+
description: JSON array of Python versions to test
|
|
8
|
+
type: string
|
|
9
|
+
default: '["3.14"]'
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ${{ fromJson(inputs.python-versions) }}
|
|
17
|
+
env:
|
|
18
|
+
UV_PYTHON: ${{ matrix.python-version }}
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7
|
|
22
|
+
|
|
23
|
+
- name: Install uv
|
|
24
|
+
uses: astral-sh/setup-uv@v7
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: uv sync
|
|
28
|
+
|
|
29
|
+
- name: Generate the types the examples and tests use
|
|
30
|
+
run: uv run cfn-typegen --out examples/cfn_types $(grep -v '^#' examples/types.txt)
|
|
31
|
+
|
|
32
|
+
- name: Run type checks
|
|
33
|
+
run: uv run ty check
|
|
34
|
+
|
|
35
|
+
- name: Run tests
|
|
36
|
+
run: uv run pytest tests/ -v
|
cfn_typed-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ville Vainio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cfn_typed-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cfn-typed
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CloudFormation templates as typed Python dicts, with types generated from the registry schemas
|
|
5
|
+
Project-URL: Homepage, https://github.com/vivainio/cfn-typed
|
|
6
|
+
Project-URL: Documentation, https://vivainio.github.io/cfn-typed/
|
|
7
|
+
Project-URL: Repository, https://github.com/vivainio/cfn-typed
|
|
8
|
+
Project-URL: Issues, https://github.com/vivainio/cfn-typed/issues
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: aws,cloudformation,codegen,infrastructure-as-code,typeddict,typing
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.14
|
|
20
|
+
Requires-Dist: typing-extensions>=4.13
|
|
21
|
+
Provides-Extra: yaml
|
|
22
|
+
Requires-Dist: pyyaml>=6; extra == 'yaml'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# cfn-typed
|
|
26
|
+
|
|
27
|
+
CloudFormation templates as typed Python dicts. Resource factories and their
|
|
28
|
+
`TypedDict`s are **generated from the official CloudFormation registry
|
|
29
|
+
schemas** (1,873 resource types), so a type checker catches typos, wrong types
|
|
30
|
+
and missing required properties before CloudFormation does. Checked with
|
|
31
|
+
[`ty`](https://docs.astral.sh/ty/).
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import json
|
|
35
|
+
import cfn_typed as cfn
|
|
36
|
+
from cfn_types import sqs # generated, see below
|
|
37
|
+
|
|
38
|
+
template: cfn.Template = {
|
|
39
|
+
"Resources": {
|
|
40
|
+
"DLQ": sqs.queue(),
|
|
41
|
+
"Queue": sqs.queue(
|
|
42
|
+
VisibilityTimeout=180,
|
|
43
|
+
RedrivePolicy={"deadLetterTargetArn": cfn.get_att("DLQ", "Arn"), "maxReceiveCount": 3},
|
|
44
|
+
),
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
print(json.dumps(template, indent=2))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
error[unknown-argument]: Argument `MemorySise` does not match any known parameter of function `function`
|
|
52
|
+
error[invalid-argument-type]: Expected `Expr[int]`, found `Literal["30"]`
|
|
53
|
+
error[missing-argument]: No arguments provided for required parameters `Code`, `Role`
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Generating only what you use
|
|
57
|
+
|
|
58
|
+
The package ships no resource types. You generate the ones you need into your
|
|
59
|
+
own project, so the output stays small (four resources is about 44 KB):
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
cfn-typegen --out cfn_types AWS::SQS::Queue 'AWS::Lambda::*'
|
|
63
|
+
cfn-typegen --out cfn_types --template template.json # types a JSON template uses
|
|
64
|
+
cfn-typegen --out cfn_types --all # everything, ~19 MB
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Type names are case-insensitive globs and an unmatched name is an error. The
|
|
68
|
+
schema zip is downloaded once into `~/.cache/cfn-typegen/`; `--schemas` points at a local copy.
|
|
69
|
+
Rerunning replaces earlier generated files in the output directory.
|
|
70
|
+
|
|
71
|
+
## Layout of the generated modules
|
|
72
|
+
|
|
73
|
+
One module per service. For `AWS::Lambda::Function` there is
|
|
74
|
+
`lambda_.function(**props)` (the factory), `lambda_.Function` (the resource dict),
|
|
75
|
+
`lambda_.FunctionProps`, and one type per nested definition
|
|
76
|
+
(`lambda_.FunctionCode`, ...). Python keywords get a trailing `_`. Factories also
|
|
77
|
+
take `depends_on`, `deletion_policy`, `update_replace_policy`, `condition` and
|
|
78
|
+
`metadata` as snake_case keywords, so they never collide with a property name.
|
|
79
|
+
|
|
80
|
+
`cfn_typed` itself holds the runtime pieces the generated code imports:
|
|
81
|
+
`intrinsics` (`Ref`, `GetAtt`, ..., and `Expr[T]` = `T | Intrinsic`, which every
|
|
82
|
+
generated property uses), `base` (`Template`, `Tag`, `build`) and a few helpers.
|
|
83
|
+
`examples/` has a worked SQS + Lambda stack and `examples/cfn_types/` is its
|
|
84
|
+
generated output.
|
|
85
|
+
|
|
86
|
+
## JSON or YAML
|
|
87
|
+
|
|
88
|
+
`cfn.to_json(template)` and `cfn.to_yaml(template)` (YAML needs `cfn-typed[yaml]`) render
|
|
89
|
+
the same template. The YAML uses short-form intrinsics (`!Ref`, `!GetAtt A.Arn`, ...).
|
|
90
|
+
|
|
91
|
+
## Patterns
|
|
92
|
+
|
|
93
|
+
Helper functions that return a resource or a small fragment (a queue with its
|
|
94
|
+
dead-letter queue, a Lambda role, ...) are documented in the book, with working
|
|
95
|
+
code in `examples/patterns.py`. Build it locally with `uv run zensical serve`.
|
|
96
|
+
|
|
97
|
+
## Type checking notes
|
|
98
|
+
|
|
99
|
+
Read-only properties (`Arn`, `Id`, ...) are left out of the inputs. Shapes whose
|
|
100
|
+
schema says `additionalProperties: false` are emitted as PEP 728 `closed=True`
|
|
101
|
+
TypedDicts, which is what makes `ty` reject unknown names.
|
|
102
|
+
|
|
103
|
+
Known gaps: enums are plain `str` (no `Literal`, so newly added values never
|
|
104
|
+
break you); `oneOf` constraints between properties are not modelled; arrays are
|
|
105
|
+
`list`, which is invariant, so a variable typed `list[Tag]` needs
|
|
106
|
+
`list[Expr[Tag]]`.
|
|
107
|
+
|
|
108
|
+
## Development
|
|
109
|
+
|
|
110
|
+
The repo contains no generated types. The examples and tests need a few, which are
|
|
111
|
+
generated into `examples/cfn_types/` (gitignored) the first time pytest runs, or by hand:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
cfn-typegen --out examples/cfn_types AWS::SQS::Queue AWS::Lambda::Function \
|
|
115
|
+
AWS::Lambda::EventSourceMapping AWS::IAM::Role
|
|
116
|
+
uv run ty check
|
|
117
|
+
uv run pytest
|
|
118
|
+
scripts/build-docs.sh # generate types and reference pages, build the book (needs zensical)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Releasing
|
|
122
|
+
|
|
123
|
+
Releases are published to PyPI from GitHub with trusted publishing (no token). The
|
|
124
|
+
version in `pyproject.toml` stays `0.0.0.dev0`; the workflow sets it from the tag.
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
gh release create v0.1.0 --generate-notes
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
That runs the tests, builds with `uv build` and publishes. One-time setup on PyPI:
|
|
131
|
+
add a trusted publisher for the project `cfn-typed` (owner `vivainio`, repository
|
|
132
|
+
`cfn-typed`, workflow `publish.yml`, environment `pypi`), and create the `pypi`
|
|
133
|
+
environment under the repository's Settings, Environments.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# cfn-typed
|
|
2
|
+
|
|
3
|
+
CloudFormation templates as typed Python dicts. Resource factories and their
|
|
4
|
+
`TypedDict`s are **generated from the official CloudFormation registry
|
|
5
|
+
schemas** (1,873 resource types), so a type checker catches typos, wrong types
|
|
6
|
+
and missing required properties before CloudFormation does. Checked with
|
|
7
|
+
[`ty`](https://docs.astral.sh/ty/).
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import json
|
|
11
|
+
import cfn_typed as cfn
|
|
12
|
+
from cfn_types import sqs # generated, see below
|
|
13
|
+
|
|
14
|
+
template: cfn.Template = {
|
|
15
|
+
"Resources": {
|
|
16
|
+
"DLQ": sqs.queue(),
|
|
17
|
+
"Queue": sqs.queue(
|
|
18
|
+
VisibilityTimeout=180,
|
|
19
|
+
RedrivePolicy={"deadLetterTargetArn": cfn.get_att("DLQ", "Arn"), "maxReceiveCount": 3},
|
|
20
|
+
),
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
print(json.dumps(template, indent=2))
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
error[unknown-argument]: Argument `MemorySise` does not match any known parameter of function `function`
|
|
28
|
+
error[invalid-argument-type]: Expected `Expr[int]`, found `Literal["30"]`
|
|
29
|
+
error[missing-argument]: No arguments provided for required parameters `Code`, `Role`
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Generating only what you use
|
|
33
|
+
|
|
34
|
+
The package ships no resource types. You generate the ones you need into your
|
|
35
|
+
own project, so the output stays small (four resources is about 44 KB):
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
cfn-typegen --out cfn_types AWS::SQS::Queue 'AWS::Lambda::*'
|
|
39
|
+
cfn-typegen --out cfn_types --template template.json # types a JSON template uses
|
|
40
|
+
cfn-typegen --out cfn_types --all # everything, ~19 MB
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Type names are case-insensitive globs and an unmatched name is an error. The
|
|
44
|
+
schema zip is downloaded once into `~/.cache/cfn-typegen/`; `--schemas` points at a local copy.
|
|
45
|
+
Rerunning replaces earlier generated files in the output directory.
|
|
46
|
+
|
|
47
|
+
## Layout of the generated modules
|
|
48
|
+
|
|
49
|
+
One module per service. For `AWS::Lambda::Function` there is
|
|
50
|
+
`lambda_.function(**props)` (the factory), `lambda_.Function` (the resource dict),
|
|
51
|
+
`lambda_.FunctionProps`, and one type per nested definition
|
|
52
|
+
(`lambda_.FunctionCode`, ...). Python keywords get a trailing `_`. Factories also
|
|
53
|
+
take `depends_on`, `deletion_policy`, `update_replace_policy`, `condition` and
|
|
54
|
+
`metadata` as snake_case keywords, so they never collide with a property name.
|
|
55
|
+
|
|
56
|
+
`cfn_typed` itself holds the runtime pieces the generated code imports:
|
|
57
|
+
`intrinsics` (`Ref`, `GetAtt`, ..., and `Expr[T]` = `T | Intrinsic`, which every
|
|
58
|
+
generated property uses), `base` (`Template`, `Tag`, `build`) and a few helpers.
|
|
59
|
+
`examples/` has a worked SQS + Lambda stack and `examples/cfn_types/` is its
|
|
60
|
+
generated output.
|
|
61
|
+
|
|
62
|
+
## JSON or YAML
|
|
63
|
+
|
|
64
|
+
`cfn.to_json(template)` and `cfn.to_yaml(template)` (YAML needs `cfn-typed[yaml]`) render
|
|
65
|
+
the same template. The YAML uses short-form intrinsics (`!Ref`, `!GetAtt A.Arn`, ...).
|
|
66
|
+
|
|
67
|
+
## Patterns
|
|
68
|
+
|
|
69
|
+
Helper functions that return a resource or a small fragment (a queue with its
|
|
70
|
+
dead-letter queue, a Lambda role, ...) are documented in the book, with working
|
|
71
|
+
code in `examples/patterns.py`. Build it locally with `uv run zensical serve`.
|
|
72
|
+
|
|
73
|
+
## Type checking notes
|
|
74
|
+
|
|
75
|
+
Read-only properties (`Arn`, `Id`, ...) are left out of the inputs. Shapes whose
|
|
76
|
+
schema says `additionalProperties: false` are emitted as PEP 728 `closed=True`
|
|
77
|
+
TypedDicts, which is what makes `ty` reject unknown names.
|
|
78
|
+
|
|
79
|
+
Known gaps: enums are plain `str` (no `Literal`, so newly added values never
|
|
80
|
+
break you); `oneOf` constraints between properties are not modelled; arrays are
|
|
81
|
+
`list`, which is invariant, so a variable typed `list[Tag]` needs
|
|
82
|
+
`list[Expr[Tag]]`.
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
The repo contains no generated types. The examples and tests need a few, which are
|
|
87
|
+
generated into `examples/cfn_types/` (gitignored) the first time pytest runs, or by hand:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
cfn-typegen --out examples/cfn_types AWS::SQS::Queue AWS::Lambda::Function \
|
|
91
|
+
AWS::Lambda::EventSourceMapping AWS::IAM::Role
|
|
92
|
+
uv run ty check
|
|
93
|
+
uv run pytest
|
|
94
|
+
scripts/build-docs.sh # generate types and reference pages, build the book (needs zensical)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Releasing
|
|
98
|
+
|
|
99
|
+
Releases are published to PyPI from GitHub with trusted publishing (no token). The
|
|
100
|
+
version in `pyproject.toml` stays `0.0.0.dev0`; the workflow sets it from the tag.
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
gh release create v0.1.0 --generate-notes
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
That runs the tests, builds with `uv build` and publishes. One-time setup on PyPI:
|
|
107
|
+
add a trusted publisher for the project `cfn-typed` (owner `vivainio`, repository
|
|
108
|
+
`cfn-typed`, workflow `publish.yml`, environment `pypi`), and create the `pypi`
|
|
109
|
+
environment under the repository's Settings, Environments.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Render a reference page per resource type from the CloudFormation schemas.
|
|
2
|
+
|
|
3
|
+
Run by scripts/build-docs.sh (and CI) before `zensical build`; the output in
|
|
4
|
+
docs/reference/ is gitignored, so the pages always match the schemas just downloaded.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from cfn_typed import typegen
|
|
12
|
+
|
|
13
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
14
|
+
OUT = ROOT / "docs" / "reference"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cell(text: str | None) -> str:
|
|
18
|
+
return (text or "").replace("|", "\\|").replace("\n", " ").strip()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def describe(node: dict[str, Any], schema: dict[str, Any], depth: int = 0) -> str:
|
|
22
|
+
if "$ref" in node and depth < 5:
|
|
23
|
+
target: Any = schema
|
|
24
|
+
for part in node["$ref"].removeprefix("#/").split("/"):
|
|
25
|
+
target = target[part]
|
|
26
|
+
name = node["$ref"].rsplit("/", 1)[-1]
|
|
27
|
+
return f"{name} (object)" if target.get("properties") else describe(target, schema, depth + 1)
|
|
28
|
+
t = node.get("type")
|
|
29
|
+
if isinstance(t, list):
|
|
30
|
+
return " or ".join(describe({**node, "type": x}, schema, depth) for x in t)
|
|
31
|
+
if t == "array":
|
|
32
|
+
item = node.get("items")
|
|
33
|
+
return f"list of {describe(item, schema, depth + 1)}" if isinstance(item, dict) else "list"
|
|
34
|
+
if t == "object":
|
|
35
|
+
return "object" if node.get("properties") else "map"
|
|
36
|
+
if t == "string" and node.get("enum"):
|
|
37
|
+
return "string (one of a fixed set)"
|
|
38
|
+
return t or "any"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def render(schema: dict[str, Any]) -> str:
|
|
42
|
+
type_name = schema["typeName"]
|
|
43
|
+
parts = type_name.split("::")
|
|
44
|
+
res = typegen.pascal("".join(parts[2:]))
|
|
45
|
+
svc = parts[1].lower() + ("_" if parts[1].lower() in typegen.keyword.kwlist else "")
|
|
46
|
+
factory = typegen.snake(res) + ("_" if typegen.reserved(typegen.snake(res)) else "")
|
|
47
|
+
hidden = typegen.readonly_props(schema)
|
|
48
|
+
required = set(schema.get("required", ())) - hidden
|
|
49
|
+
rows = [
|
|
50
|
+
f"| `{k}` | {cell(describe(v, schema))} | {'yes' if k in required else ''} | {cell(typegen.short(v.get('description')))} |"
|
|
51
|
+
for k, v in schema.get("properties", {}).items() if k not in hidden
|
|
52
|
+
]
|
|
53
|
+
attrs = sorted(hidden)
|
|
54
|
+
lines = [
|
|
55
|
+
f"# {type_name}",
|
|
56
|
+
"",
|
|
57
|
+
cell(typegen.short(schema.get("description"))),
|
|
58
|
+
"",
|
|
59
|
+
"```python",
|
|
60
|
+
f"from cfn_types import {svc}",
|
|
61
|
+
"",
|
|
62
|
+
f"{svc}.{factory}(**props) # returns {svc}.{res}",
|
|
63
|
+
"```",
|
|
64
|
+
"",
|
|
65
|
+
"## Properties",
|
|
66
|
+
"",
|
|
67
|
+
"| Property | Type | Required | Description |",
|
|
68
|
+
"|---|---|---|---|",
|
|
69
|
+
*rows,
|
|
70
|
+
"",
|
|
71
|
+
]
|
|
72
|
+
if attrs:
|
|
73
|
+
lines += [
|
|
74
|
+
"## Attributes",
|
|
75
|
+
"",
|
|
76
|
+
"Read-only values you can read with `get_att(logical_id, attribute)`. They are not accepted as inputs.",
|
|
77
|
+
"",
|
|
78
|
+
*[f"- `{a}`" for a in attrs],
|
|
79
|
+
"",
|
|
80
|
+
]
|
|
81
|
+
if url := schema.get("documentationUrl"):
|
|
82
|
+
lines += [f"[AWS documentation]({url})", ""]
|
|
83
|
+
return "\n".join(lines)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main() -> None:
|
|
87
|
+
wanted = [t for t in (ROOT / "examples" / "types.txt").read_text().split("\n") if t and not t.startswith("#")]
|
|
88
|
+
schemas = typegen.select(typegen.load_schemas(typegen.ensure_schemas(typegen.CACHE)), wanted)
|
|
89
|
+
OUT.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
index = ["# Reference", "", "Generated from the CloudFormation registry schemas when the book is built.", ""]
|
|
91
|
+
for schema in schemas:
|
|
92
|
+
slug = re.sub(r"\W+", "-", schema["typeName"].replace("AWS::", "")).lower()
|
|
93
|
+
(OUT / f"{slug}.md").write_text(render(schema))
|
|
94
|
+
index.append(f"- [{schema['typeName']}]({slug}.md)")
|
|
95
|
+
(OUT / "index.md").write_text("\n".join(index) + "\n")
|
|
96
|
+
print(f"wrote {len(schemas)} reference pages to {OUT}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# The generator
|
|
2
|
+
|
|
3
|
+
`cfn-typegen` turns AWS's CloudFormation registry schemas (about 1,900 resource
|
|
4
|
+
types) into Python modules, for **only the types you select**.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
cfn-typegen --out cfn_types AWS::SQS::Queue 'AWS::Lambda::*'
|
|
8
|
+
cfn-typegen --out cfn_types --template template.json
|
|
9
|
+
cfn-typegen --out cfn_types --all # everything, ~19 MB
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Type names are case-insensitive globs and an unmatched name is an error. The
|
|
13
|
+
schema zip is downloaded once into `~/.cache/cfn-typegen/`; `--schemas` points at
|
|
14
|
+
a local copy. A rerun replaces only files it generated earlier.
|
|
15
|
+
|
|
16
|
+
## What one resource looks like
|
|
17
|
+
|
|
18
|
+
For `AWS::SQS::Queue`, one module `sqs.py` holds:
|
|
19
|
+
|
|
20
|
+
| Name | Purpose |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `QueueProps` | the properties, as a `TypedDict` |
|
|
23
|
+
| `Queue` | the resource dict, with `Type: Literal["AWS::SQS::Queue"]` |
|
|
24
|
+
| `queue(**props)` | a factory; keyword arguments are checked against `QueueProps` |
|
|
25
|
+
| one class per nested definition | named `{Resource}{Definition}`, for example `FunctionCode` |
|
|
26
|
+
|
|
27
|
+
Factories also take `depends_on`, `deletion_policy`, `update_replace_policy`,
|
|
28
|
+
`condition` and `metadata` as snake_case keywords, so they never collide with a
|
|
29
|
+
property name. Python keywords and builtins get a trailing underscore
|
|
30
|
+
(`lambda_`).
|
|
31
|
+
|
|
32
|
+
## Why the checker is strict
|
|
33
|
+
|
|
34
|
+
- **Closed TypedDicts.** Shapes the schema marks `additionalProperties: false` are
|
|
35
|
+
emitted with `closed=True` (PEP 728). That is what turns `MemorySise` into an
|
|
36
|
+
error; `ty` ignores unknown keywords on ordinary TypedDicts.
|
|
37
|
+
- **`Expr[T]` everywhere.** A property is `T` or an intrinsic, because
|
|
38
|
+
CloudFormation accepts `Ref`, `Fn::GetAtt` and friends wherever a value goes.
|
|
39
|
+
- **Read-only properties are left out.** You cannot set `Arn` or `QueueUrl`, so the
|
|
40
|
+
inputs do not offer them.
|
|
41
|
+
|
|
42
|
+
## Known gaps
|
|
43
|
+
|
|
44
|
+
- Enums are plain `str`, not `Literal`, so values AWS adds later never break you.
|
|
45
|
+
- `oneOf` constraints between properties are not modelled.
|
|
46
|
+
- Arrays are `list`, which is invariant: a variable typed `list[Tag]` must be
|
|
47
|
+
`list[Expr[Tag]]` to be passed in. `cfn.tags()` returns the right type.
|
|
48
|
+
- Types do not replace `cfn-lint`: they check shape, not values.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
uv add cfn-typed # the runtime pieces and the cfn-typegen command
|
|
7
|
+
uv add --dev ty # the type checker this project is tested with
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Until it is on PyPI, install from GitHub:
|
|
11
|
+
`uv add git+https://github.com/vivainio/cfn-typed`. Python 3.14 or later is required.
|
|
12
|
+
|
|
13
|
+
## Generate the types you use
|
|
14
|
+
|
|
15
|
+
The package ships no resource types. You generate the ones your templates need
|
|
16
|
+
into your own project:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
cfn-typegen --out cfn_types AWS::SQS::Queue 'AWS::Lambda::*' AWS::IAM::Role
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or point it at a template that already exists and it emits exactly the types that
|
|
23
|
+
template uses:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
cfn-typegen --out cfn_types --template template.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Each service becomes one module (`cfn_types/sqs.py`, `cfn_types/lambda_.py`, ...).
|
|
30
|
+
Each run writes a clean set of modules and replaces only files it generated earlier.
|
|
31
|
+
Whether to commit them or generate them in CI is up to you.
|
|
32
|
+
|
|
33
|
+
## A minimal program
|
|
34
|
+
|
|
35
|
+
The whole program is one file: build the template as data, print it as JSON.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
--8<-- "examples/minimal.py:main"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`build` is a pure function of its arguments, so tests can call it directly. `main`
|
|
42
|
+
only reads the stage from the command line and prints. Render and deploy:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
python minimal.py prod > template.json
|
|
46
|
+
aws cloudformation deploy --template-file template.json --stack-name jobs-prod
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## JSON or YAML
|
|
50
|
+
|
|
51
|
+
`cfn.to_json(template)` is `json.dumps` with a default indent. `cfn.to_yaml(template)`
|
|
52
|
+
writes the same template as readable CloudFormation YAML:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
print(cfn.to_yaml(build("prod")))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
AWSTemplateFormatVersion: '2010-09-09'
|
|
60
|
+
Resources:
|
|
61
|
+
Jobs:
|
|
62
|
+
Type: AWS::SQS::Queue
|
|
63
|
+
Properties:
|
|
64
|
+
VisibilityTimeout: 60
|
|
65
|
+
Tags:
|
|
66
|
+
- Key: Stage
|
|
67
|
+
Value: prod
|
|
68
|
+
Outputs:
|
|
69
|
+
JobsUrl:
|
|
70
|
+
Value: !Ref Jobs
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Intrinsics use CloudFormation's short forms (`!Ref`, `!GetAtt Jobs.Arn`, `!Sub`, `!Join`,
|
|
74
|
+
...), keys keep the order you wrote them, lists are indented under their key, and
|
|
75
|
+
multi-line strings such as `UserData` become `|` blocks. A list that is shared between
|
|
76
|
+
resources (the tags in the examples) is repeated, never turned into a YAML anchor.
|
|
77
|
+
|
|
78
|
+
!!! note "YAML needs PyYAML"
|
|
79
|
+
JSON has no dependencies. For YAML install the extra: `uv add 'cfn-typed[yaml]'`.
|
|
80
|
+
|
|
81
|
+
!!! note "Nested short forms"
|
|
82
|
+
CloudFormation does not allow a short form directly inside another one
|
|
83
|
+
(`!Base64 !Sub ...`). When that happens `to_yaml` keeps the outer function in long
|
|
84
|
+
form (`Fn::Base64: !Sub ...`). Functions with no short form, such as `Fn::Length`,
|
|
85
|
+
also stay long. Parsing the YAML back gives exactly the template you started with;
|
|
86
|
+
the test suite checks that round trip.
|
|
87
|
+
|
|
88
|
+
## Let the checker help
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
ty check
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Anywhere a property takes a value you can pass either a literal or an intrinsic
|
|
95
|
+
such as `cfn.ref(...)` or `cfn.get_att(...)`: every generated property is
|
|
96
|
+
`Expr[T]`, meaning `T` or an intrinsic. Unknown names, wrong types and missing
|
|
97
|
+
required properties are reported before any template exists. Keep `cfn-lint` in CI
|
|
98
|
+
as well: types do not check values, such as whether a Lambda runtime exists.
|