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.
Files changed (38) hide show
  1. cfn_typed-0.1.0/.github/workflows/ci.yml +11 -0
  2. cfn_typed-0.1.0/.github/workflows/docs.yml +36 -0
  3. cfn_typed-0.1.0/.github/workflows/publish.yml +34 -0
  4. cfn_typed-0.1.0/.github/workflows/test.yml +36 -0
  5. cfn_typed-0.1.0/.gitignore +9 -0
  6. cfn_typed-0.1.0/LICENSE +21 -0
  7. cfn_typed-0.1.0/PKG-INFO +133 -0
  8. cfn_typed-0.1.0/README.md +109 -0
  9. cfn_typed-0.1.0/docs/gen_reference.py +100 -0
  10. cfn_typed-0.1.0/docs/generator.md +48 -0
  11. cfn_typed-0.1.0/docs/getting-started.md +98 -0
  12. cfn_typed-0.1.0/docs/index.md +54 -0
  13. cfn_typed-0.1.0/docs/patterns/complete-stack.md +39 -0
  14. cfn_typed-0.1.0/docs/patterns/composing.md +42 -0
  15. cfn_typed-0.1.0/docs/patterns/index.md +49 -0
  16. cfn_typed-0.1.0/docs/patterns/lambda-role.md +29 -0
  17. cfn_typed-0.1.0/docs/patterns/queue-with-dlq.md +39 -0
  18. cfn_typed-0.1.0/docs/patterns/sqs-trigger.md +24 -0
  19. cfn_typed-0.1.0/examples/minimal.py +28 -0
  20. cfn_typed-0.1.0/examples/patterns.py +79 -0
  21. cfn_typed-0.1.0/examples/sqs_worker.py +49 -0
  22. cfn_typed-0.1.0/examples/types.txt +5 -0
  23. cfn_typed-0.1.0/pyproject.toml +56 -0
  24. cfn_typed-0.1.0/scripts/build-docs.sh +7 -0
  25. cfn_typed-0.1.0/src/cfn_typed/__init__.py +15 -0
  26. cfn_typed-0.1.0/src/cfn_typed/base.py +75 -0
  27. cfn_typed-0.1.0/src/cfn_typed/helpers.py +36 -0
  28. cfn_typed-0.1.0/src/cfn_typed/intrinsics.py +48 -0
  29. cfn_typed-0.1.0/src/cfn_typed/py.typed +0 -0
  30. cfn_typed-0.1.0/src/cfn_typed/render.py +73 -0
  31. cfn_typed-0.1.0/src/cfn_typed/typegen.py +314 -0
  32. cfn_typed-0.1.0/tests/conftest.py +12 -0
  33. cfn_typed-0.1.0/tests/test_gen.py +47 -0
  34. cfn_typed-0.1.0/tests/test_helpers.py +13 -0
  35. cfn_typed-0.1.0/tests/test_patterns.py +30 -0
  36. cfn_typed-0.1.0/tests/test_render.py +66 -0
  37. cfn_typed-0.1.0/tests/test_sqs_worker.py +41 -0
  38. cfn_typed-0.1.0/zensical.toml +83 -0
@@ -0,0 +1,11 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ uses: ./.github/workflows/test.yml
@@ -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
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ site/
5
+ .cache/
6
+ examples/cfn_types/
7
+ docs/reference/
8
+ uv.lock
9
+ dist/
@@ -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.
@@ -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.