├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .prettierignore ├── .vscode └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── docs_backup ├── blog │ ├── .gitkeep │ ├── 2020-01-16-hello-world.md │ └── 2020-03-31-introducing-ingraph.md ├── docs │ ├── .editorconfig │ └── ingraph │ │ ├── dot │ │ ├── cloudformation.dot │ │ ├── comparison.dot │ │ ├── deployment.dot │ │ ├── external_abstraction.dot │ │ ├── ingraph.dot │ │ ├── internal_abstraction.dot │ │ └── native_abstraction.dot │ │ ├── language.md │ │ ├── overview.md │ │ ├── start.md │ │ └── stdlib.md └── img │ ├── blog │ └── 2020-03-31-introducing-ingraph │ │ ├── assistance.png │ │ ├── autocompletion.png │ │ ├── cloudformation.png │ │ ├── comparison.png │ │ └── external_abstraction.png │ ├── favicon.png │ ├── home │ ├── undraw_blooming_jtv6.svg │ ├── undraw_game_world_0o6q.svg │ └── undraw_good_team_m7uu.svg │ ├── ingraph │ ├── autocomplete.png │ ├── cloudformation.png │ ├── comparison.png │ ├── deployment.png │ ├── example.png │ ├── external_abstraction.png │ ├── ingraph.png │ ├── internal_abstraction.png │ ├── native_abstraction.png │ ├── typing.png │ ├── undraw_code_typing_7jnv.svg │ ├── undraw_deliveries_131a.svg │ └── undraw_dev_focus_b9xo.svg │ ├── logo-256x256.png │ └── logo-32x32.png ├── poetry.lock ├── pyproject.toml ├── scaffold ├── .devcontainer │ ├── Dockerfile │ └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .prettierignore ├── .vscode │ └── settings.json ├── COPYING ├── Makefile ├── README.md ├── poetry.lock ├── pyproject.toml ├── setup.cfg └── src │ └── helloworld │ ├── __init__.py │ ├── example.py │ └── handler.js ├── setup.cfg ├── src └── ingraph │ ├── __main__.py │ ├── cli │ ├── __init__.py │ ├── cmd_cfn.py │ └── py.typed │ └── engine │ ├── Grammar │ ├── __init__.py │ ├── core.py │ ├── encoder.py │ ├── importer.py │ ├── parser.py │ └── py.typed └── tests ├── __init__.py ├── e2e ├── __init__.py ├── fixture │ ├── __init__.py │ ├── complete_file │ │ ├── handler.js │ │ ├── input.py │ │ └── output.yaml │ ├── complete_module │ │ ├── __init__.py │ │ ├── handler.js │ │ ├── input.py │ │ ├── output.yaml │ │ ├── sidecar │ │ │ ├── __init__.py │ │ │ ├── assets │ │ │ │ ├── __init__.py │ │ │ │ └── handler.js │ │ │ └── input.py │ │ └── submodule.py │ ├── intrinsic │ │ ├── __init__.py │ │ ├── input.py │ │ └── output.yaml │ ├── output │ │ ├── __init__.py │ │ ├── input.py │ │ └── output.yaml │ ├── param │ │ ├── __init__.py │ │ ├── input.py │ │ └── output.yaml │ ├── pseudo │ │ ├── __init__.py │ │ ├── input.py │ │ └── output.yaml │ ├── simple │ │ ├── __init__.py │ │ ├── input.py │ │ └── output.yaml │ └── tree │ │ ├── __init__.py │ │ ├── input.py │ │ └── output.yaml └── test_all.py └── unit ├── __init__.py └── engine ├── __init__.py ├── test_core.py ├── test_importer.py ├── test_packager.py └── test_parser.py /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-buster 2 | 3 | RUN set -ex; \ 4 | \ 5 | echo "deb https://deb.nodesource.com/node_13.x buster main" \ 6 | > /etc/apt/sources.list.d/node.list; \ 7 | wget -qO- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -; \ 8 | \ 9 | apt update; \ 10 | apt install --yes --no-install-recommends \ 11 | git make nodejs silversearcher-ag tree zip; \ 12 | \ 13 | pip install poetry; \ 14 | mkdir -p ~/.config/pypoetry; \ 15 | echo '[virtualenvs]\ncreate=false' > ~/.config/pypoetry/config.toml; \ 16 | \ 17 | echo 'export PS1="\w>>> "' > ~/.bashrc 18 | 19 | CMD ["/bin/bash"] 20 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "dockerFile": "Dockerfile", 3 | "appPort": [8763], 4 | "extensions": [ 5 | "bungcip.better-toml", 6 | "editorconfig.editorconfig", 7 | "esbenp.prettier-vscode", 8 | "ms-python.python", 9 | "ryanluker.vscode-coverage-gutters" 10 | ], 11 | "postCreateCommand": "make install" 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | charset=utf-8 5 | end_of_line=lf 6 | insert_final_newline=true 7 | indent_size=4 8 | indent_style=space 9 | trim_trailing_whitespace=true 10 | 11 | [*.{md,rst}] 12 | trim_trailing_whitespace=false 13 | 14 | [Makefile] 15 | indent_style=tab 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage* 2 | .mypy_cache 3 | .pytest_cache 4 | .vscode/* 5 | !.vscode/settings.json 6 | *.egg-info 7 | __pycache__ 8 | coverage.xml 9 | build 10 | dist 11 | tmp 12 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .coverage* 2 | .mypy_cache 3 | .pytest_cache 4 | .vscode/* 5 | !.vscode/settings.json 6 | *.egg-info 7 | __pycache__ 8 | coverage.xml 9 | build 10 | dist 11 | tests/e2e/fixture 12 | tmp 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // 3 | // Editor 4 | // 5 | 6 | "files.exclude": { 7 | "**/.git": true, 8 | "**/__pycache__": true, 9 | "**/*.egg-info": true, 10 | ".coverage": true, 11 | ".coverage.*": true, 12 | ".devcontainer": true, 13 | ".editorconfig": true, 14 | ".gitattributes": true, 15 | ".gitignore": true, 16 | ".mypy_cache": true, 17 | ".prettierignore": true, 18 | ".pytest_cache": true, 19 | ".vscode": true, 20 | "coverage.xml": true, 21 | "dist": true, 22 | "Makefile": true, 23 | "pyproject.toml": true, 24 | "poetry.lock": true, 25 | "setup.cfg": true 26 | }, 27 | 28 | // 29 | // Extensions 30 | // 31 | 32 | "python.formatting.provider": "black", 33 | "python.linting.enabled": true, 34 | "python.linting.mypyEnabled": true, 35 | "python.linting.pylintEnabled": false 36 | } 37 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell python -c 'import toml; print(toml.load("pyproject.toml")["tool"]["poetry"]["version"])') 2 | SRCDIRS=src tests 3 | 4 | test: 5 | pytest --exitfirst --forked --capture=no \ 6 | --cov --cov-branch --cov-report=term --cov-report=xml 7 | 8 | type: 9 | MYPYPATH=./src mypy -p ingraph 10 | 11 | format: 12 | black --include '\.pyi?$$' $(SRCDIRS) 13 | autoflake -ri $(SRCDIRS) 14 | isort -rc $(SRCDIRS) 15 | npx prettier --end-of-line lf --write '**/*.{css,html,js,json,md,yaml,yml}' 16 | sed -i "s/version-[0-9]\+.[0-9]\+.[0-9]\+/version-$(VERSION)/g" README.md 17 | sed -i "s/ingraph\/[0-9]\+.[0-9]\+.[0-9]\+/ingraph\/$(VERSION)/g" README.md 18 | 19 | clean: 20 | rm -rf .coverage* .mypy_cache .pytest_cache coverage.xml dist 21 | find . -name __pycache__ -o -name *.egg-info | xargs rm -rf 22 | 23 | install: 24 | poetry install 25 | 26 | dist: clean install format type test 27 | poetry build 28 | cd scaffold; make clean 29 | cp -r scaffold dist/helloworld 30 | cd dist; zip -r ingraph-helloworld.zip helloworld 31 | rm -rf dist/helloworld 32 | 33 | .PHONY: dist 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | InGraph 3 |

4 | 5 | # [InGraph][website] · [![Version][badge-version]][version] [![License][badge-license]][license] 6 | 7 | > InGraph ain't template generator :stuck_out_tongue_winking_eye: 8 | 9 | InGraph is a Declarative Infrastructure Graph DSL for AWS 10 | CloudFormation. 11 | 12 | - **Declarative**: Never again abstract away from CloudFormation. 13 | Simply declare your infrastructure components, without the hassle of 14 | YAML, and preserve the strict semantic of the AWS CloudFormation 15 | language. Veterans, build on top of your knowledge. Beginners, learn 16 | CloudFormation effectively. 17 | 18 | - **Composable**: Create encapsulated components with their assets and 19 | dependencies, then share or compose them to build more complex 20 | infrastructures. From simple nodes to your whole graph, everything 21 | is a deployable infrastructure unit. 22 | 23 | - **Integrated**: Leverage the evergrowing Python ecosystem. Benefit 24 | from static type checking, take advantage of autocompletion in your 25 | editor, or even consume open infrastructure components via the 26 | Python Package Index, among others. 27 | 28 | [Learn how to use InGraph in your own project][overview]. 29 | 30 | ## Installation 31 | 32 | InGraph requires [Python 3.8][python] or newer. Feel free to use your 33 | favorite tool or [`pip`][pip] to install the 34 | [`ingraph` package][version]. 35 | 36 | ``` 37 | python3.8 -m pip install --upgrade --user ingraph 38 | ``` 39 | 40 | Verify your installation by invoking the `ig` command. You should see 41 | a welcome screen. 42 | 43 | Note that this project relies on [InGraph AWS][ingraph-aws] to provide 44 | access to the latest native AWS CloudFormation resources, automatically. 45 | 46 | ## Example 47 | 48 | We have several examples on the [website][website]. Here is the first 49 | one to whet your appetite. 50 | 51 | ``` 52 | ig cfn -i example.py -r HelloWorld -o example.yaml 53 | ``` 54 | 55 | ![InGraph Example](https://lifa.dev/img/ingraph/example.png) 56 | 57 | This example creates a simple AWS Lambda function that returns a 58 | "Hello, World!" message. 59 | 60 | You'll notice that CloudFormation parameters, along with their types and 61 | default values are simple constructor parameters, or that CloudFormation 62 | outputs are class attributes, or even that CloudFormation resource names 63 | are derived from variable names. It's only a taste of 64 | [what is in store][overview] for you. 65 | 66 | ## Contributing 67 | 68 | The primary purpose of this project is to continue to evolve the core of 69 | InGraph. We are grateful to the community for any contribution. You may, 70 | for example, proof-read the documentation, submit bugs and fixes, 71 | provide improvements, discuss new axes of evolution, or spread the word 72 | around you. 73 | 74 | ## Thanks 75 | 76 | We want to thank and express our gratitude to [Ben Kehoe][ben]. Without 77 | his guidance and support, this project wouldn't have been possible. 78 | 79 | ## License 80 | 81 | Unless otherwise stated, the source code of the project is released 82 | under the [GNU Affero General Public License Version 3][agplv3]. Please 83 | note, however, that **all public interfaces** subject to be embedded 84 | within the source code of your infrastructure are part of the [InGraph 85 | AWS][ingraph-aws] project that is **released under the [Apache License 86 | Version 2][apachev2]**. 87 | 88 | [badge-version]: https://img.shields.io/badge/version-0.2.1-blue?style=flat-square 89 | [version]: https://pypi.org/project/ingraph/0.2.1/ 90 | [badge-license]: https://img.shields.io/badge/license-AGPL3-blue?style=flat-square 91 | [license]: https://github.com/lifadev/ingraph#license 92 | [website]: https://lifa.dev/ingraph 93 | [agplv3]: https://www.gnu.org/licenses/agpl-3.0.txt 94 | [apachev2]: http://www.apache.org/licenses/LICENSE-2.0.txt 95 | [overview]: https://lifa.dev/docs/ingraph/overview 96 | [example]: https://raw.githubusercontent.com/lifadev/ingraph/master/example.png 97 | [python]: https://www.python.org/downloads/ 98 | [pip]: https://pip.pypa.io/en/stable/ 99 | [ben]: https://twitter.com/ben11kehoe 100 | [ingraph-aws]: https://github.com/lifadev/ingraph-aws 101 | -------------------------------------------------------------------------------- /docs_backup/blog/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/blog/.gitkeep -------------------------------------------------------------------------------- /docs_backup/blog/2020-01-16-hello-world.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hello, World! 3 | tags: [just-chatting] 4 | author: Farzad Senart 5 | author_title: Co-Chief Frog Officer at lifa.dev 6 | author_url: https://twitter.com/fsenart 7 | author_image_url: https://graph.facebook.com/749433322/picture/?height=200&width=200 8 | --- 9 | 10 | Hi there, and welcome to this blog. 11 | 12 | 13 | 14 | [Lionel][twitter-lionel] and I ([Farzad][twitter-farzad]) are long-time 15 | cloud practitioners, and today we feel an urge to share our findings 16 | with the community. So, here we are. 17 | We will mostly write about Amazon Web Services, but not only. We will 18 | sometimes go deep in detail and sometimes only scratch the surface. We 19 | will try to publish regularly, but sometimes we just won't. 20 | We are French, so also expect some writings in our native language, Le 21 | Code, on [GitHub][github-lifadev]. 22 | 23 | Can't wait :3 24 | 25 | [twitter-lionel]: https://twitter.com/lion3ls 26 | [twitter-farzad]: https://twitter.com/fsenart 27 | [github-lifadev]: https://github.com/lifadev 28 | -------------------------------------------------------------------------------- /docs_backup/blog/2020-03-31-introducing-ingraph.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introducing InGraph 3 | tags: 4 | [ 5 | aws, 6 | cloudformation, 7 | domain-specific-language, 8 | infrastructure-as-code, 9 | infrastructure-graph, 10 | ] 11 | author: Farzad Senart 12 | author_title: Co-Chief Frog Officer at lifa.dev 13 | author_url: https://twitter.com/fsenart 14 | author_image_url: https://graph.facebook.com/749433322/picture/?height=200&width=200 15 | --- 16 | 17 | Lionel and I are pleased to announce the open-source release of our 18 | abstraction on top of AWS CloudFormation: InGraph! 19 | 20 | 21 | 22 | And before leaping to any conclusions, we solemnly promise it is not yet 23 | another template generator. Put simply, InGraph is: 24 | 25 | **CloudFormation like it is 2020. A familiar syntax, editor support, 26 | modularity and composability, and way more while rigorously following 27 | the semantic of CloudFormation.** 28 | 29 | If you’re a CloudFormation aficionado, you’ll feel immediately 30 | comfortable and sense a free upgrade. At first sight, InGraph is an 31 | updated syntax that still tastes CloudFormation but also deeply 32 | integrates with the tooling you’re used to (e.g., auto-completion, 33 | typing, packaging, etc.). 34 | 35 | If you’re new to the world of Infrastructure as Code on AWS, you’ll find 36 | a companion who will teach you the ins and outs of CloudFormation while 37 | assisting you in your journey. InGraph is CloudFormation stripped of its 38 | quirks and dirtiness. It is similar in all aspects to modern programming 39 | languages but also makes a point of not disconnecting from 40 | CloudFormation. 41 | 42 | Sounds like a fit? Continue reading! 43 | 44 | # The Challenge of Infrastructure Management 45 | 46 | If you haven’t yet watch [Ben Kehoe][twitter-ben]'s [talk][talk-ben] at 47 | Serverlessconf New York City ’19, stop here, do so and come back only 48 | after. It’s a foundational talk that makes a strong statement about what 49 | should be the right abstraction on top of AWS CloudFormation. By the 50 | way, it is also the backbone of InGraph. 51 | 52 | Using the AWS CloudFormation language is painful. To be convinced, we 53 | only need to look at the various attempts made both by AWS itself and 54 | the broader open-source community to overcome the difficulties and 55 | frustration. In fact, the pain is so deep that all the alternatives 56 | ended up, invariably, disconnecting from it or ignoring it altogether. 57 | We all do agree with the problem. However, let’s take a closer look at 58 | the path chosen toward the solution. 59 | 60 | There is, at least, a cognitive bias accentuated by the fact that, as 61 | software engineers, we have power. We can resolve problems with more and 62 | more elaborated concepts and tools. So why stick with something that 63 | annoys us? Let’s say that this question is too philosophical for this 64 | post. 65 | 66 | Moreover, and Ben’s talk brings this up, there is also a misconception, 67 | firmly anchored in the collective sub-consciousness, that CloudFormation 68 | is the assembly language of the cloud. And this very statement is enough 69 | to comfort us in need of high-level constructs that have nothing to do 70 | with the low-level CloudFormation language. After all, this is precisely 71 | what we’ve been used to do with the assembly languages of processors. 72 | 73 | > The AWS CloudFormation language is not the assembly language of the 74 | > cloud. 75 | 76 | To better understand these misunderstandings, let’s define what exactly 77 | is AWS CloudFormation. It is an infrastructure graph management service 78 | of a two-prong nature. On the one hand, it is a _language_ that allows, 79 | via the YAML syntax, to describe an infrastructure graph. On the other, 80 | it is a deployment engine that interprets the description, extracts the 81 | infrastructure graph, and then creates and executes a plan to 82 | materialize that graph. 83 | 84 |

85 | CloudFormation 87 |

88 | 89 | It turns out that the actual assembly language is not the description 90 | but the deployment plan. And we, as CloudFormation users, don’t have 91 | access to it. What does that mean is we cannot simply disconnect from or 92 | ignore the CloudFormation language. We have to play the game of the 93 | interpreter. Because, in case of any problems, our understanding of the 94 | CloudFormation language is our only way to salvation. 95 | 96 | > Learning the AWS CloudFormation language is a necessity. 97 | 98 | Finally, the desire to get rid of the CloudFormation language is also 99 | incentivized by the legitimate willingness to manipulate infrastructure 100 | graphs at a higher level (e.g., packaging up related resources, reusing 101 | patterns, etc.). And general-purpose programming languages and 102 | frameworks _inside_ them seem perfect candidates. However, they come at 103 | the expense of the comparability of the desired and the actual graph. 104 | 105 |

106 | Comparing Graphs 108 |

109 | 110 | Indeed, as these abstractions occur _outside_ the AWS CloudFormation 111 | language, they not only disconnect you from the input of the interpreter 112 | (i.e., the desired graph) but from its output as well (i.e., the actual 113 | graph of deployed resources on the cloud environment). This points out a 114 | fundamental problem as our capacity to effectively manage 115 | infrastructures is closely related to our ability to compare these two 116 | graphs meaningfully and understand how the same or different they are. 117 | 118 |

119 | External Abstraction 121 |

122 | 123 | # Toward Better Infrastructure Management 124 | 125 | As outlined earlier, infrastructure management on AWS is not a new 126 | challenge. Tools like AWS Serverless Application Model (SAM), AWS Cloud 127 | Development Kit (CDK), and AWS Serverless Application Repository (SAR), 128 | to name but a few, are already helping many organizations. When we 129 | undertook to create a new abstraction on top of AWS CloudFormation, we 130 | had two main goals: 131 | 132 | 1. Have a familiar language that scrupulously respects the semantic of 133 | AWS CloudFormation. 134 | 135 | 2. Have a high-level abstraction that unfolds from AWS CloudFormation 136 | low-level constructs (e.g., resources, stacks, parameters, outputs, 137 | etc.). 138 | 139 | Meet [InGraph][ingraph]! 140 | 141 | # InGraph 142 | 143 | InGraph is an [open-source][github] and declarative, infrastructure 144 | graph DSL for AWS CloudFormation. 145 | 146 | InGraph appears as a subset of Python and, therefore, seamlessly 147 | integrates with its existing ecosystem of tooling. For instance, you're 148 | given autocompletion, type checking, and live support from within your 149 | editor, out-of-the-box. You can also use the Python Package Index (PyPI) 150 | along with pip to share and consume available infrastructure patterns. 151 | 152 |

153 | Autocompletion 155 |

156 | 157 |

158 | Error Reporting 160 |

161 | 162 | Unlike other tools that run _inside_ general-purpose languages, InGraph 163 | operates _at_ the language level. It doesn't execute the code you write 164 | to produce a CloudFormation template. The syntax already encodes all the 165 | information. This also enables the ability to compare the InGraph 166 | description with its YAML counterpart. You're no more disconnected from 167 | the AWS CloudFormation language. You actually evolve in the same 168 | language but with an updated syntax. 169 | 170 |

171 | 172 | Example 174 | 175 |

176 | 177 | InGraph steps away from the YAML syntax to supersede its limitations but 178 | preserves the benefits of its declarative programming model. Also, under 179 | the hood, InGraph comes with a custom interpretation engine that 180 | rigorously enforces the semantic of the AWS CloudFormation language. 181 | 182 | > InGraph uses declarations to describe what is the infrastructure graph 183 | > instead of instructions to convey how to build it. 184 | 185 | Naturally, mutations have no place in a declarative world. So, while 186 | InGraph allows you to use variables, it also prevents you from mutating 187 | anything. Within InGraph, everything is immutable. 188 | 189 | InGraph highlights the typing system of AWS CloudFormation by providing 190 | five primary types: _booleans_, _numbers_, _strings_, _lists_, and 191 | _maps_. The engine carefully verifies that you don't overstep the frame 192 | imposed by the AWS CloudFormation language. For instance, you are not 193 | allowed to perform numeric operations or to access an item inside a list 194 | with an index that is not known statically. In contrast, where semantic 195 | correctness is not at stake, InGraph frees you from low-level details 196 | that parasitize your intent (e.g., `param.replace(":", "-")` is 197 | translated to `!Join [":", !Split ["-", !Ref param]]`). 198 | 199 | Last but not least, InGraph unifies the concepts of CloudFormation 200 | resources and CloudFormation stacks into a new domain-specific resource. 201 | This new kind of resource can carry parameters and outputs, and be used 202 | in place of or along with any other native CloudFormation resource. It 203 | is the very expression of a [multiscale graph][graph] in which nodes are 204 | resources or collection of resources. It also constitutes the primary 205 | unit of composability and share and offers an unprecedented way to 206 | capitalize on knowledge within companies and across the community. 207 | 208 | > InGraph unifies the resources and stacks of AWS CloudFormation into 209 | > nodes of a multiscale graph. 210 | 211 |

212 | InGraph 214 |

215 | 216 | # Getting Started 217 | 218 | InGraph is available now on [GitHub][github]. You can 219 | [try it out][start] using Visual Studio Code Remote Containers. 220 | Detailed information about the underlying reasoning, setup, and usage 221 | are also available in [our documentation][ingraph]. 222 | 223 | # Contributing 224 | 225 | InGraph is currently in [MVP][mvp] status. [Lionel][twitter-lionel] and 226 | [I][twitter-farzad] are proud of what we've built and excited to share 227 | it with the community. We hope you find it as useful as we do. InGraph 228 | only has a small portion of features we intend to build, and we are 229 | actively seeking feedback. If you have any ideas, suggestions, or 230 | issues, feel free to reach us on [Twitter][twitter-lifa] or 231 | [GitHub][github]. 232 | 233 | > **We can't finish this post without a big thanks to Ben Kehoe. His 234 | > theories served as the foundations of InGraph, his guidance and 235 | > support made it possible.** 236 | 237 | [twitter-ben]: https://twitter.com/ben11kehoe 238 | [talk-ben]: https://acloud.guru/series/serverlessconf-nyc-2019/view/yaml-better 239 | [ingraph]: /ingraph 240 | [github]: https://github.com/lifadev/ingraph 241 | [graph]: /docs/ingraph/overview#multiscale-graph 242 | [start]: /docs/ingraph/start#usage 243 | [mvp]: https://en.wikipedia.org/wiki/Minimum_viable_product 244 | [twitter-lionel]: https://twitter.com/lion3ls 245 | [twitter-farzad]: https://twitter.com/fsenart 246 | [twitter-lifa]: https://twitter.com/lifadev 247 | -------------------------------------------------------------------------------- /docs_backup/docs/.editorconfig: -------------------------------------------------------------------------------- 1 | root=false 2 | 3 | [*] 4 | indent_size=2 5 | -------------------------------------------------------------------------------- /docs_backup/docs/ingraph/dot/cloudformation.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | bgcolor=transparent 3 | edge [color="#546E7A"] 4 | fontcolor="#546E7A" 5 | fontname="JetBrains Mono bold" 6 | fontsize=12 7 | node [ 8 | color="#546E7A" 9 | fontcolor="#546E7A" 10 | fontname="JetBrains Mono bold" 11 | fontsize=12 12 | shape=rectangle 13 | ] 14 | rankdir=LR 15 | 16 | input [label="" shape=none] 17 | output [label="" shape=none] 18 | executor [shape=octagon] 19 | 20 | input -> plan 21 | subgraph cluster { 22 | color="#546E7A" 23 | label=interpreter 24 | style=dashed 25 | plan -> executor 26 | } 27 | executor -> output 28 | } 29 | -------------------------------------------------------------------------------- /docs_backup/docs/ingraph/dot/comparison.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | bgcolor=transparent 3 | concentrate=true 4 | edge [color="#546e7a"] 5 | fontcolor="#546e7a" 6 | fontname="JetBrains Mono bold" 7 | fontsize=12 8 | node [ 9 | color="#546e7a" 10 | fontcolor="#546e7a" 11 | fontname="JetBrains Mono bold" 12 | fontsize=12 13 | shape=rectangle 14 | ] 15 | rankdir=LR 16 | 17 | input [label="desired\ngraph"] 18 | interpreter [style=dashed] 19 | output [label="actual\ngraph"] 20 | 21 | input -> interpreter 22 | input:s -> output:s [ 23 | color="#546e7a" 24 | constraint=false 25 | dir=both 26 | style=dotted 27 | ] 28 | interpreter -> output 29 | } 30 | -------------------------------------------------------------------------------- /docs_backup/docs/ingraph/dot/deployment.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | bgcolor=transparent 3 | edge [color="#546E7A"] 4 | fontcolor="#546E7A" 5 | fontname="JetBrains Mono bold" 6 | fontsize=12 7 | node [ 8 | color="#546E7A" 9 | fontcolor="#546E7A" 10 | fontname="JetBrains Mono bold" 11 | fontsize=12 12 | shape=rectangle 13 | ] 14 | rankdir=LR 15 | 16 | input [label="desired\ngraph"] 17 | interpreter [style=dashed] 18 | output [label="actual\ngraph"] 19 | 20 | input -> interpreter 21 | interpreter -> output 22 | } 23 | -------------------------------------------------------------------------------- /docs_backup/docs/ingraph/dot/external_abstraction.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | bgcolor=transparent 3 | concentrate=true 4 | edge [color="#546e7a"] 5 | fontcolor="#546e7a" 6 | fontname="JetBrains Mono bold" 7 | fontsize=12 8 | node [ 9 | color="#546e7a" 10 | fontcolor="#546e7a" 11 | fontname="JetBrains Mono bold" 12 | fontsize=12 13 | shape=rectangle 14 | ] 15 | nodesep=0.5 16 | 17 | input_abstraction [label="external\nabstraction"] 18 | input [label="desired\ngraph"] 19 | interpreter [style=dashed] 20 | output [label="actual\ngraph"] 21 | 22 | input_abstraction -> input 23 | input_abstraction:e -> output:n [ 24 | color="#546e7a" 25 | constraint=false 26 | dir=both 27 | style=dotted 28 | ] 29 | subgraph cluster { 30 | color=invis 31 | rank=same 32 | 33 | { 34 | input -> interpreter 35 | interpreter -> output 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs_backup/docs/ingraph/dot/ingraph.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | bgcolor=transparent 3 | compound=true 4 | edge [color="#546E7A"] 5 | node [color="#546e7a" label="" shape=doublecircle style=filled] 6 | nodesep=0.5 7 | rankdir=LR 8 | 9 | a [fillcolor="#00c379"] 10 | b [fillcolor="#00c379"] 11 | c [fillcolor="#8e24aa"] 12 | d [fillcolor="#8e24aa"] 13 | e [fillcolor="#8e24aa"] 14 | 15 | a -> b 16 | a -> c [lhead=cluster] 17 | b -> c [constraint=false lhead=cluster] 18 | subgraph cluster { 19 | color="#00c379" 20 | style=rounded 21 | c -> d 22 | c -> e 23 | d -> e [constraint=false] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs_backup/docs/ingraph/dot/internal_abstraction.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | bgcolor=transparent 3 | concentrate=true 4 | edge [color="#546e7a"] 5 | fontcolor="#546e7a" 6 | fontname="JetBrains Mono bold" 7 | fontsize=12 8 | node [ 9 | color="#546e7a" 10 | fontcolor="#546e7a" 11 | fontname="JetBrains Mono bold" 12 | fontsize=12 13 | shape=rectangle 14 | ] 15 | nodesep=0.5 16 | rankdir=BT 17 | 18 | input_abstraction [label="internal\nabstraction"] 19 | input [label="desired\ngraph"] 20 | interpreter [style=dashed] 21 | output [label="actual\ngraph"] 22 | output_abstraction [label="persisted\nabstraction"] 23 | 24 | input:c -> input_abstraction [arrowtail=dot dir=back tailclip=false] 25 | output_abstraction -> input_abstraction [ 26 | color="#546e7a" 27 | constraint=false 28 | dir=both 29 | style=dotted 30 | ] 31 | subgraph cluster { 32 | color=invis 33 | rank=same 34 | 35 | { 36 | input -> interpreter 37 | interpreter -> output 38 | } 39 | } 40 | output:c -> output_abstraction [arrowtail=dot dir=back tailclip=false] 41 | } 42 | -------------------------------------------------------------------------------- /docs_backup/docs/ingraph/dot/native_abstraction.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | bgcolor=transparent 3 | concentrate=true 4 | edge [color="#546e7a"] 5 | fontcolor="#546e7a" 6 | fontname="JetBrains Mono bold" 7 | fontsize=12 8 | node [ 9 | color="#546e7a" 10 | fontcolor="#546e7a" 11 | fontname="JetBrains Mono bold" 12 | fontsize=12 13 | shape=rectangle 14 | ] 15 | nodesep=0.5 16 | 17 | input_abstraction [label="external\nabstraction"] 18 | input [label="desired\ngraph"] 19 | interpreter [style=dashed] 20 | output [label="actual\ngraph"] 21 | output_abstraction [label="native\nabstraction"] 22 | 23 | input_abstraction -> input 24 | input_abstraction -> output_abstraction [ 25 | color="#546e7a" 26 | constraint=false 27 | dir=both 28 | style=dotted 29 | ] 30 | subgraph cluster { 31 | color=invis 32 | rank=same 33 | 34 | { 35 | input -> interpreter 36 | interpreter -> output 37 | } 38 | } 39 | output_abstraction -> output [dir=back] 40 | } 41 | -------------------------------------------------------------------------------- /docs_backup/docs/ingraph/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: overview 3 | title: Overview 4 | --- 5 | 6 |

7 | InGraph 8 |

9 | 10 | InGraph is an [open-source][github] and declarative, infrastructure 11 | graph DSL for AWS CloudFormation. The key feature is the ability to 12 | create composable infrastructure components while preserving the 13 | rigorous semantic of the AWS CloudFormation language. 14 | 15 | ## Rationale 16 | 17 | During his [talk][talk] at Serverlessconf New York City '19, 18 | [Ben Kehoe][ben] laid the foundations of what should be the adequate 19 | abstraction on top of AWS CloudFormation. Hereafter is an interpretation 20 | of this talk that eventually resulted in the InGraph project. 21 | 22 | ### CloudFormation 23 | 24 | Before going any further, it is crucial to define what exactly is AWS 25 | CloudFormation. It is an [infrastructure graph](#infrastructure-graph) 26 | management service of a two-prong nature. On the one hand, it is a 27 | _language_ that allows, via the YAML _syntax_, to describe an 28 | infrastructure graph. On the other, it is a deployment engine that 29 | interprets the description, extracts the infrastructure graph, and then 30 | creates and executes a plan to materialize that graph. 31 | 32 |

33 | CloudFormation 34 |

35 | 36 | With the above clarification, a deployment becomes the act of passing 37 | some desired infrastructure graph to the interpreter, which in turn 38 | takes care of materializing the resources in the cloud environment. 39 | 40 |

41 | Deployment 42 |

43 | 44 | ### Problem 45 | 46 | It's all about infrastructure graphs. On one side, there is the graph 47 | one wants to see it exist, and on the other, the graph that actually 48 | exists. Effectively managing infrastructure can be summed up in the 49 | ability to compare these two graphs meaningfully and understand how the 50 | same or different they are. 51 | 52 |

53 | Comparing Graphs 55 |

56 | 57 | The fact of the matter is that the abstraction provided by 58 | CloudFormation only ever allows manipulating raw resources at the lowest 59 | level of the infrastructure graph. Moreover, it's not only tricky and 60 | frustrating to use the YAML syntax, but there is also no way to package 61 | up related resources, to reuse patterns, and in sum, to manipulate the 62 | graph at a higher level of abstraction. 63 | 64 | Hence, the problem is how to provide better syntax and high-level 65 | abstractions without jeopardizing the ability to compare the two graphs. 66 | 67 | ### Solutions 68 | 69 | #### External Abstraction 70 | 71 | The easiest way of reaching a higher level of abstraction is by doing so 72 | from the _outside_ of the graph definition language fed to the 73 | interpreter (i.e., the CloudFormation template in YAML). 74 | 75 |

76 | External Abstraction 78 |

79 | 80 | Usually, a library created _inside_ a general-purpose programming 81 | language can provide this kind of abstraction. One then has access to 82 | high-level concepts to develop the infrastructure graph, and the tooling 83 | takes care of compiling down into the graph definition language. 84 | 85 | However, the initial problem of being able to compare the desired 86 | infrastructure graph with the actual infrastructure graph now shifts to 87 | the more complex, if not impossible, issue of being able to compare the 88 | abstraction with the actual infrastructure graph. 89 | 90 | It's worth noting that a more subtle downside also appears. This kind of 91 | abstraction doesn't provide any learning path toward the graph 92 | definition language. And while this faster development experience may 93 | be appealing to mature CloudFormation practitioners, it also disconnects 94 | newcomers from CloudFormation with the consequence of being helpless in 95 | case a problem occurs outside the abstraction. 96 | 97 | #### Native Abstraction 98 | 99 | To transform external abstractions into a viable solution, one could 100 | combine them with a similar approach at the cloud environment side. 101 | 102 |

103 | Native Abstraction 105 |

106 | 107 | In theory, it would solve the initial problem by making possible the 108 | comparison of the two sides. However, in practice, it means that one 109 | needs to find a way, given an actual infrastructure graph, to create a 110 | high-level program that generates this graph while preserving syntactic 111 | and semantic correctness with the initial program that generated the 112 | desired infrastructure graph. It is equivalent to finding an automatic 113 | and reliable reverse-engineering process. 114 | 115 | #### Internal Abstraction 116 | 117 | The truth of the matter is that the comparison of the desired graph with 118 | the actual graph is only possible if these two graphs are expressible in 119 | the same language. Hence, one should not provide abstractions from the 120 | outside. Abstractions must happen within the graph definition language 121 | itself so that they can be passed into the interpreter and persisted in 122 | the actual infrastructure graph. 123 | 124 |

125 | Internal Abstraction 127 |

128 | 129 | Nevertheless, this doesn't imply that one needs to stick with the 130 | historical AWS CloudFormation YAML syntax. In effect, any language that 131 | is equivalent to the graph definition language, [up to][upto] 132 | [isomorphism][isomorphism], is acceptable. 133 | 134 | Recall the [above definition](#cloudformation) of AWS CloudFormation, 135 | whereby it's a service of a two-prong nature. The language apart, it is 136 | a deployment engine whose main job is to extract the infrastructure 137 | graph to create the deployment plan. This definition points out the fact 138 | that the assembly language of the cloud is not the YAML template but the 139 | deployment plan. As CloudFormation users have no access to the latter, 140 | it prevents one from simply ditching the historical language and limits 141 | alternatives to equivalent languages (i.e., compatible with the 142 | interpreter). 143 | 144 | In other words, one needs sort of a program that represents an 145 | infrastructure graph in such a way that if presented to the interpreter, 146 | hypothetically, this one could _parse_ the program, understand it and 147 | extract the infrastructure graph. 148 | 149 | ## Proposal 150 | 151 | For obvious reasons, InGraph chooses to engage in the internal 152 | abstractions' path. 153 | 154 | ### Domain-Specific Language 155 | 156 | The first part of the contract consists of reconsidering the AWS 157 | CloudFormation language from a syntax perspective while paying special 158 | attention to preserve the semantic. 159 | 160 | To this end, InGraph started by carefully reconsidering all the quirks 161 | and dirtiness of the original syntax and delineating the semantic of the 162 | CloudFormation language. Some notions are essential to the problem 163 | domain (e.g., the declarative nature of the syntax, the interactions 164 | between the built-in functions, the typing system governing the 165 | parameters, outputs, and resources, etc.). In contrast others are 166 | low-level details that obfuscate the initial intent (e.g., 167 | `!Join [":", !Split ["-", !Ref param]]` is really 168 | `param.replace(":", "-")`). 169 | 170 | That eventually led to a domain-specific language. Its syntax is similar 171 | in all aspects to that of modern programming languages. It comes with 172 | conciseness, expressivity, and tooling, out-of-the-box. And, as it 173 | operates _at_ the language level, it can strictly enforce the semantic. 174 | Moreover, it has enough metadata to tie the very lines of the new syntax 175 | to its counterpart translated into YAML, enabling rich syntactic 176 | introspection capabilities on top of the semantic correctness. 177 | 178 | ### Domain-Specific Resource 179 | 180 | The second part of the contract consists of providing high-level 181 | abstractions without jeopardizing the ability to compare the desired and 182 | the actual infrastructure graphs. It literally means that these 183 | abstractions must unfold from low-level constructs of AWS 184 | CloudFormation. 185 | 186 | To this end, InGraph started by analyzing the native units of 187 | composition used by CloudFormation. Resources are parametric and have a 188 | well-defined API. They are also opaque, and one doesn't need to know 189 | about their internals to use them effectively. For their part, stacks 190 | convey the ability to package related resources, to reuse patterns, and 191 | are the de facto unit of share. 192 | 193 | That eventually led to a domain-specific resource. It is similar in all 194 | aspects to both a CloudFormation stack and a CloudFormation resource. 195 | It can carry parameters and outputs. It can be used in place of or along 196 | with any other native CloudFormation resource. It is the very expression 197 | of a [multiscale graph](#multiscale-graph) in which nodes are resources 198 | or collection of resources (i.e., an infrastructure graph). Combined 199 | with modules and assets, it offers an unprecedented way to capitalize on 200 | knowledge within companies and across the community. 201 | 202 | ### Familiar and Effective 203 | 204 | With a high-level interface on top of AWS CloudFormation that not only 205 | preserves its underlying semantic but also literally unfolds from it, 206 | InGraph aims to serve both ends of the AWS CloudFormation community, by: 207 | 208 | - allowing professionals to leverage their existing knowledge and 209 | become more productive, 210 | - offering newcomers an effective learning path toward AWS 211 | CloudFormation. 212 | 213 | ## Glossary 214 | 215 | ### Infrastructure 216 | 217 | Infrastructure means deployed resources and collection of resources in 218 | contrast with custom code running on the infrastructure (e.g., code 219 | inside a Lambda function). Higher-level abstractions still represent 220 | infrastructure (e.g., a constellation of microservices) 221 | 222 | ### Infrastructure as Code 223 | 224 | Infrastructure as Code (IaC) means an artifact that represents this 225 | infrastructure in contrast with manual configuration (e.g., clicking 226 | through things in a console or doing stuff with a CLI). 227 | 228 | ### Graph 229 | 230 | A graph represents a state of the world, either that exists, or that 231 | should be. It is a declaration (the what, the data) and not an 232 | instruction (the how, the code). 233 | 234 | ### Multiscale Graph 235 | 236 | A multiscale graph is a graph in which the language is the same no 237 | matter the level of zoom. A good analogy is a geographic map and its 238 | graph of connectivity between areas. If one understands how to navigate 239 | the plan, one understands it at all levels of zoom (country, city, 240 | district, etc.). 241 | 242 | ### Infrastructure Graph 243 | 244 | An infrastructure graph is a particular type of multiscale graph in 245 | which nodes are resources or collection of resources. At the lowest 246 | level, it can, for instance, represent an S3 bucket notifying a Lambda 247 | function. At the highest level, it may represent [microservices at 248 | Netflix][netflix]. 249 | 250 | [github]: https://github.com/lifadev/ingraph 251 | [talk]: https://acloud.guru/series/serverlessconf-nyc-2019/view/yaml-better 252 | [ben]: https://twitter.com/ben11kehoe 253 | [netflix]: https://www.youtube.com/watch?v=-mL3zT1iIKw&feature=youtu.be&t=931 254 | [upto]: https://en.wikipedia.org/wiki/Up_to 255 | [isomorphism]: https://en.wikipedia.org/wiki/Isomorphism 256 | -------------------------------------------------------------------------------- /docs_backup/docs/ingraph/start.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: start 3 | title: Getting Started 4 | --- 5 | 6 | ## Installation 7 | 8 | InGraph requires [Python 3.8][python] or newer. Feel free to use your 9 | favorite tool or [pip][pip] to install the [`ingraph` package][pypi]. 10 | 11 | ``` 12 | python3.8 -m pip install --upgrade --user ingraph 13 | ``` 14 | 15 | > Note that it will also install InGraph's dependencies, which might 16 | > cause conflicts with other packages. You can also jump to the next 17 | > section for a more streamlined experience. 18 | 19 | Once the `ingraph` package installed, you can verify your installation 20 | by running the `ig` command. 21 | 22 | ``` 23 | ig 24 | ``` 25 | 26 | You should see the following welcome screen. 27 | 28 | ``` 29 | Usage: ig [OPTIONS] COMMAND [ARGS]... 30 | 31 | ___ ____ _ 32 | |_ _|_ __ / ___|_ __ __ _ _ __ | |__ 33 | | || '_ \| | _| '__/ _` | '_ \| '_ \ 34 | | || | | | |_| | | | (_| | |_) | | | | 35 | |___|_| |_|\____|_| \__,_| .__/|_| |_| 36 | https://lifa.dev/ingraph |_| v0.2.1 37 | 38 | Infrastructure Graph DSL for AWS CloudFormation. 39 | 40 | Options: 41 | -v, --version Show the version and exit. 42 | -h, --help Show this message and exit. 43 | 44 | Commands: 45 | cfn Translate infrastructure graphs to CloudFormation templates. 46 | ``` 47 | 48 | ## Usage 49 | 50 | For a better experience, we provide a [ready-to-use scaffold][scaffold] 51 | you can download to get started quickly and to adapt it to your needs. 52 | 53 | Start by unzipping it, and you should see the following content. 54 | 55 | ``` 56 | tree 57 | . 58 | ├── COPYING 59 | ├── Makefile 60 | ├── poetry.lock 61 | ├── pyproject.toml 62 | ├── README.md 63 | ├── setup.cfg 64 | └── src 65 | └── helloworld 66 | ├── example.py 67 | ├── handler.js 68 | └── __init__.py 69 | 70 | 2 directories, 9 files 71 | ``` 72 | 73 | It is a minimal Python package fine-tuned for [Poetry][poetry] (a 74 | high-level tool that assists you in the Python packaging ceremony) and 75 | [Visual Studio Code Remote Containers][remotedev] (a Docker powered 76 | development environment). 77 | 78 | > The usage of the [Visual Studio Code][vscode] editor is optional. 79 | > If you decide to use another editor, you need, at least, to 80 | > [install Poetry][poetrydoc] for the rest of the tutorial. Otherwise, 81 | > feel free to adapt the `pyproject.toml` file to your tool of choice. 82 | 83 | When you open the scaffold inside VS Code, a prompt asks you if you 84 | want to continue developing in a container. Choose `Reopen in Container` 85 | and wait for it to become ready. 86 | 87 | The scaffold comes with an `example.py` file located in `src/helloworld` 88 | that contains the following infrastructure description. 89 | 90 | ```python 91 | from ingraph.aws import Asset, aws_iam, aws_lambda 92 | 93 | 94 | class Example: 95 | arn: str 96 | 97 | def __init__(self) -> None: 98 | role = aws_iam.Role( 99 | AssumeRolePolicyDocument={ 100 | "Version": "2012-10-17", 101 | "Statement": { 102 | "Effect": "Allow", 103 | "Principal": {"Service": "lambda.amazonaws.com"}, 104 | "Action": "sts:AssumeRole", 105 | }, 106 | }, 107 | ManagedPolicyArns=[ 108 | "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 109 | ], 110 | ) 111 | handler = Asset(name="handler.js") 112 | function = aws_lambda.Function( 113 | Code=aws_lambda.Function.Code(ZipFile=handler.text), 114 | Handler="index.handle", 115 | Role=role.Arn, 116 | Runtime="nodejs12.x", 117 | ) 118 | self.arn = function.Arn 119 | ``` 120 | 121 | It creates an AWS Lambda function that returns a "Hello, World!" 122 | message. To provide the actual code of the Lambda function, we use an 123 | asset to reference the content of the `handler.js` file located in 124 | `src/helloworld`. 125 | 126 | ```js 127 | exports.handle = async () => "Hello, World!"; 128 | ``` 129 | 130 | We can use the InGraph CLI to translate from the InGraph DSL to the 131 | AWS CloudFormation YAML template. 132 | 133 | ``` 134 | ig cfn -i helloworld.example -r Example -o build/example.yaml 135 | ``` 136 | 137 | > In `helloworld.example`, `helloworld` is the name of your package 138 | > located in `src` and `example` is the name of your module located in 139 | > `src/helloworld/example.py`. 140 | 141 | The CLI creates a `build` folder with a file named `example.yaml` that 142 | contains your AWS CloudFormation template. 143 | 144 | ``` 145 | tree build/ 146 | build/ 147 | └── example.yaml 148 | 149 | 0 directories, 1 file 150 | ``` 151 | 152 | ```yaml 153 | AWSTemplateFormatVersion: "2010-09-09" 154 | Resources: 155 | RoleMW2CSPSW: 156 | Type: AWS::IAM::Role 157 | Properties: 158 | AssumeRolePolicyDocument: 159 | Version: "2012-10-17" 160 | Statement: 161 | Effect: Allow 162 | Principal: 163 | Service: lambda.amazonaws.com 164 | Action: sts:AssumeRole 165 | ManagedPolicyArns: 166 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 167 | FunctionODIWTV6H: 168 | Type: AWS::Lambda::Function 169 | Properties: 170 | Code: 171 | ZipFile: | 172 | exports.handle = async () => "Hello, World!"; 173 | Handler: index.handle 174 | Role: !GetAtt RoleMW2CSPSW.Arn 175 | Runtime: nodejs12.x 176 | Outputs: 177 | Arn: 178 | Value: !GetAtt FunctionODIWTV6H.Arn 179 | ``` 180 | 181 | Finally, you can use your tool of choice or the AWS CLI to deploy the 182 | AWS CloudFormation template. 183 | 184 | ``` 185 | aws cloudformation deploy \ 186 | --template-file build/example.yaml \ 187 | --stack-name ingraph-helloworld \ 188 | --capabilities CAPABILITY_IAM 189 | ``` 190 | 191 | Also, you can inspect the output of the freshly deployed stack to 192 | retrieve the identifier of your function. 193 | 194 | ``` 195 | aws cloudformation describe-stacks \ 196 | --stack-name ingraph-helloworld 197 | ``` 198 | 199 | ``` 200 | { 201 | "Stacks": [ 202 | { 203 | "Outputs": [ 204 | { 205 | "OutputKey": "Arn", 206 | "OutputValue": "arn:aws:lambda:..." 207 | } 208 | ], 209 | } 210 | ] 211 | } 212 | ``` 213 | 214 | To delete your stack, run the following command. 215 | 216 | ``` 217 | aws cloudformation delete-stack \ 218 | --stack-name ingraph-helloworld 219 | ``` 220 | 221 | That's it. Welcome to InGraph! 222 | 223 | [python]: https://www.python.org/downloads/ 224 | [pip]: https://pip.pypa.io/en/stable/ 225 | [pypi]: https://pypi.org/project/ingraph 226 | [poetry]: https://python-poetry.org/ 227 | [poetrydoc]: https://python-poetry.org/docs/ 228 | [vscode]: https://code.visualstudio.com/ 229 | [remotedev]: https://code.visualstudio.com/docs/remote/containers 230 | [scaffold]: https://github.com/lifadev/ingraph/releases/download/v0.2.1/ingraph-helloworld.zip 231 | -------------------------------------------------------------------------------- /docs_backup/img/blog/2020-03-31-introducing-ingraph/assistance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/blog/2020-03-31-introducing-ingraph/assistance.png -------------------------------------------------------------------------------- /docs_backup/img/blog/2020-03-31-introducing-ingraph/autocompletion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/blog/2020-03-31-introducing-ingraph/autocompletion.png -------------------------------------------------------------------------------- /docs_backup/img/blog/2020-03-31-introducing-ingraph/cloudformation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/blog/2020-03-31-introducing-ingraph/cloudformation.png -------------------------------------------------------------------------------- /docs_backup/img/blog/2020-03-31-introducing-ingraph/comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/blog/2020-03-31-introducing-ingraph/comparison.png -------------------------------------------------------------------------------- /docs_backup/img/blog/2020-03-31-introducing-ingraph/external_abstraction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/blog/2020-03-31-introducing-ingraph/external_abstraction.png -------------------------------------------------------------------------------- /docs_backup/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/favicon.png -------------------------------------------------------------------------------- /docs_backup/img/home/undraw_blooming_jtv6.svg: -------------------------------------------------------------------------------- 1 | blooming -------------------------------------------------------------------------------- /docs_backup/img/home/undraw_game_world_0o6q.svg: -------------------------------------------------------------------------------- 1 | game_world -------------------------------------------------------------------------------- /docs_backup/img/home/undraw_good_team_m7uu.svg: -------------------------------------------------------------------------------- 1 | good team -------------------------------------------------------------------------------- /docs_backup/img/ingraph/autocomplete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/ingraph/autocomplete.png -------------------------------------------------------------------------------- /docs_backup/img/ingraph/cloudformation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/ingraph/cloudformation.png -------------------------------------------------------------------------------- /docs_backup/img/ingraph/comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/ingraph/comparison.png -------------------------------------------------------------------------------- /docs_backup/img/ingraph/deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/ingraph/deployment.png -------------------------------------------------------------------------------- /docs_backup/img/ingraph/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/ingraph/example.png -------------------------------------------------------------------------------- /docs_backup/img/ingraph/external_abstraction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/ingraph/external_abstraction.png -------------------------------------------------------------------------------- /docs_backup/img/ingraph/ingraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/ingraph/ingraph.png -------------------------------------------------------------------------------- /docs_backup/img/ingraph/internal_abstraction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/ingraph/internal_abstraction.png -------------------------------------------------------------------------------- /docs_backup/img/ingraph/native_abstraction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/ingraph/native_abstraction.png -------------------------------------------------------------------------------- /docs_backup/img/ingraph/typing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/ingraph/typing.png -------------------------------------------------------------------------------- /docs_backup/img/ingraph/undraw_code_typing_7jnv.svg: -------------------------------------------------------------------------------- 1 | code typing -------------------------------------------------------------------------------- /docs_backup/img/ingraph/undraw_deliveries_131a.svg: -------------------------------------------------------------------------------- 1 | deliveries -------------------------------------------------------------------------------- /docs_backup/img/ingraph/undraw_dev_focus_b9xo.svg: -------------------------------------------------------------------------------- 1 | dev_focus -------------------------------------------------------------------------------- /docs_backup/img/logo-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/logo-256x256.png -------------------------------------------------------------------------------- /docs_backup/img/logo-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/docs_backup/img/logo-32x32.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend="poetry.masonry.api" 3 | requires=["poetry>=1.0.0"] 4 | 5 | [tool.poetry] 6 | name="ingraph" 7 | version="0.2.1" 8 | description="InGraph is an Infrastructure Graph DSL for AWS CloudFormation." 9 | license="AGPL-3.0" 10 | authors=["lifa.dev "] 11 | homepage="https://lifa.dev/ingraph" 12 | repository="https://github.com/lifadev/ingraph" 13 | readme="README.md" 14 | exclude=["**/*_test.py"] 15 | packages=[{ include="ingraph", from="src" }] 16 | 17 | [tool.poetry.dependencies] 18 | click="^7.1.1" 19 | "ingraph.aws" = "0.*" 20 | more-itertools="^8.2.0" 21 | mypy="^0.770" 22 | networkx="^2.4" 23 | parso="^0.7.0" 24 | python="^3.8" 25 | "ruamel.yaml"="^0.16.10" 26 | 27 | [tool.poetry.dev-dependencies] 28 | autoflake="^1.3.1" 29 | black="^19.10b0" 30 | isort="^4.3.21" 31 | pytest="^5.4.1" 32 | pytest-cov="^2.8.1" 33 | pytest-mock="^3.1.0" 34 | pytest-xdist="^1.31.0" 35 | rope="^0.16.0" 36 | toml="^0.10.0" 37 | twine="^3.1.1" 38 | 39 | [tool.poetry.scripts] 40 | ig="ingraph.cli:main" 41 | -------------------------------------------------------------------------------- /scaffold/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-buster 2 | 3 | RUN set -ex; \ 4 | \ 5 | echo "deb https://deb.nodesource.com/node_13.x buster main" \ 6 | > /etc/apt/sources.list.d/node.list; \ 7 | wget -qO- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -; \ 8 | \ 9 | apt update; \ 10 | apt install --yes --no-install-recommends \ 11 | git make nodejs silversearcher-ag tree zip; \ 12 | \ 13 | pip install poetry; \ 14 | mkdir -p ~/.config/pypoetry; \ 15 | echo '[virtualenvs]\ncreate=false' > ~/.config/pypoetry/config.toml; \ 16 | \ 17 | echo 'export PS1="\w>>> "' > ~/.bashrc 18 | 19 | CMD ["/bin/bash"] 20 | -------------------------------------------------------------------------------- /scaffold/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "dockerFile": "Dockerfile", 3 | "extensions": [ 4 | "bungcip.better-toml", 5 | "editorconfig.editorconfig", 6 | "esbenp.prettier-vscode", 7 | "ms-python.python" 8 | ], 9 | "postCreateCommand": "make install" 10 | } 11 | -------------------------------------------------------------------------------- /scaffold/.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | charset=utf-8 5 | end_of_line=lf 6 | insert_final_newline=true 7 | indent_size=4 8 | indent_style=space 9 | trim_trailing_whitespace=true 10 | 11 | [*.{md,rst}] 12 | trim_trailing_whitespace=false 13 | 14 | [Makefile] 15 | indent_style=tab 16 | -------------------------------------------------------------------------------- /scaffold/.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | -------------------------------------------------------------------------------- /scaffold/.gitignore: -------------------------------------------------------------------------------- 1 | .mypy_cache 2 | .pytest_cache 3 | .vscode/* 4 | !.vscode/settings.json 5 | *.egg-info 6 | __pycache__ 7 | build 8 | dist 9 | tmp 10 | -------------------------------------------------------------------------------- /scaffold/.prettierignore: -------------------------------------------------------------------------------- 1 | .mypy_cache 2 | .vscode/* 3 | !.vscode/settings.json 4 | *.egg-info 5 | __pycache__ 6 | build 7 | dist 8 | -------------------------------------------------------------------------------- /scaffold/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // 3 | // Editor 4 | // 5 | 6 | "files.exclude": { 7 | "**/.git": true, 8 | "**/__pycache__": true, 9 | "**/*.egg-info": true, 10 | ".devcontainer": true, 11 | ".editorconfig": true, 12 | ".gitattributes": true, 13 | ".gitignore": true, 14 | ".mypy_cache": true, 15 | ".prettierignore": true, 16 | "poetry.lock": true, 17 | "setup.cfg": true 18 | }, 19 | 20 | // 21 | // Extensions 22 | // 23 | 24 | "python.formatting.provider": "black", 25 | "python.linting.enabled": true, 26 | "python.linting.mypyEnabled": true, 27 | "python.linting.pylintEnabled": false 28 | } 29 | -------------------------------------------------------------------------------- /scaffold/COPYING: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /scaffold/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | ig cfn -i helloworld.example -r Example -o build/example.yaml 3 | 4 | .PHONY: build 5 | 6 | type: 7 | MYPYPATH=./src mypy -p helloworld 8 | 9 | format: 10 | black --include '\.pyi?$$' src 11 | autoflake -ri src 12 | isort -rc src 13 | npx prettier --end-of-line lf --write '**/*.{css,html,js,json,md,yaml,yml}' 14 | 15 | install: 16 | poetry install 17 | 18 | clean: 19 | rm -rf .mypy_cache build dist 20 | find . -name __pycache__ -o -name *.egg-info | xargs rm -rf 21 | 22 | dist: clean install format type 23 | poetry build 24 | 25 | .PHONY: dist 26 | -------------------------------------------------------------------------------- /scaffold/README.md: -------------------------------------------------------------------------------- 1 | # ingraph-helloworld 2 | 3 | > A 'Hello, World!' InGraph component. 4 | -------------------------------------------------------------------------------- /scaffold/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend="poetry.masonry.api" 3 | requires=["poetry>=1.0.0"] 4 | 5 | [tool.poetry] 6 | name="ingraph-helloworld" 7 | version="0.0.0" 8 | description="A 'Hello, World!' InGraph component." 9 | license="MIT" 10 | authors=[] 11 | readme="README.md" 12 | packages=[{ include="helloworld", from="src" }] 13 | 14 | [tool.poetry.dependencies] 15 | python="^3.8" 16 | ingraph = "*" 17 | 18 | [tool.poetry.dev-dependencies] 19 | autoflake="^1.3.1" 20 | black="^19.10b0" 21 | isort="^4.3.21" 22 | rope="^0.16.0" 23 | twine="^3.1.1" 24 | -------------------------------------------------------------------------------- /scaffold/setup.cfg: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = False 3 | incremental = True 4 | python_version = 3.8 5 | warn_unused_configs = True 6 | 7 | # Untyped definitions and calls 8 | check_untyped_defs = True 9 | disallow_incomplete_defs = True 10 | disallow_untyped_calls = True 11 | disallow_untyped_defs = True 12 | 13 | # Configuring warnings 14 | warn_unused_ignores = True 15 | 16 | # Suppressing errors 17 | ignore_errors = False 18 | show_none_errors = True 19 | 20 | # Miscellaneous strictness flags 21 | allow_redefinition = False 22 | implicit_reexport = False 23 | -------------------------------------------------------------------------------- /scaffold/src/helloworld/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/scaffold/src/helloworld/__init__.py -------------------------------------------------------------------------------- /scaffold/src/helloworld/example.py: -------------------------------------------------------------------------------- 1 | from ingraph.aws import Asset, aws_iam, aws_lambda 2 | 3 | 4 | class Example: 5 | arn: str 6 | 7 | def __init__(self) -> None: 8 | role = aws_iam.Role( 9 | AssumeRolePolicyDocument={ 10 | "Version": "2012-10-17", 11 | "Statement": { 12 | "Effect": "Allow", 13 | "Principal": {"Service": "lambda.amazonaws.com"}, 14 | "Action": "sts:AssumeRole", 15 | }, 16 | }, 17 | ManagedPolicyArns=[ 18 | "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 19 | ], 20 | ) 21 | handler = Asset(name="handler.js") 22 | function = aws_lambda.Function( 23 | Code=aws_lambda.Function.Code(ZipFile=handler.text), 24 | Handler="index.handle", 25 | Role=role.Arn, 26 | Runtime="nodejs12.x", 27 | ) 28 | self.arn = function.Arn 29 | -------------------------------------------------------------------------------- /scaffold/src/helloworld/handler.js: -------------------------------------------------------------------------------- 1 | exports.handle = async () => "Hello, World!"; 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | incremental = True 4 | namespace_packages = True 5 | python_version = 3.8 6 | warn_unused_configs = True 7 | 8 | # Untyped definitions and calls 9 | check_untyped_defs = True 10 | disallow_incomplete_defs = True 11 | disallow_untyped_calls = False 12 | disallow_untyped_decorators = True 13 | disallow_untyped_defs = True 14 | 15 | # None and optional handling 16 | no_implicit_optional = True 17 | strict_optional = True 18 | 19 | # Configuring warnings 20 | warn_no_return = True 21 | warn_redundant_casts = True 22 | warn_unreachable = True 23 | warn_unused_ignores = True 24 | 25 | # Suppressing errors 26 | ignore_errors = False 27 | show_none_errors = True 28 | 29 | # Miscellaneous strictness flags 30 | allow_redefinition = False 31 | implicit_reexport = True 32 | strict_equality = True 33 | 34 | [mypy-ingraph.engine.core] 35 | ignore_errors = True 36 | 37 | [mypy-tests.*] 38 | ignore_errors = True 39 | 40 | [tool:pytest] 41 | norecursedirs = 42 | .* *.egg-info __pycache__ build dist tmp 43 | tests/e2e/data 44 | 45 | log_cli = true 46 | log_cli_level = INFO 47 | 48 | markers = 49 | focus: targets a specific test during development 50 | 51 | [coverage:report] 52 | omit= 53 | /tmp/* 54 | /usr/local/* 55 | src/ingraph/cli/* 56 | tests/* 57 | 58 | exclude_lines = 59 | pragma: no cover 60 | def __repr__ 61 | if TYPE_CHECKING: 62 | -------------------------------------------------------------------------------- /src/ingraph/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Farzad Senart and Lionel Suss. All rights reserved. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | import sys 17 | 18 | if __name__ == "__main__": 19 | from .cli import main 20 | 21 | sys.exit(main()) 22 | -------------------------------------------------------------------------------- /src/ingraph/cli/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Farzad Senart and Lionel Suss. All rights reserved. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | import importlib 17 | import importlib.metadata 18 | import importlib.resources 19 | import json 20 | import platform 21 | import re 22 | import sys 23 | from typing import Any 24 | 25 | import click 26 | 27 | VER = importlib.metadata.version("ingraph") 28 | 29 | 30 | @click.group( 31 | "ig", 32 | context_settings=dict(help_option_names=["-h", "--help"]), 33 | help=f"""\b 34 | ___ ____ _ 35 | |_ _|_ __ / ___|_ __ __ _ _ __ | |__ 36 | | || '_ \| | _| '__/ _` | '_ \| '_ \\ 37 | | || | | | |_| | | | (_| | |_) | | | | 38 | |___|_| |_|\____|_| \__,_| .__/|_| |_| 39 | https://lifa.dev/ingraph |_| v{VER} 40 | \b 41 | Infrastructure Graph DSL for AWS CloudFormation. 42 | """, 43 | ) 44 | @click.version_option( 45 | None, 46 | "-v", 47 | "--version", 48 | message=json.dumps( 49 | { 50 | "ingraph": VER, 51 | "python": platform.python_version(), 52 | platform.system().lower(): platform.release(), 53 | **{ 54 | d: importlib.metadata.version(d) 55 | for d in [ 56 | re.split(r"[^a-zA-Z0-9_.-]", d)[0] 57 | for d in (importlib.metadata.requires("ingraph") or []) 58 | ] 59 | }, 60 | }, 61 | indent=2, 62 | ), 63 | ) 64 | def command() -> None: 65 | ... 66 | 67 | 68 | def main() -> int: 69 | try: 70 | init() 71 | command.main(prog_name=command.name, args=sys.argv[1:], standalone_mode=False) 72 | return 0 73 | except click.ClickException as exc: 74 | exc.show() 75 | return exc.exit_code 76 | return 1 # type: ignore 77 | 78 | 79 | def init() -> None: 80 | for name in importlib.resources.contents(__package__): 81 | if name.startswith("cmd_"): 82 | name = name[:-3] if name.endswith(".py") else name 83 | mod: Any = importlib.import_module(f".{name}", __package__) 84 | if hasattr(mod, "init"): 85 | mod.init() 86 | command.add_command(mod.command) 87 | -------------------------------------------------------------------------------- /src/ingraph/cli/cmd_cfn.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Farzad Senart and Lionel Suss. All rights reserved. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | from pathlib import Path 17 | from shutil import copyfile 18 | 19 | import click 20 | 21 | from .. import engine 22 | 23 | 24 | @click.command(name="cfn") 25 | @click.option( 26 | "--input", 27 | "-i", 28 | "input_", 29 | metavar="FILE|MODULE", 30 | help="Read graph from file or module.", 31 | required=True, 32 | ) 33 | @click.option( 34 | "--root", "-r", metavar="DEFINITION", help="Start with definition.", required=True, 35 | ) 36 | @click.option( 37 | "--output", "-o", metavar="FILE", help="Write YAML to file.", required=True, 38 | ) 39 | def command(input_: str, root: str, output: str) -> None: 40 | """Translate infrastructure graphs to CloudFormation templates.""" 41 | 42 | template, assets = engine.tocfn(input_, root) 43 | Path(output).parent.mkdir(parents=True, exist_ok=True) 44 | Path(output).write_text(template, "utf-8") 45 | for k, v in assets.items(): 46 | copyfile(v, Path(output).parent / k) 47 | -------------------------------------------------------------------------------- /src/ingraph/cli/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/src/ingraph/cli/py.typed -------------------------------------------------------------------------------- /src/ingraph/engine/Grammar: -------------------------------------------------------------------------------- 1 | # Grammar for InGraph 2 | 3 | file_input: (NEWLINE | import_stmt | classdef)* ENDMARKER 4 | 5 | funcdef: 'def' NAME parameters ['->' 'None'] ':' suite 6 | parameters: '(' [typedargslist] ')' 7 | typedargslist: (tfpdef ['=' expr] (',' tfpdef ['=' expr])*) [','] 8 | tfpdef: NAME [':' expr] 9 | 10 | stmt: expr_stmt NEWLINE 11 | expr_stmt: testlist_expr (annassign | testlist | ('=' testlist_expr)*) 12 | annassign: ':' expr ['=' expr] 13 | testlist_expr: expr (',' expr)* [','] 14 | 15 | # note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS 16 | import_stmt: ('from' (('.' | '...')* dotted_name | ('.' | '...')+) 17 | 'import' import_as_names) 18 | import_as_name: NAME ['as' NAME] 19 | import_as_names: import_as_name (',' import_as_name)* [','] 20 | dotted_name: NAME ('.' NAME)* 21 | 22 | suite: stmt | NEWLINE INDENT stmt+ DEDENT 23 | 24 | expr: factor ('+' factor)* 25 | factor: ('-') atom_expr | atom_expr 26 | atom_expr: atom trailer* 27 | atom: ( 28 | '[' [testlist] ']' | 29 | '{' [testdict] '}' | 30 | NAME | NUMBER | strings | '...' | 'None' | 'True' | 'False') 31 | trailer: '(' [arglist] ')' | '[' expr ']' | '.' NAME 32 | testlist: expr (',' expr)* [','] 33 | testdict: (expr ':' expr) (',' (expr ':' expr))* [','] 34 | 35 | classdef: 'class' NAME ['(' dotted_name ')'] ':' classdef_suite 36 | classdef_suite: classhead | NEWLINE INDENT (classhead | funcdef)+ DEDENT 37 | classhead: doc | annotation 38 | doc : strings NEWLINE 39 | annotation: NAME ':' (NAME | (NAME '[' NAME ']')) NEWLINE 40 | 41 | arglist: argument (',' argument)* [','] 42 | 43 | argument: expr | expr '=' expr 44 | 45 | strings: (STRING | fstring)+ 46 | fstring: FSTRING_START fstring_content* FSTRING_END 47 | fstring_content: FSTRING_STRING | fstring_expr 48 | fstring_expr: '{' testlist '}' 49 | -------------------------------------------------------------------------------- /src/ingraph/engine/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Farzad Senart and Lionel Suss. All rights reserved. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | from pathlib import Path 18 | from typing import Mapping, Tuple 19 | 20 | import networkx as nx 21 | from ruamel.yaml import YAML 22 | 23 | from . import core, encoder, importer 24 | 25 | 26 | def tocfn(target: str, entrypoint: str) -> Tuple[str, Mapping[str, Path]]: 27 | with importer.external_hook(): 28 | with importer.aws_hook(): 29 | module = importer.import_target(target) 30 | root = getattr(module, entrypoint) 31 | graph = core.process(root) 32 | return encoder.encode_cfn(graph) 33 | -------------------------------------------------------------------------------- /src/ingraph/engine/encoder.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Farzad Senart and Lionel Suss. All rights reserved. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | import base64 17 | import hashlib 18 | import io 19 | from dataclasses import dataclass 20 | from pathlib import Path 21 | from typing import Any, ClassVar, Mapping, Optional, Set, Tuple, cast 22 | 23 | import networkx as nx 24 | from ruamel.yaml import YAML, Representer, ScalarNode 25 | 26 | from . import core 27 | 28 | 29 | def encode_cfn(graph: nx.Graph) -> Tuple[str, Mapping[str, Path]]: 30 | template: Any = dict(AWSTemplateFormatVersion="2010-09-09") 31 | 32 | assets: Set[core.Asset] = set() 33 | for node in graph: 34 | assets.update(node.assets) 35 | if assets: 36 | template["Parameters"] = dict( 37 | AssetsS3Bucket=dict(Type="String"), AssetsS3Prefix=dict(Type="String"), 38 | ) 39 | 40 | if params := core.getparameters(graph): 41 | template.setdefault("Parameters", {}).update( 42 | {_getidn(k): _getparam(v) for k, v in params.items()} 43 | ) 44 | 45 | nodes = [node for node in graph if isinstance(node.resource, core.NativeResource)] 46 | if not nodes: 47 | raise TypeError("at least one AWS resource must be provided") 48 | resources = template["Resources"] = cast(Any, {}) 49 | for node in nodes: 50 | resource = resources[_getnidn(node)] = dict(Type=node.resource._ig_kind) 51 | if props := _t(node.resource._ig_data): 52 | resource["Properties"] = props 53 | if deps := node.resource._ig_deps: 54 | resource["DependsOn"] = [_getnidn(d._ig_node) for d in deps] 55 | 56 | if outputs := core.getoutputs(graph): 57 | template["Outputs"] = {_getidn(k): _getoutput(v) for k, v in outputs.items()} 58 | 59 | yaml = YAML() 60 | for t in ( 61 | MultilineStr, 62 | GetAtt, 63 | Join, 64 | Ref, 65 | RefValue, 66 | Select, 67 | Split, 68 | Sub, 69 | CIDR, 70 | ): 71 | yaml.register_class(t) 72 | 73 | stream = io.BytesIO() 74 | YAML().dump(template, stream) 75 | 76 | return ( 77 | stream.getvalue().decode("utf-8"), 78 | {asset._ig_hash: asset._ig_path for asset in assets}, 79 | ) 80 | 81 | 82 | def _getnidn(node: core.Node) -> str: 83 | data = cast(core.NativeResource, node.resource) 84 | pre = "-".join([name for name in node.lineage] + [data._ig_kind]) 85 | dig = hashlib.sha1(pre.encode("utf-8")).digest()[:5] 86 | idn = base64.b32encode(dig).decode("utf-8").upper() 87 | name = "".join([name.title().replace("_", "") for name in node.lineage[1:]]) 88 | return (name + idn)[-255:] 89 | 90 | 91 | def _getidn(orig: str) -> str: 92 | return orig.title().replace("_", "") 93 | 94 | 95 | def _getaidn(orig: str) -> str: 96 | return orig.rstrip("_").replace("_", ".") 97 | 98 | 99 | def _getparam(info: core.Parameter) -> Any: 100 | typ: str 101 | if isinstance(info, core.String): 102 | typ = "String" 103 | elif isinstance(info, core.Number): 104 | typ = "Number" 105 | elif isinstance(info, core.LISTS[core.String]): 106 | typ = "CommaDelimitedList" 107 | elif isinstance(info, core.LISTS[core.Number]): 108 | typ = "List" 109 | res = dict(Type=typ) 110 | defl = info._ig_default 111 | if defl is None: 112 | return res 113 | defl = defl._ig_value if isinstance(defl, core.ManagedList) else [defl] 114 | defl = [v._ig_value for v in defl] 115 | if len(defl) > 1: 116 | defl = [v if isinstance(v, str) else str(v) for v in defl] 117 | res.update(Default=",".join(defl)) 118 | else: 119 | res.update(Default=defl[0]) 120 | return res 121 | 122 | 123 | def _getoutput(value: Any) -> Any: 124 | return dict(Value=_t(value)) 125 | 126 | 127 | def _t(value: Any) -> Any: 128 | 129 | if isinstance(value, core.CIDR): 130 | return CIDR(value) 131 | if isinstance(value, core.Base64Encoded): 132 | return {"Fn::Base64": _t(value._ig_target)} 133 | if isinstance(value, core.AccountID): 134 | return RefValue("AWS::AccountId") 135 | if isinstance(value, core.AvailabilityZones): 136 | return {"Fn::GetAZs": ""} 137 | if isinstance(value, core.NotificationARNs): 138 | return RefValue("AWS::NotificationARNs") 139 | if isinstance(value, core.Partition): 140 | return RefValue("AWS::Partition") 141 | if isinstance(value, core.Region): 142 | return RefValue("AWS::Region") 143 | if isinstance(value, core.StackID): 144 | return RefValue("AWS::StackId") 145 | if isinstance(value, core.StackName): 146 | return RefValue("AWS::StackName") 147 | if isinstance(value, core.URLSuffix): 148 | return RefValue("AWS::URLSuffix") 149 | if isinstance(value, core.Parameter): 150 | return RefValue(_getidn(value._ig_name), style=None) 151 | if isinstance(value, core.Join): 152 | return Join(value) 153 | if isinstance(value, core.Ref): 154 | return Ref(value) 155 | if isinstance(value, core.Select): 156 | return Select(value) 157 | if isinstance(value, core.Split): 158 | return Split(value) 159 | if isinstance(value, core.Sub): 160 | return Sub(value) 161 | if isinstance(value, core.AssetBucket): 162 | return RefValue("AssetsS3Bucket") 163 | if isinstance(value, core.AssetKey): 164 | value = core.String(f"${{AssetsS3Prefix}}{value._ig_asset._ig_hash}") 165 | return Sub(core.Sub(value, core.Map({}))) 166 | if isinstance(value, core.AssetURI): 167 | value = core.String( 168 | f"s3://${{AssetsS3Bucket}}/${{AssetsS3Prefix}}" 169 | f"{value._ig_asset._ig_hash}" 170 | ) 171 | return Sub(core.Sub(value, core.Map({}))) 172 | if isinstance(value, core.AssetURL): 173 | value = core.String( 174 | f"https://${{AssetsS3Bucket}}.s3.amazonaws.com/${{AssetsS3Prefix}}" 175 | f"{value._ig_asset._ig_hash}" 176 | ) 177 | return Sub(core.Sub(value, core.Map({}))) 178 | if isinstance(value, core.Property): 179 | return _t(value._ig_data) 180 | if isinstance(value, core.Attribute): 181 | return GetAtt(value) 182 | if isinstance( 183 | value, 184 | ( 185 | core.Boolean, 186 | core.Number, 187 | core.String, 188 | core.ManagedList, 189 | core.FreeList, 190 | core.Map, 191 | ), 192 | ): 193 | return _t(value._ig_value) 194 | if isinstance(value, dict): 195 | return {_t(k): _t(v) for k, v in value.items()} 196 | if isinstance(value, list): 197 | return [_t(v) for v in value] 198 | if isinstance(value, str) and "\n" in value: 199 | return MultilineStr(value) 200 | return value 201 | 202 | 203 | @dataclass 204 | class MultilineStr: 205 | tag: ClassVar[str] = "tag:yaml.org,2002:str" 206 | v: str 207 | 208 | @classmethod 209 | def to_yaml(cls, r: Representer, n: "MultilineStr") -> Any: 210 | return r.represent_scalar(cls.tag, n.v, style="|") 211 | 212 | 213 | @dataclass 214 | class GetAtt: 215 | tag: ClassVar[str] = "!GetAtt" 216 | v: core.Attribute 217 | 218 | @classmethod 219 | def to_yaml(cls, r: Representer, n: "GetAtt") -> Any: 220 | return r.represent_scalar( 221 | cls.tag, f"{_getnidn(n.v._ig_node)}.{_getaidn(n.v._ig_name)}", 222 | ) 223 | 224 | 225 | @dataclass 226 | class Join: 227 | tag: ClassVar[str] = "!Join" 228 | v: core.Join 229 | 230 | @classmethod 231 | def to_yaml(cls, r: Representer, n: "Join") -> Any: 232 | return r.represent_sequence(cls.tag, _t([n.v._ig_separator, n.v._ig_items]),) 233 | 234 | 235 | @dataclass 236 | class Ref: 237 | tag: ClassVar[str] = "!Ref" 238 | v: core.Ref 239 | 240 | @classmethod 241 | def to_yaml(cls, r: Representer, n: "Ref") -> Any: 242 | return r.represent_scalar(cls.tag, _getnidn(n.v._ig_node)) 243 | 244 | 245 | @dataclass 246 | class RefValue: 247 | tag: ClassVar[str] = "!Ref" 248 | v: str 249 | style: Optional[str] = "'" 250 | 251 | @classmethod 252 | def to_yaml(cls, r: Representer, n: "RefValue") -> Any: 253 | return r.represent_scalar(cls.tag, n.v, style=n.style) 254 | 255 | 256 | @dataclass 257 | class Select: 258 | tag: ClassVar[str] = "!Select" 259 | v: core.Select 260 | 261 | @classmethod 262 | def to_yaml(cls, r: Representer, n: "Select") -> Any: 263 | return r.represent_sequence(cls.tag, _t([n.v._ig_index, n.v._ig_items]),) 264 | 265 | 266 | @dataclass 267 | class Split: 268 | tag: ClassVar[str] = "!Split" 269 | v: core.Split 270 | 271 | @classmethod 272 | def to_yaml(cls, r: Representer, n: "Split") -> Any: 273 | return r.represent_sequence(cls.tag, _t([n.v._ig_separator, n.v._ig_target]),) 274 | 275 | 276 | @dataclass 277 | class Sub: 278 | tag: ClassVar[str] = "!Sub" 279 | v: core.Sub 280 | 281 | @classmethod 282 | def to_yaml(cls, r: Representer, n: "Sub") -> Any: 283 | fmt = n.v._ig_format._ig_value 284 | kwargs = {} 285 | for k, v in n.v._ig_kwargs._ig_value.items(): 286 | old = new = f"${{{k._ig_value}}}" 287 | if isinstance(v, core.Parameter): 288 | new = f"${{{_getidn(v._ig_name)}}}" 289 | elif isinstance(v, core.Ref): 290 | new = f"${{{_getnidn(v._ig_node)}}}" 291 | elif isinstance(v, core.Attribute): 292 | new = f"${{{_getnidn(v._ig_node)}.{_getaidn(v._ig_name)}}}" 293 | elif isinstance(getattr(v, "_ig_value", None), (str, int, float)): 294 | new = f"{v._ig_value}" 295 | else: 296 | kwargs[k] = v 297 | fmt = fmt.replace(old, new) 298 | if not kwargs: 299 | return r.represent_scalar(cls.tag, _t(fmt)) 300 | return r.represent_sequence(cls.tag, _t([fmt, kwargs])) 301 | 302 | 303 | @dataclass 304 | class CIDR: 305 | tag: ClassVar[str] = "!Cidr" 306 | v: core.CIDR 307 | 308 | @classmethod 309 | def to_yaml(cls, r: Representer, n: "CIDR") -> Any: 310 | return r.represent_sequence( 311 | cls.tag, _t([n.v._ig_block, n.v._ig_count, n.v._ig_bits]) 312 | ) 313 | -------------------------------------------------------------------------------- /src/ingraph/engine/importer.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Farzad Senart and Lionel Suss. All rights reserved. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | import ast 17 | import importlib 18 | import importlib.abc 19 | import importlib.util 20 | import platform 21 | import sys 22 | import types 23 | import typing 24 | from contextlib import contextmanager 25 | from functools import lru_cache 26 | from importlib.machinery import ModuleSpec, PathFinder, SourceFileLoader 27 | from importlib.util import spec_from_file_location 28 | from pathlib import Path 29 | from types import ModuleType 30 | from typing import (Any, Dict, Final, Iterator, List, Mapping, Optional, 31 | Sequence, Tuple, Type, Union, cast) 32 | 33 | from mypy import api as mypyapi 34 | 35 | from . import core, parser 36 | 37 | 38 | def import_target(target: str) -> types.ModuleType: 39 | if Path(target).is_file(): 40 | return import_file(Path(target)) 41 | else: 42 | return import_module(target) 43 | 44 | 45 | def import_file(path: Path) -> types.ModuleType: 46 | source = path.read_text("utf-8") 47 | filename = str(path) 48 | module = types.ModuleType("") 49 | import_content(source, filename, module) 50 | return module 51 | 52 | 53 | def import_module(name: str) -> types.ModuleType: 54 | module = importlib.import_module(name) 55 | spec = module.__spec__ 56 | if spec is None or not isinstance( 57 | spec.loader, (AWSPackageImporter, AWSModuleImporter, ExternalImporter) 58 | ): 59 | raise ModuleNotFoundError(f"No module named {name!r}") 60 | return module 61 | 62 | 63 | def import_content(source: str, filename: str, module: ModuleType) -> None: 64 | _runmypy(filename) 65 | setattr(module, "__builtins__", dict(__import__=_hook__import__)) 66 | parser.process(source, filename, module) 67 | 68 | 69 | def _runmypy(filename: str) -> None: # pragma: no cover 70 | null = "nul" if platform.system() == "Windows" else "/dev/null" 71 | hooks = sys.meta_path[:3] 72 | sys.meta_path[:] = sys.meta_path[3:] 73 | try: 74 | result = mypyapi.run( 75 | [ 76 | "--python-version", 77 | "3.8", 78 | "--namespace-packages", 79 | "--disallow-untyped-calls", 80 | "--disallow-untyped-defs", 81 | "--disallow-incomplete-defs", 82 | "--no-implicit-reexport", 83 | "--strict", 84 | "--no-incremental", 85 | f"--cache-dir={null}", 86 | f"--config-file={null}", 87 | "--no-warn-unused-configs", 88 | "--pretty", 89 | # "--show-error-codes", 90 | filename, 91 | ] 92 | ) 93 | if result[2] != 0: 94 | if result[0]: 95 | print(result[0], file=sys.stderr) 96 | sys.exit(result[2]) 97 | finally: 98 | sys.meta_path[0:0] = hooks 99 | 100 | 101 | class AWSPackageImporter(importlib.abc.MetaPathFinder, importlib.abc.Loader): 102 | def find_spec( 103 | self, 104 | fullname: str, 105 | path: Optional[Sequence[Union[bytes, str]]], 106 | target: Optional[ModuleType] = None, 107 | ) -> Optional[ModuleSpec]: 108 | if fullname != "ingraph.aws": 109 | return None 110 | parent = Path(cast(Sequence[str], path)[0]) / "aws" 111 | origin = parent / "__init__.pyi" 112 | return spec_from_file_location( 113 | fullname, origin, loader=self, submodule_search_locations=[str(parent)] 114 | ) 115 | 116 | @staticmethod 117 | def create_module(spec: ModuleSpec) -> Optional[ModuleType]: 118 | ... 119 | 120 | @staticmethod 121 | def exec_module(module: ModuleType) -> None: 122 | info = dict(__module__=module.__name__) 123 | for k, v in dict( 124 | ACCOUNT_ID=core.AccountID, 125 | AVAILABILITY_ZONES=core.AvailabilityZones, 126 | NOTIFICATION_ARNS=core.NotificationARNs, 127 | PARTITION=core.Partition, 128 | REGION=core.Region, 129 | STACK_ID=core.StackID, 130 | STACK_NAME=core.StackName, 131 | URL_SUFFIX=core.URLSuffix, 132 | ).items(): 133 | setattr(module, k, v()) 134 | for k, v in dict( 135 | b64encode=core.Base64Encoded, cidr=core.CIDR, Asset=core.Asset 136 | ).items(): 137 | setattr(module, k, v) 138 | Tag = types.new_class( 139 | "Tag", 140 | (core.Property,), 141 | dict(module=module.__name__, params=[("Key", True), ("Value", True)]), 142 | ) 143 | setattr(module, Tag.__name__, Tag) 144 | 145 | 146 | class AWSModuleImporter(importlib.abc.MetaPathFinder, importlib.abc.Loader): 147 | def find_spec( 148 | self, 149 | fullname: str, 150 | path: Optional[Sequence[Union[bytes, str]]], 151 | target: Optional[ModuleType] = None, 152 | ) -> Optional[ModuleSpec]: 153 | pre, _, post = fullname.rpartition(".") 154 | if pre != "ingraph.aws": 155 | return None 156 | origin = Path(cast(Sequence[str], path)[0]) / f"{post}.pyi" 157 | if not origin.is_file(): 158 | raise ModuleNotFoundError(f"No module named {fullname!r}") 159 | return spec_from_file_location(fullname, origin, loader=self) 160 | 161 | @staticmethod 162 | def create_module(spec: ModuleSpec) -> Optional[ModuleType]: 163 | ... 164 | 165 | @staticmethod 166 | def exec_module(module: ModuleType) -> None: 167 | filename = module.__file__ 168 | source = Path(filename).read_text() 169 | tree = ast.parse(source, filename, "exec") 170 | ns = _getawsns(tree) 171 | defs = _getawsdefs(module.__name__, source, ns, tree) 172 | for k, v in defs.items(): 173 | setattr(module, k, v) 174 | 175 | 176 | def _getawsns(tree: ast.Module) -> str: 177 | for node in ast.iter_child_nodes(tree): 178 | if isinstance(node, ast.Assign): 179 | return cast(ast.Constant, node.value).value 180 | raise NotImplementedError("namespace not found") # pragma: no cover 181 | 182 | 183 | def _getawsdefs( 184 | module: str, source: str, ns: str, tree: ast.Module 185 | ) -> Mapping[str, Type]: 186 | res = {} 187 | for node in ast.iter_child_nodes(tree): 188 | if isinstance(node, ast.ClassDef): 189 | res[node.name] = _getawsdef(module, source, ns, node.name, node) 190 | return res 191 | 192 | 193 | def _getawsdef( 194 | module: str, source: str, ns: str, name: str, tree: ast.ClassDef 195 | ) -> Type: 196 | kind = f"{ns}::{name}" 197 | attrs = _getawsattrs(source, tree) 198 | params = _getawsparams(tree) 199 | defn = types.new_class( 200 | name, 201 | (core.NativeResource,), 202 | dict(module=module, kind=kind, attrs=attrs, params=params), 203 | ) 204 | defn.__doc__ = ast.get_docstring(tree) 205 | props = _getawsprops(module, tree) 206 | for k, v in props.items(): 207 | setattr(defn, k, v) 208 | return defn 209 | 210 | 211 | def _getawsprops(module: str, tree: ast.ClassDef) -> Mapping[str, Type]: 212 | res = {} 213 | for node in ast.iter_child_nodes(tree): 214 | if isinstance(node, ast.ClassDef): 215 | res[node.name] = _getawsprop(module, node.name, node) 216 | return res 217 | 218 | 219 | def _getawsprop(module: str, name: str, tree: ast.ClassDef) -> Type: 220 | params = _getawsparams(tree) 221 | return types.new_class(name, (core.Property,), dict(module=module, params=params),) 222 | 223 | 224 | def _getawsattrs(source: str, tree: ast.ClassDef) -> Mapping[str, Type]: 225 | res = {} 226 | for node in ast.iter_child_nodes(tree): 227 | if isinstance(node, ast.AnnAssign): 228 | name = cast(ast.Name, node.target).id 229 | anno = node.annotation 230 | txt = cast(str, ast.get_source_segment(source, anno)) 231 | res[name] = eval(txt) 232 | return res 233 | 234 | 235 | def _getawsparams(tree: ast.ClassDef) -> Sequence[Tuple[str, bool]]: 236 | res = [] 237 | for node in ast.iter_child_nodes(tree): 238 | if isinstance(node, ast.FunctionDef): 239 | args = node.args 240 | defs = args.kw_defaults 241 | for idx, arg in enumerate(args.kwonlyargs): 242 | res.append((arg.arg, defs[idx] is None)) 243 | return res 244 | 245 | 246 | @contextmanager 247 | def aws_hook() -> Iterator[None]: 248 | hooks = [AWSPackageImporter(), AWSModuleImporter()] 249 | sys.meta_path[0:0] = hooks 250 | try: 251 | yield 252 | finally: 253 | sys.meta_path[:] = sys.meta_path[len(hooks) :] 254 | 255 | 256 | class ExternalImporter(importlib.abc.MetaPathFinder, importlib.abc.Loader): 257 | def find_spec( 258 | self, 259 | fullname: str, 260 | path: Optional[Sequence[Union[bytes, str]]], 261 | target: Optional[ModuleType] = None, 262 | ) -> Optional[ModuleSpec]: 263 | spec = PathFinder.find_spec(fullname, path, target) # type: ignore 264 | if spec is None or not isinstance(spec.loader, SourceFileLoader): 265 | raise ModuleNotFoundError(f"No module named {fullname!r}") 266 | return spec_from_file_location( 267 | spec.name, 268 | cast(str, spec.origin), 269 | loader=self, 270 | submodule_search_locations=spec.submodule_search_locations, 271 | ) 272 | 273 | @staticmethod 274 | def create_module(spec: ModuleSpec) -> Optional[ModuleType]: 275 | ... 276 | 277 | @staticmethod 278 | def exec_module(module: ModuleType) -> None: 279 | spec = cast(ModuleSpec, module.__spec__) 280 | filename = cast(str, spec.origin) 281 | source = Path(filename).read_text("utf-8") 282 | import_content(source, filename, module) 283 | 284 | 285 | @contextmanager 286 | def external_hook() -> Iterator[None]: 287 | hook = ExternalImporter() 288 | sys.meta_path.insert(0, hook) 289 | try: 290 | yield 291 | finally: 292 | del sys.meta_path[0] 293 | 294 | 295 | @lru_cache 296 | def _hook_typing() -> ModuleType: 297 | hook = ModuleType(typing.__name__) 298 | hook.__file__ = typing.__file__ 299 | for attr in ["__file__", "List"]: 300 | setattr(hook, attr, getattr(typing, attr)) 301 | return hook 302 | 303 | 304 | def _hook__import__( 305 | name: str, 306 | globals_: Any, 307 | locals_: Any, 308 | fromlist: Optional[Sequence[str]], 309 | level: int, 310 | ) -> Any: 311 | if level == 0 and name in sys.modules: 312 | if name == "typing": 313 | return _hook_typing() 314 | ns, _, rest = name.partition(".") 315 | ig = {"__main__", "cli", "engine"} 316 | if ns == "ingraph": 317 | if not ( 318 | (not rest and not (set(fromlist or set()) & ig)) 319 | or (rest and rest.partition(".")[0] not in ig) 320 | ): 321 | raise ImportError("Unexpected import") 322 | else: 323 | spec = sys.modules[name].__spec__ 324 | if spec is None or not isinstance(spec.loader, (ExternalImporter,)): 325 | raise ModuleNotFoundError(f"No module named {ns!r}") 326 | return __import__(name, globals_, locals_, fromlist, level) # type: ignore 327 | -------------------------------------------------------------------------------- /src/ingraph/engine/parser.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Farzad Senart and Lionel Suss. All rights reserved. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | import ast 17 | import importlib.resources 18 | import inspect 19 | from functools import reduce 20 | from types import FunctionType, ModuleType 21 | from typing import (Any, Dict, List, NamedTuple, NoReturn, Optional, Sequence, 22 | Tuple, Type, Union) 23 | 24 | import parso 25 | 26 | from . import core 27 | 28 | TRANSFORMERS: List[Type["Transformer"]] 29 | 30 | 31 | def process(source: str, filename: str, module: ModuleType) -> None: 32 | tree = parse(source, filename) 33 | code = compile(tree, filename, "exec") 34 | builtins = getattr(module, "__builtins__", {}) 35 | builtins.update( 36 | __build_class__=__build_class__, # type: ignore 37 | str=str, 38 | int=int, 39 | float=float, 40 | **{_n(t): t for t in [NameError]}, 41 | **{ 42 | _n(t): t 43 | for t in [ 44 | core.Boolean, 45 | core.Number, 46 | core.String, 47 | core.Map, 48 | core.new_list, 49 | core.CustomResource, 50 | core.ExternalResource, 51 | ] 52 | }, 53 | **{ 54 | _n(v): v 55 | for t in TRANSFORMERS 56 | for (k, v) in inspect.getmembers(t, predicate=inspect.isfunction) 57 | if k.startswith("proxy_") 58 | }, 59 | ) 60 | setattr(module, "__builtins__", builtins) 61 | exec(code, vars(module)) 62 | 63 | 64 | def parse(source: str, filename: str) -> ast.Module: 65 | with importlib.resources.path(__package__, "Grammar") as path: 66 | grammar = parso.load_grammar(path=path) 67 | module = grammar.parse(source) 68 | errs = grammar.iter_errors(module) 69 | if errs: 70 | msg = errs[0].message.replace("SyntaxError: ", "") 71 | raise SyntaxError(msg, (filename, *errs[0].start_pos, None)) 72 | return reduce( 73 | lambda tree, trans: ast.fix_missing_locations( 74 | trans(source, filename).visit(_setparents(tree)) 75 | ), 76 | TRANSFORMERS, 77 | ast.parse(source, filename), 78 | ) 79 | 80 | 81 | def _setparent(node: ast.AST, parent: Optional[ast.AST]) -> ast.AST: 82 | setattr(node, "_ig_parent", parent) 83 | return node 84 | 85 | 86 | def _getparent(node: ast.AST) -> Optional[ast.AST]: 87 | return getattr(node, "_ig_parent", None) 88 | 89 | 90 | def _setparents(node: ast.AST) -> ast.AST: 91 | for parent in ast.walk(node): 92 | for child in ast.iter_child_nodes(parent): 93 | _setparent(child, parent) 94 | return node 95 | 96 | 97 | def _n(fn: Any) -> str: 98 | return f"_ig_{fn.__qualname__.replace('.', '_')}" 99 | 100 | 101 | class Location(NamedTuple): 102 | filename: ast.Constant 103 | lines: ast.Tuple 104 | columns: ast.Tuple 105 | source: ast.Constant 106 | 107 | 108 | class Transformer(ast.NodeTransformer): 109 | def __init__(self, source: str, filename: str): 110 | self._source = source 111 | self._filename = filename 112 | 113 | def location(self, node: ast.AST) -> Location: 114 | filename = ast.Constant(value=self._filename) 115 | lstart = ast.Constant(value=getattr(node, "lineno", 0)) 116 | lend = ast.Constant(value=getattr(node, "end_lineno", 0)) 117 | lines = ast.Tuple(elts=[lstart, lend], ctx=ast.Load()) 118 | cstart = ast.Constant(value=getattr(node, "col_offset", 0)) 119 | cend = ast.Constant(value=getattr(node, "end_col_offset", 0)) 120 | columns = ast.Tuple(elts=[cstart, cend], ctx=ast.Load()) 121 | source = ast.Constant(value=ast.get_source_segment(self._source, node)) 122 | return Location(filename, lines, columns, source) 123 | 124 | def error( 125 | self, node: ast.AST, msg: str = "invalid syntax" 126 | ) -> SyntaxError: # pragma: no cover 127 | parent = _getparent(node) 128 | line = getattr(node, "lineno", getattr(parent, "lineno", 1)) 129 | col = getattr(node, "col_offset", getattr(parent, "col_offset", 0)) 130 | src = ast.get_source_segment(self._source, node) 131 | if not src and parent is not None: 132 | src = ast.get_source_segment(self._source, parent) 133 | return SyntaxError(msg, (self._filename, line, col, src)) 134 | 135 | 136 | class PrivateChecker(Transformer): 137 | 138 | message = "names cannot start with an underscore" 139 | 140 | def visit_Attribute(self, node: ast.Attribute) -> ast.Attribute: 141 | self.generic_visit(node) 142 | if node.attr.startswith("_"): 143 | raise self.error(node, self.message) 144 | return node 145 | 146 | def visit_Call(self, node: ast.Call) -> ast.Call: 147 | self.generic_visit(node) 148 | for arg in node.keywords: 149 | if arg.arg and arg.arg.startswith("_"): 150 | raise self.error(arg, self.message) 151 | return node 152 | 153 | def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: 154 | self.generic_visit(node) 155 | if node.name.startswith("_"): 156 | raise self.error(node, self.message) 157 | return node 158 | 159 | def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: 160 | self.generic_visit(node) 161 | for arg in node.args.args: 162 | if arg.arg.startswith("_"): 163 | raise self.error(arg, self.message) 164 | return node 165 | 166 | def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.ImportFrom: 167 | self.generic_visit(node) 168 | for name in node.names: 169 | aliases = [name.name, name.asname if name.asname is not None else ""] 170 | if any(a.startswith("_") for a in aliases): 171 | raise self.error(node, self.message) 172 | return node 173 | 174 | def visit_Name(self, node: ast.Name) -> ast.Name: 175 | self.generic_visit(node) 176 | if node.id.startswith("_"): 177 | raise self.error(node, self.message) 178 | return node 179 | 180 | 181 | class SignatureModifier(Transformer): 182 | def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: 183 | self.generic_visit(node) 184 | node.args.kwonlyargs = node.args.args[1:] 185 | node.args.args = node.args.args[0:1] 186 | count = len(node.args.kwonlyargs) - len(node.args.defaults) 187 | node.args.kw_defaults = count * [None] + node.args.defaults # type: ignore 188 | node.args.defaults = [] 189 | return node 190 | 191 | 192 | class ImmutabilityGuard(Transformer): 193 | def visit_Assign(self, node: ast.Assign) -> Any: 194 | self.generic_visit(node) 195 | target = node.targets[0] 196 | if not isinstance(target, ast.Name): 197 | return node 198 | expr = ast.Name(id=target.id, ctx=ast.Load()) 199 | name = target.id 200 | check = ast.Try( 201 | body=[ast.Expr(expr)], 202 | handlers=[ 203 | ast.ExceptHandler( 204 | type=ast.Tuple( 205 | elts=[ast.Name(id=_n(NameError), ctx=ast.Load())], 206 | ctx=ast.Load(), 207 | ), 208 | name=None, 209 | body=[ast.Expr(value=ast.Constant(value=Ellipsis))], 210 | ) 211 | ], 212 | orelse=[ 213 | ast.Raise( 214 | exc=ast.Call( 215 | func=ast.Name( 216 | id=_n(ImmutabilityGuard.proxy_Assign), ctx=ast.Load() 217 | ), 218 | args=[ast.Constant(value=name)], 219 | keywords=[], 220 | ), 221 | cause=None, 222 | ) 223 | ], 224 | finalbody=[], 225 | ) 226 | return [check, node] 227 | 228 | @staticmethod 229 | def proxy_Assign(target: str) -> NoReturn: 230 | raise TypeError(f"{target!r} is read-only") 231 | 232 | 233 | class DefinitionWrapper(Transformer): 234 | def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: 235 | self.generic_visit(node) 236 | if node.bases: 237 | node.bases = [ 238 | ast.Name(id=_n(core.CustomResource), ctx=ast.Load()), 239 | *node.bases, 240 | ] 241 | else: 242 | node.bases = [ast.Name(id=_n(core.ExternalResource), ctx=ast.Load())] 243 | return node 244 | 245 | 246 | class TypeWrapper(Transformer): 247 | def visit_Constant(self, node: ast.Constant) -> Union[ast.Constant, ast.Call]: 248 | self.generic_visit(node) 249 | parent = _getparent(node) 250 | value = node.value 251 | if value is Ellipsis or value is None: 252 | return node 253 | if isinstance(value, bool): 254 | proxy = _n(core.Boolean) 255 | elif isinstance(value, (int, float)): 256 | proxy = _n(core.Number) 257 | elif isinstance(value, str): 258 | proxy = _n(core.String) 259 | else: 260 | t = type(value) 261 | raise self.error(node, f"unsupported primitive type {t.__name__!r}") 262 | return ast.Call( 263 | func=ast.Name(id=proxy, ctx=ast.Load()), args=[node], keywords=[] 264 | ) 265 | 266 | def visit_Dict(self, node: ast.Dict) -> ast.Call: 267 | self.generic_visit(node) 268 | return ast.Call( 269 | func=ast.Name(id=_n(core.Map), ctx=ast.Load()), args=[node], keywords=[], 270 | ) 271 | 272 | def visit_List(self, node: ast.List) -> ast.Call: 273 | self.generic_visit(node) 274 | return ast.Call( 275 | func=ast.Name(id=_n(core.new_list), ctx=ast.Load()), 276 | args=[node], 277 | keywords=[], 278 | ) 279 | 280 | def visit_UnaryOp(self, node: ast.UnaryOp) -> Any: 281 | if ( 282 | isinstance(node.op, ast.USub) 283 | and isinstance(node.operand, ast.Constant) 284 | and isinstance(node.operand.value, (int, float)) 285 | ): 286 | res = ast.Constant(value=ast.literal_eval(node)) 287 | _setparent(res, _getparent(node)) 288 | return self.visit_Constant(res) 289 | else: # pragma: no cover 290 | self.generic_visit(node) 291 | return node 292 | 293 | 294 | class LocationEnricher(Transformer): 295 | def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: 296 | self.generic_visit(node) 297 | loc = self.location(node) 298 | node.body[0:0] = [ 299 | ast.Assign( 300 | targets=[ast.Name(id=f"_ig_{attr}", ctx=ast.Store())], 301 | value=getattr(loc, attr), 302 | ) 303 | for attr in ("filename", "lines", "columns", "source") 304 | ] 305 | return node 306 | 307 | 308 | class CallWrapper(Transformer): 309 | def visit_Call(self, node: ast.Call) -> ast.Call: 310 | self.generic_visit(node) 311 | parent = _getparent(node) 312 | assert parent is not None 313 | if isinstance(parent, ast.Assign) and isinstance(parent.targets[0], ast.Name): 314 | name = ast.Constant(value=parent.targets[0].id) 315 | else: 316 | name = ast.Constant(value=None) 317 | definition = node.func 318 | filename, lines, columns, source = self.location(parent) 319 | return ast.Call( 320 | func=ast.Name(id=_n(CallWrapper.proxy_Call), ctx=ast.Load()), 321 | args=node.args, 322 | keywords=node.keywords 323 | + [ 324 | ast.keyword(arg="_ig_name", value=name), 325 | ast.keyword(arg="_ig_definition", value=definition), 326 | ast.keyword(arg="_ig_filename", value=filename), 327 | ast.keyword(arg="_ig_lines", value=lines), 328 | ast.keyword(arg="_ig_columns", value=columns), 329 | ast.keyword(arg="_ig_source", value=source), 330 | ], 331 | ) 332 | 333 | @staticmethod 334 | def proxy_Call( 335 | *args: Any, 336 | _ig_name: Optional[str], 337 | _ig_definition: Union[FunctionType, Type], 338 | _ig_filename: str, 339 | _ig_lines: Tuple[int, int], 340 | _ig_columns: Tuple[int, int], 341 | _ig_source: str, 342 | **kwargs: Any, 343 | ) -> Any: 344 | if _ig_definition in {int, float, str}: 345 | raise TypeError("invalid call") 346 | if not ( 347 | isinstance(_ig_definition, type) 348 | and issubclass( 349 | _ig_definition, 350 | ( 351 | core.NativeResource, 352 | core.CustomResource, 353 | core.ExternalResource, 354 | core.Asset, 355 | ), 356 | ) 357 | ): 358 | return _ig_definition(*args, **kwargs) 359 | if _ig_name is None: 360 | raise TypeError("invalid resource instantiation") 361 | if issubclass(_ig_definition, core.Asset): 362 | return _ig_definition(*args, **kwargs) 363 | loc = core.Location( 364 | filename=_ig_filename, 365 | lines=_ig_lines, 366 | columns=_ig_columns, 367 | source=_ig_source, 368 | ) 369 | with core.new_node(_ig_name, loc) as node: 370 | return _ig_definition(*args, _ig_node=node, **kwargs) # type: ignore 371 | 372 | 373 | class FStringWrapper(Transformer): 374 | def visit_FormattedValue(self, node: ast.FormattedValue) -> ast.Call: 375 | self.generic_visit(node) 376 | spec = node.format_spec 377 | return ast.Call( 378 | func=ast.Name(id=_n(self.proxy_FormattedValue), ctx=ast.Load()), 379 | args=[ 380 | node.value, 381 | ast.Constant(value=node.conversion), 382 | spec if spec is not None else ast.Constant(value=""), 383 | ], 384 | keywords=[], 385 | ) 386 | 387 | @staticmethod 388 | def proxy_FormattedValue(value: Any, conv: int, spec: Union[str, core.Sub]) -> Any: 389 | value = core.Formatter.ig_convert(value, chr(conv) if conv > -1 else None) 390 | value = core.Formatter.ig_format(value, spec) 391 | return value 392 | 393 | def visit_JoinedStr(self, node: ast.JoinedStr) -> ast.Call: 394 | self.generic_visit(node) 395 | return ast.Call( 396 | func=ast.Name(id=_n(self.proxy_JoinedStr), ctx=ast.Load()), 397 | args=[ast.List(elts=node.values, ctx=ast.Load())], 398 | keywords=[], 399 | ) 400 | 401 | @staticmethod 402 | def proxy_JoinedStr(values: Sequence[Any]) -> core.Sub: 403 | fmts = "" 404 | kwargs: Dict[core.String, Any] = {} 405 | for v in values: 406 | fmt, kwarg = core.Formatter.ig_map(v, kwargs) 407 | fmts += fmt 408 | kwargs.update(kwarg) 409 | return core.Sub(core.String(fmts), core.Map(kwargs)) 410 | 411 | 412 | TRANSFORMERS = [ 413 | PrivateChecker, 414 | SignatureModifier, 415 | ImmutabilityGuard, 416 | DefinitionWrapper, 417 | TypeWrapper, 418 | LocationEnricher, 419 | CallWrapper, 420 | FStringWrapper, 421 | ] 422 | -------------------------------------------------------------------------------- /src/ingraph/engine/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/src/ingraph/engine/py.typed -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/tests/__init__.py -------------------------------------------------------------------------------- /tests/e2e/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/tests/e2e/__init__.py -------------------------------------------------------------------------------- /tests/e2e/fixture/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/tests/e2e/fixture/__init__.py -------------------------------------------------------------------------------- /tests/e2e/fixture/complete_file/handler.js: -------------------------------------------------------------------------------- 1 | exports.handle = async () => "Hello, World!"; 2 | -------------------------------------------------------------------------------- /tests/e2e/fixture/complete_file/input.py: -------------------------------------------------------------------------------- 1 | from ingraph.aws import Asset, aws_iam, aws_lambda 2 | 3 | 4 | class Entrypoint: 5 | """An Entrypoint documentation""" 6 | 7 | asset_uri: str 8 | asset_url: str 9 | 10 | def __init__(self) -> None: 11 | role = aws_iam.Role( 12 | AssumeRolePolicyDocument={ 13 | "Version": "2012-10-17", 14 | "Statement": { 15 | "Effect": "Allow", 16 | "Principal": {"Service": "lambda.amazonaws.com"}, 17 | "Action": "sts:AssumeRole", 18 | }, 19 | }, 20 | ManagedPolicyArns=[ 21 | "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 22 | ], 23 | Policies=[ 24 | aws_iam.Role.Policy( 25 | PolicyName="acm", 26 | PolicyDocument={ 27 | "Effect": "Allow", 28 | "Action": [ 29 | "acm:DescribeCertificate", 30 | "acm:CreateCertificate", 31 | "acm:DeleteCertificate", 32 | "acm:RequestCertificate", 33 | ], 34 | "Resource": "*", 35 | }, 36 | ), 37 | aws_iam.Role.Policy( 38 | PolicyName="r53", 39 | PolicyDocument={ 40 | "Effect": "Allow", 41 | "Action": ["route53:ChangeResourceRecordSets"], 42 | "Resource": "*", 43 | }, 44 | ), 45 | ], 46 | ) 47 | handler = Asset(name="handler.js") 48 | func_plain = aws_lambda.Function( 49 | Code=aws_lambda.Function.Code(ZipFile=handler.text), 50 | Handler="index.handle", 51 | Role=role.Arn, 52 | Runtime="nodejs12.x", 53 | ) 54 | archive = Asset(name="handler.js", compress=True) 55 | func_zip = aws_lambda.Function( 56 | Code=aws_lambda.Function.Code(S3Bucket=archive.bucket, S3Key=archive.key), 57 | Handler="handler.handle", 58 | Role=role.Arn, 59 | Runtime="nodejs12.x", 60 | DependsOn=[func_plain, role], 61 | ) 62 | self.asset_uri = archive.uri 63 | same_archive = Asset(name="handler.js", compress=True) 64 | self.asset_url = same_archive.url 65 | -------------------------------------------------------------------------------- /tests/e2e/fixture/complete_file/output.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Parameters: 3 | AssetsS3Bucket: 4 | Type: String 5 | AssetsS3Prefix: 6 | Type: String 7 | Resources: 8 | RoleMW2CSPSW: 9 | Type: AWS::IAM::Role 10 | Properties: 11 | AssumeRolePolicyDocument: 12 | Version: '2012-10-17' 13 | Statement: 14 | Effect: Allow 15 | Principal: 16 | Service: lambda.amazonaws.com 17 | Action: sts:AssumeRole 18 | ManagedPolicyArns: 19 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 20 | Policies: 21 | - PolicyDocument: 22 | Effect: Allow 23 | Action: 24 | - acm:DescribeCertificate 25 | - acm:CreateCertificate 26 | - acm:DeleteCertificate 27 | - acm:RequestCertificate 28 | Resource: '*' 29 | PolicyName: acm 30 | - PolicyDocument: 31 | Effect: Allow 32 | Action: 33 | - route53:ChangeResourceRecordSets 34 | Resource: '*' 35 | PolicyName: r53 36 | FuncPlainAX4C4WBG: 37 | Type: AWS::Lambda::Function 38 | Properties: 39 | Code: 40 | ZipFile: | 41 | exports.handle = async () => "Hello, World!"; 42 | Handler: index.handle 43 | Role: !GetAtt RoleMW2CSPSW.Arn 44 | Runtime: nodejs12.x 45 | FuncZipCYI6F352: 46 | Type: AWS::Lambda::Function 47 | Properties: 48 | Code: 49 | S3Bucket: !Ref 'AssetsS3Bucket' 50 | S3Key: !Sub ${AssetsS3Prefix}64b9df3b7f85f000aca4b3297957613a5629e6ab 51 | Handler: handler.handle 52 | Role: !GetAtt RoleMW2CSPSW.Arn 53 | Runtime: nodejs12.x 54 | DependsOn: 55 | - FuncPlainAX4C4WBG 56 | - RoleMW2CSPSW 57 | Outputs: 58 | AssetUri: 59 | Value: !Sub s3://${AssetsS3Bucket}/${AssetsS3Prefix}64b9df3b7f85f000aca4b3297957613a5629e6ab 60 | AssetUrl: 61 | Value: !Sub https://${AssetsS3Bucket}.s3.amazonaws.com/${AssetsS3Prefix}64b9df3b7f85f000aca4b3297957613a5629e6ab 62 | -------------------------------------------------------------------------------- /tests/e2e/fixture/complete_module/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/tests/e2e/fixture/complete_module/__init__.py -------------------------------------------------------------------------------- /tests/e2e/fixture/complete_module/handler.js: -------------------------------------------------------------------------------- 1 | exports.handle = async () => "Hello, World!"; 2 | -------------------------------------------------------------------------------- /tests/e2e/fixture/complete_module/input.py: -------------------------------------------------------------------------------- 1 | from ingraph.aws import Asset, aws_iam, aws_lambda 2 | 3 | from complete_module.sidecar import assets, input 4 | 5 | from . import submodule 6 | 7 | 8 | class Entrypoint: 9 | asset_uri: str 10 | asset_url: str 11 | 12 | def __init__(self) -> None: 13 | sub = submodule.Entrypoint() 14 | handler = Asset(name="handler.js") 15 | func_plain = aws_lambda.Function( 16 | Code=aws_lambda.Function.Code(ZipFile=handler.text), 17 | Handler="index.handle", 18 | Role=sub.role_arn, 19 | Runtime="nodejs12.x", 20 | ) 21 | archive = Asset(name="handler.js", compress=True) 22 | func_zip = aws_lambda.Function( 23 | Code=aws_lambda.Function.Code(S3Bucket=archive.bucket, S3Key=archive.key), 24 | Handler="handler.handle", 25 | Role=sub.role_arn, 26 | Runtime="nodejs12.x", 27 | ) 28 | self.asset_uri = archive.uri 29 | same_archive = Asset(name="handler.js", compress=True) 30 | self.asset_url = same_archive.url 31 | side = input.Entrypoint() 32 | asset_side = Asset(name="handler.js", package=assets) 33 | func_side = aws_lambda.Function( 34 | Code=aws_lambda.Function.Code( 35 | ZipFile=asset_side.text.replace("sidecar", "inside") 36 | ), 37 | Handler="index.handle", 38 | Role=sub.role_arn, 39 | Runtime="nodejs12.x", 40 | ) 41 | -------------------------------------------------------------------------------- /tests/e2e/fixture/complete_module/output.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Parameters: 3 | AssetsS3Bucket: 4 | Type: String 5 | AssetsS3Prefix: 6 | Type: String 7 | Resources: 8 | SubRole45U335Z4: 9 | Type: AWS::IAM::Role 10 | Properties: 11 | AssumeRolePolicyDocument: 12 | Version: '2012-10-17' 13 | Statement: 14 | Effect: Allow 15 | Principal: 16 | Service: lambda.amazonaws.com 17 | Action: sts:AssumeRole 18 | ManagedPolicyArns: 19 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 20 | FuncPlainAX4C4WBG: 21 | Type: AWS::Lambda::Function 22 | Properties: 23 | Code: 24 | ZipFile: | 25 | exports.handle = async () => "Hello, World!"; 26 | Handler: index.handle 27 | Role: !GetAtt SubRole45U335Z4.Arn 28 | Runtime: nodejs12.x 29 | FuncZipCYI6F352: 30 | Type: AWS::Lambda::Function 31 | Properties: 32 | Code: 33 | S3Bucket: !Ref 'AssetsS3Bucket' 34 | S3Key: !Sub ${AssetsS3Prefix}64b9df3b7f85f000aca4b3297957613a5629e6ab 35 | Handler: handler.handle 36 | Role: !GetAtt SubRole45U335Z4.Arn 37 | Runtime: nodejs12.x 38 | SideRoleR6IRXJPT: 39 | Type: AWS::IAM::Role 40 | Properties: 41 | AssumeRolePolicyDocument: 42 | Version: '2012-10-17' 43 | Statement: 44 | Effect: Allow 45 | Principal: 46 | Service: lambda.amazonaws.com 47 | Action: sts:AssumeRole 48 | ManagedPolicyArns: 49 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 50 | SideFuncPlainR7HTZ65U: 51 | Type: AWS::Lambda::Function 52 | Properties: 53 | Code: 54 | ZipFile: | 55 | exports.handle = async () => "Hello, World! (from sidecar)"; 56 | Handler: index.handle 57 | Role: !GetAtt SideRoleR6IRXJPT.Arn 58 | Runtime: nodejs12.x 59 | SideFuncZipASOCLY3E: 60 | Type: AWS::Lambda::Function 61 | Properties: 62 | Code: 63 | S3Bucket: !Ref 'AssetsS3Bucket' 64 | S3Key: !Sub ${AssetsS3Prefix}66c3b02bfc8b38831fcec495df73b87f1cc28965 65 | Handler: handler.handle 66 | Role: !GetAtt SideRoleR6IRXJPT.Arn 67 | Runtime: nodejs12.x 68 | DependsOn: 69 | - SideFuncPlainR7HTZ65U 70 | - SideRoleR6IRXJPT 71 | FuncSide7XAPBEE5: 72 | Type: AWS::Lambda::Function 73 | Properties: 74 | Code: 75 | ZipFile: !Join 76 | - inside 77 | - !Split 78 | - sidecar 79 | - | 80 | exports.handle = async () => "Hello, World! (from sidecar)"; 81 | Handler: index.handle 82 | Role: !GetAtt SubRole45U335Z4.Arn 83 | Runtime: nodejs12.x 84 | Outputs: 85 | AssetUri: 86 | Value: !Sub s3://${AssetsS3Bucket}/${AssetsS3Prefix}64b9df3b7f85f000aca4b3297957613a5629e6ab 87 | AssetUrl: 88 | Value: !Sub https://${AssetsS3Bucket}.s3.amazonaws.com/${AssetsS3Prefix}64b9df3b7f85f000aca4b3297957613a5629e6ab 89 | -------------------------------------------------------------------------------- /tests/e2e/fixture/complete_module/sidecar/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/tests/e2e/fixture/complete_module/sidecar/__init__.py -------------------------------------------------------------------------------- /tests/e2e/fixture/complete_module/sidecar/assets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/tests/e2e/fixture/complete_module/sidecar/assets/__init__.py -------------------------------------------------------------------------------- /tests/e2e/fixture/complete_module/sidecar/assets/handler.js: -------------------------------------------------------------------------------- 1 | exports.handle = async () => "Hello, World! (from sidecar)"; 2 | -------------------------------------------------------------------------------- /tests/e2e/fixture/complete_module/sidecar/input.py: -------------------------------------------------------------------------------- 1 | from ingraph.aws import Asset, aws_iam, aws_lambda 2 | 3 | from . import assets 4 | 5 | 6 | class Entrypoint: 7 | asset_uri: str 8 | asset_url: str 9 | 10 | def __init__(self) -> None: 11 | role = aws_iam.Role( 12 | AssumeRolePolicyDocument={ 13 | "Version": "2012-10-17", 14 | "Statement": { 15 | "Effect": "Allow", 16 | "Principal": {"Service": "lambda.amazonaws.com"}, 17 | "Action": "sts:AssumeRole", 18 | }, 19 | }, 20 | ManagedPolicyArns=[ 21 | "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 22 | ], 23 | ) 24 | handler = Asset(name="handler.js", package=assets) 25 | func_plain = aws_lambda.Function( 26 | Code=aws_lambda.Function.Code(ZipFile=handler.text), 27 | Handler="index.handle", 28 | Role=role.Arn, 29 | Runtime="nodejs12.x", 30 | ) 31 | archive = Asset(name="handler.js", package=assets, compress=True) 32 | func_zip = aws_lambda.Function( 33 | Code=aws_lambda.Function.Code(S3Bucket=archive.bucket, S3Key=archive.key), 34 | Handler="handler.handle", 35 | Role=role.Arn, 36 | Runtime="nodejs12.x", 37 | DependsOn=[func_plain, role], 38 | ) 39 | self.asset_uri = archive.uri 40 | same_archive = Asset(name="handler.js", package=assets, compress=True) 41 | self.asset_url = same_archive.url 42 | -------------------------------------------------------------------------------- /tests/e2e/fixture/complete_module/submodule.py: -------------------------------------------------------------------------------- 1 | from ingraph.aws import aws_iam 2 | 3 | 4 | class Entrypoint: 5 | """A Multiline 6 | Entrypoint documentation""" 7 | 8 | role_arn: str 9 | 10 | def __init__(self) -> None: 11 | role = aws_iam.Role( 12 | AssumeRolePolicyDocument={ 13 | "Version": "2012-10-17", 14 | "Statement": { 15 | "Effect": "Allow", 16 | "Principal": {"Service": "lambda.amazonaws.com"}, 17 | "Action": "sts:AssumeRole", 18 | }, 19 | }, 20 | ManagedPolicyArns=[ 21 | "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 22 | ], 23 | ) 24 | self.role_arn = role.Arn 25 | -------------------------------------------------------------------------------- /tests/e2e/fixture/intrinsic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/tests/e2e/fixture/intrinsic/__init__.py -------------------------------------------------------------------------------- /tests/e2e/fixture/intrinsic/input.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ingraph import aws 4 | from ingraph.aws import aws_cloudformation, aws_iam 5 | 6 | 7 | class Entrypoint: 8 | attr: str 9 | join0: str 10 | join1: str 11 | join2: str 12 | join3: str 13 | join4: str 14 | ref: str 15 | select_int: int 16 | select_str: str 17 | split: str 18 | sub0: str 19 | sub1: str 20 | sub2: str 21 | base64: str 22 | cidr: str 23 | static: str 24 | 25 | def __init__(self, ps: str, pli: List[int], pls: List[str]) -> None: 26 | wch = aws_cloudformation.WaitConditionHandle() 27 | grp = aws_iam.Group() 28 | 29 | index = 0 30 | self.attr = grp.Arn 31 | self.join0 = wch.Ref + grp.Arn + wch.Ref 32 | self.join1 = wch.Ref + grp.Arn + "foo" 33 | self.join2 = "-".join([wch.Ref, grp.Arn]) 34 | self.join3 = grp.Arn.replace(":", "-") 35 | self.join4 = ",".join(pls) + wch.Ref 36 | self.ref = wch.Ref 37 | self.select_int = pli[0] 38 | self.select_str = aws.AVAILABILITY_ZONES[index] 39 | self.split = grp.Arn.split(":")[0] 40 | self.sub0 = f"{wch.Ref} {grp.Arn}" 41 | self.sub1 = f"{wch.Ref + grp.Arn}" 42 | self.sub2 = "{} {} {}".format(wch.Ref, grp.Arn, ps) 43 | self.base64 = aws.b64encode("foo") 44 | self.cidr = aws.cidr(block=ps, count=4, bits=2)[0] 45 | self.static = ["foo", "bar"][1] 46 | -------------------------------------------------------------------------------- /tests/e2e/fixture/intrinsic/output.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Parameters: 3 | Ps: 4 | Type: String 5 | Pli: 6 | Type: List 7 | Pls: 8 | Type: CommaDelimitedList 9 | Resources: 10 | WchQVIJ3LOW: 11 | Type: AWS::CloudFormation::WaitConditionHandle 12 | GrpJNMTGX2J: 13 | Type: AWS::IAM::Group 14 | Outputs: 15 | Attr: 16 | Value: !GetAtt GrpJNMTGX2J.Arn 17 | Join0: 18 | Value: !Join 19 | - '' 20 | - - !Join 21 | - '' 22 | - - !Ref WchQVIJ3LOW 23 | - !GetAtt GrpJNMTGX2J.Arn 24 | - !Ref WchQVIJ3LOW 25 | Join1: 26 | Value: !Join 27 | - '' 28 | - - !Join 29 | - '' 30 | - - !Ref WchQVIJ3LOW 31 | - !GetAtt GrpJNMTGX2J.Arn 32 | - foo 33 | Join2: 34 | Value: !Join 35 | - '-' 36 | - - !Ref WchQVIJ3LOW 37 | - !GetAtt GrpJNMTGX2J.Arn 38 | Join3: 39 | Value: !Join 40 | - '-' 41 | - !Split 42 | - ':' 43 | - !GetAtt GrpJNMTGX2J.Arn 44 | Join4: 45 | Value: !Join 46 | - '' 47 | - - !Join 48 | - ',' 49 | - !Ref Pls 50 | - !Ref WchQVIJ3LOW 51 | Ref: 52 | Value: !Ref WchQVIJ3LOW 53 | SelectInt: 54 | Value: !Select 55 | - 0 56 | - !Ref Pli 57 | SelectStr: 58 | Value: !Select 59 | - 0 60 | - Fn::GetAZs: '' 61 | Split: 62 | Value: !Select 63 | - 0 64 | - !Split 65 | - ':' 66 | - !GetAtt GrpJNMTGX2J.Arn 67 | Sub0: 68 | Value: !Sub ${WchQVIJ3LOW} ${GrpJNMTGX2J.Arn} 69 | Sub1: 70 | Value: !Sub 71 | - ${A0} 72 | - A0: !Join 73 | - '' 74 | - - !Ref WchQVIJ3LOW 75 | - !GetAtt GrpJNMTGX2J.Arn 76 | Sub2: 77 | Value: !Sub ${WchQVIJ3LOW} ${GrpJNMTGX2J.Arn} ${Ps} 78 | Base64: 79 | Value: 80 | Fn::Base64: foo 81 | Cidr: 82 | Value: !Select 83 | - 0 84 | - !Cidr 85 | - !Ref Ps 86 | - 4 87 | - 2 88 | Static: 89 | Value: bar 90 | -------------------------------------------------------------------------------- /tests/e2e/fixture/output/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/tests/e2e/fixture/output/__init__.py -------------------------------------------------------------------------------- /tests/e2e/fixture/output/input.py: -------------------------------------------------------------------------------- 1 | from ingraph.aws import aws_cloudformation 2 | 3 | 4 | class Entrypoint: 5 | oi: int 6 | os: str 7 | 8 | def __init__(self) -> None: 9 | wch = aws_cloudformation.WaitConditionHandle() 10 | self.oi = -42 11 | self.os = "foo" 12 | -------------------------------------------------------------------------------- /tests/e2e/fixture/output/output.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Resources: 3 | WchQVIJ3LOW: 4 | Type: AWS::CloudFormation::WaitConditionHandle 5 | Outputs: 6 | Oi: 7 | Value: -42 8 | Os: 9 | Value: foo 10 | -------------------------------------------------------------------------------- /tests/e2e/fixture/param/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/tests/e2e/fixture/param/__init__.py -------------------------------------------------------------------------------- /tests/e2e/fixture/param/input.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ingraph.aws import aws_cloudformation 4 | 5 | 6 | class Entrypoint: 7 | def __init__( 8 | self, 9 | i: int, 10 | s: str, 11 | li: List[int], 12 | ls: List[str], 13 | id: int = 42, 14 | sd: str = "foo", 15 | lid: List[int] = [4, 2], 16 | lsd: List[str] = ["f", "o", "o"], 17 | ): 18 | wch = aws_cloudformation.WaitConditionHandle() 19 | -------------------------------------------------------------------------------- /tests/e2e/fixture/param/output.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Parameters: 3 | I: 4 | Type: Number 5 | S: 6 | Type: String 7 | Li: 8 | Type: List 9 | Ls: 10 | Type: CommaDelimitedList 11 | Id: 12 | Type: Number 13 | Default: 42 14 | Sd: 15 | Type: String 16 | Default: foo 17 | Lid: 18 | Type: List 19 | Default: 4,2 20 | Lsd: 21 | Type: CommaDelimitedList 22 | Default: f,o,o 23 | Resources: 24 | WchQVIJ3LOW: 25 | Type: AWS::CloudFormation::WaitConditionHandle 26 | -------------------------------------------------------------------------------- /tests/e2e/fixture/pseudo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/tests/e2e/fixture/pseudo/__init__.py -------------------------------------------------------------------------------- /tests/e2e/fixture/pseudo/input.py: -------------------------------------------------------------------------------- 1 | from ingraph import aws 2 | from ingraph.aws import aws_cloudformation 3 | 4 | 5 | class Entrypoint: 6 | account_id: str 7 | availability_zone: str 8 | notification_arn: str 9 | partition: str 10 | region: str 11 | stack_id: str 12 | stack_name: str 13 | url_suffix: str 14 | 15 | def __init__(self) -> None: 16 | wch = aws_cloudformation.WaitConditionHandle() 17 | self.account_id = aws.ACCOUNT_ID 18 | self.availability_zone = aws.AVAILABILITY_ZONES[0] 19 | self.notification_arn = aws.NOTIFICATION_ARNS[0] 20 | self.partition = aws.PARTITION 21 | self.region = aws.REGION 22 | self.stack_id = aws.STACK_ID 23 | self.stack_name = aws.STACK_NAME 24 | self.url_suffix = aws.URL_SUFFIX 25 | -------------------------------------------------------------------------------- /tests/e2e/fixture/pseudo/output.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Resources: 3 | WchQVIJ3LOW: 4 | Type: AWS::CloudFormation::WaitConditionHandle 5 | Outputs: 6 | AccountId: 7 | Value: !Ref 'AWS::AccountId' 8 | AvailabilityZone: 9 | Value: !Select 10 | - 0 11 | - Fn::GetAZs: '' 12 | NotificationArn: 13 | Value: !Select 14 | - 0 15 | - !Ref 'AWS::NotificationARNs' 16 | Partition: 17 | Value: !Ref 'AWS::Partition' 18 | Region: 19 | Value: !Ref 'AWS::Region' 20 | StackId: 21 | Value: !Ref 'AWS::StackId' 22 | StackName: 23 | Value: !Ref 'AWS::StackName' 24 | UrlSuffix: 25 | Value: !Ref 'AWS::URLSuffix' 26 | -------------------------------------------------------------------------------- /tests/e2e/fixture/simple/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/tests/e2e/fixture/simple/__init__.py -------------------------------------------------------------------------------- /tests/e2e/fixture/simple/input.py: -------------------------------------------------------------------------------- 1 | from ingraph.aws import aws_cloudformation 2 | 3 | 4 | class Entrypoint: 5 | def __init__(self) -> None: 6 | wch = aws_cloudformation.WaitConditionHandle() 7 | -------------------------------------------------------------------------------- /tests/e2e/fixture/simple/output.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Resources: 3 | WchQVIJ3LOW: 4 | Type: AWS::CloudFormation::WaitConditionHandle 5 | -------------------------------------------------------------------------------- /tests/e2e/fixture/tree/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/tests/e2e/fixture/tree/__init__.py -------------------------------------------------------------------------------- /tests/e2e/fixture/tree/input.py: -------------------------------------------------------------------------------- 1 | from ingraph.aws import aws_cloudformation, aws_ec2, aws_iam 2 | 3 | 4 | class Entrypoint: 5 | def __init__(self) -> None: 6 | foo = Foo() 7 | bar = Bar() 8 | 9 | 10 | class Foo: 11 | def __init__(self) -> None: 12 | wch = aws_cloudformation.WaitConditionHandle() 13 | 14 | 15 | class Bar: 16 | def __init__(self) -> None: 17 | baz = Baz() 18 | qux = Qux() 19 | 20 | 21 | class Baz: 22 | def __init__(self) -> None: 23 | neti = aws_ec2.NetworkInterface(SubnetId="subnet_id") 24 | 25 | 26 | class Qux: 27 | def __init__(self, group: str = "qux") -> None: 28 | grp = aws_iam.Group(GroupName=group) 29 | -------------------------------------------------------------------------------- /tests/e2e/fixture/tree/output.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Resources: 3 | FooWchYS7ZC2NU: 4 | Type: AWS::CloudFormation::WaitConditionHandle 5 | BarBazNetiPCY6OZJU: 6 | Type: AWS::EC2::NetworkInterface 7 | Properties: 8 | SubnetId: subnet_id 9 | BarQuxGrp6XB4Y4UC: 10 | Type: AWS::IAM::Group 11 | Properties: 12 | GroupName: qux 13 | -------------------------------------------------------------------------------- /tests/e2e/test_all.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Farzad Senart and Lionel Suss. All rights reserved. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | import importlib.resources 17 | import logging 18 | import os 19 | import subprocess 20 | 21 | import pytest 22 | 23 | from . import fixture 24 | 25 | LOGGER = logging.getLogger(__name__) 26 | 27 | TESTS = [f for f in importlib.resources.contents(fixture) if not f.startswith("_")] 28 | 29 | 30 | @pytest.mark.parametrize("tid", TESTS) 31 | def test_file(tid, tmpdir): 32 | output = tmpdir / "output.yaml" 33 | with importlib.resources.path(fixture, tid) as path: 34 | if path.name == "complete_module": 35 | return 36 | res = subprocess.run( 37 | f"ig cfn -i input.py -r Entrypoint -o {str(output)}", 38 | cwd=path, 39 | capture_output=True, 40 | shell=True, 41 | encoding="utf-8", 42 | ) 43 | if res.stderr: 44 | LOGGER.info("[stderr]\n%s", res.stderr) 45 | if res.stdout: 46 | LOGGER.info("[stdout]\n%s", res.stdout) 47 | if res.returncode: 48 | assert False, "unexpected command failure" 49 | want = (path / "output.yaml").read_text("utf-8") 50 | got = output.read_text("utf-8") 51 | assert got == want, str(output) 52 | 53 | 54 | @pytest.mark.parametrize("tid", TESTS) 55 | def test_module(tid, tmpdir): 56 | output = tmpdir / "output.yaml" 57 | with importlib.resources.path(fixture, tid) as path: 58 | if path.name == "complete_file": 59 | return 60 | env = {k: v for k, v in os.environ.items()} 61 | env.update(PYTHONPATH=".") 62 | res = subprocess.run( 63 | f"ig cfn -i {path.name}.input -r Entrypoint -o {str(output)}", 64 | cwd=path.parent, 65 | capture_output=True, 66 | shell=True, 67 | encoding="utf-8", 68 | env=env, 69 | ) 70 | if res.stderr: 71 | LOGGER.info("[stderr]\n%s", res.stderr) 72 | if res.stdout: 73 | LOGGER.info("[stdout]\n%s", res.stdout) 74 | if res.returncode: 75 | assert False, "unexpected command failure" 76 | want = (path / "output.yaml").read_text("utf-8") 77 | got = output.read_text("utf-8") 78 | assert got == want, str(output) 79 | # print(str(output)) 80 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/engine/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifadev-archive/ingraph/d3558eb28fa77c45caf7a4859e724185dc48b5aa/tests/unit/engine/__init__.py -------------------------------------------------------------------------------- /tests/unit/engine/test_importer.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Farzad Senart and Lionel Suss. All rights reserved. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | from pathlib import Path 17 | 18 | import pytest 19 | from ingraph.engine import core 20 | from ingraph.engine.importer import (aws_hook, external_hook, import_file, 21 | import_module) 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "lid,typ", 26 | [ 27 | pytest.param(*t, id=t[0]) 28 | for t in [ 29 | ("account_id", core.AccountID), 30 | ("availability_zones", core.AvailabilityZones), 31 | ("notification_arns", core.NotificationARNs), 32 | ("partition", core.Partition), 33 | ("region", core.Region), 34 | ("stack_id", core.StackID), 35 | ("stack_name", core.StackName), 36 | ("url_suffix", core.URLSuffix), 37 | ] 38 | ], 39 | ) 40 | def test_aws_pseudo(lid, typ): 41 | with aws_hook(): 42 | from ingraph import aws 43 | 44 | val = getattr(aws, lid.upper()) 45 | assert isinstance(val, typ) 46 | 47 | 48 | @pytest.mark.parametrize( 49 | "lid,typ", 50 | [ 51 | pytest.param(*t, id=t[0]) 52 | for t in [("b64encode", core.Base64Encoded), ("cidr", core.CIDR),] 53 | ], 54 | ) 55 | def test_aws_intrinsic(lid, typ): 56 | with aws_hook(): 57 | from ingraph import aws 58 | 59 | val = getattr(aws, lid) 60 | assert issubclass(val, typ) 61 | 62 | 63 | def test_aws_tag(): 64 | with aws_hook(): 65 | from ingraph import aws 66 | 67 | tag = aws.Tag(Key="key", Value="value") 68 | assert isinstance(tag, core.Property) 69 | 70 | 71 | def test_aws_modules(): 72 | base = Path(__file__).parent.parent.parent.parent / "src" / "ingraph" / "aws" 73 | with aws_hook(): 74 | for file in list(base.glob("[a-z]*_*.pyi")): 75 | import_module(f"ingraph.aws.{file.stem}") 76 | 77 | 78 | @pytest.mark.parametrize( 79 | "module,error", 80 | [ 81 | pytest.param(*t[1:], id=t[0]) 82 | for t in [ 83 | ("not_managed", "ingraph.engine", "No module named 'ingraph.engine'",), 84 | ("not_managed", "sys", "No module named 'sys'",), 85 | ( 86 | "aws_notexists", 87 | "ingraph.aws.aws_exp", 88 | "No module named 'ingraph.aws.aws_exp'", 89 | ), 90 | ("module_notexists", "dummy", "No module named 'dummy'",), 91 | ] 92 | ], 93 | ) 94 | def test_import_module(module, error): 95 | with pytest.raises(Exception, match=error): 96 | with external_hook(): 97 | with aws_hook(): 98 | import_module(module) 99 | 100 | 101 | @pytest.mark.parametrize( 102 | "stmt,error", 103 | [ 104 | pytest.param(*t[1:], id=t[0]) 105 | for t in [ 106 | ("not_managed", "from ingraph import cli", "Unexpected import",), 107 | ("not_managed", "from math import cos", "No module named 'math'"), 108 | ] 109 | ], 110 | ) 111 | def test_import_file(stmt, error, tmpdir): 112 | path = tmpdir / "foo.py" 113 | path.write_text(stmt, "utf-8") 114 | with pytest.raises(Exception, match=error): 115 | with external_hook(): 116 | with aws_hook(): 117 | import_file(path) 118 | -------------------------------------------------------------------------------- /tests/unit/engine/test_packager.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Farzad Senart and Lionel Suss. All rights reserved. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | import time 17 | import zipfile 18 | from io import BytesIO 19 | from pathlib import Path 20 | 21 | import pytest 22 | from ingraph.engine.core import gethash, package_zip 23 | 24 | 25 | def test_package_zip(tmpdir): 26 | path = Path(tmpdir.mkdir("package")) 27 | (path / "foo.txt").write_text("foo") 28 | (path / "bar").mkdir() 29 | (path / "bar" / "baz.txt").write_text("baz") 30 | (path / ".qux").touch() 31 | 32 | pack_file = package_zip(path / "foo.txt") 33 | with zipfile.ZipFile(pack_file.name, "r") as zip_file: 34 | assert zipfile.Path(zip_file, "foo.txt").is_file() 35 | 36 | pack_dir = package_zip(path) 37 | h1 = gethash(Path(pack_dir.name)) 38 | 39 | with zipfile.ZipFile(pack_dir.name, "r") as zip_file: 40 | assert zipfile.Path(zip_file, "foo.txt").is_file() 41 | assert zipfile.Path(zip_file, "bar/").is_dir() 42 | assert zipfile.Path(zip_file, "bar/baz.txt").is_file() 43 | assert not zipfile.Path(zip_file, ".qux").exists() 44 | 45 | time.sleep(1) 46 | (path / "foo.txt").touch() 47 | 48 | pack_dir = package_zip(path) 49 | h2 = gethash(Path(pack_dir.name)) 50 | 51 | assert h1 == h2 52 | 53 | pack_dir = package_zip(path, [path / "bar" / "baz.txt"]) 54 | with zipfile.ZipFile(pack_dir.name, "r") as zip_file: 55 | assert not zipfile.Path(zip_file, str(path / "foo.txt")).exists() 56 | assert zipfile.Path(zip_file, str(path / "bar" / "baz.txt")).is_file() 57 | 58 | 59 | def test_gethash_blob(): 60 | h = gethash(BytesIO(b"foo")) 61 | 62 | assert h == "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33" 63 | -------------------------------------------------------------------------------- /tests/unit/engine/test_parser.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Farzad Senart and Lionel Suss. All rights reserved. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | import inspect 17 | import textwrap 18 | import types 19 | 20 | import pytest 21 | from ingraph.engine import core, importer, parser 22 | from ingraph.engine.parser import (CallWrapper, DefinitionWrapper, 23 | FStringWrapper, ImmutabilityGuard, 24 | LocationEnricher, PrivateChecker, 25 | SignatureModifier, TypeWrapper, process) 26 | 27 | 28 | @pytest.mark.parametrize( 29 | "source", 30 | [ 31 | pytest.param(*t[1:], id=t[0]) 32 | for t in [ 33 | ( 34 | "import", 35 | """ 36 | from foo import _bar 37 | """, 38 | ), 39 | ( 40 | "import", 41 | """ 42 | from foo import bar as _baz 43 | """, 44 | ), 45 | ( 46 | "class", 47 | """ 48 | class _Foo: 49 | def __init__(self): 50 | ... 51 | """, 52 | ), 53 | ( 54 | "param", 55 | """ 56 | class Foo: 57 | def __init__(self, _foo): 58 | ... 59 | """, 60 | ), 61 | ( 62 | "var", 63 | """ 64 | class Foo: 65 | def __init__(self): 66 | _foo = 42 67 | """, 68 | ), 69 | ( 70 | "attr", 71 | """ 72 | class Foo: 73 | def __init__(self): 74 | foo._bar = 42 75 | """, 76 | ), 77 | ( 78 | "call", 79 | """ 80 | class Foo: 81 | def __init__(self): 82 | _foo() 83 | """, 84 | ), 85 | ( 86 | "arg", 87 | """ 88 | class Foo: 89 | def __init__(self): 90 | foo(_bar=42) 91 | """, 92 | ), 93 | ] 94 | ], 95 | ) 96 | def test_private_names(source, mocker): 97 | module = types.ModuleType("") 98 | mocker.patch.object(parser, "TRANSFORMERS", [PrivateChecker]) 99 | 100 | err = "names cannot start with an underscore" 101 | with pytest.raises(SyntaxError, match=err): 102 | process(textwrap.dedent(source), "", module) 103 | 104 | 105 | def test_signature_modifier(mocker): 106 | source = textwrap.dedent( 107 | """ 108 | class Foo: 109 | def __init__(self, foo, bar=42): 110 | ... 111 | """ 112 | ) 113 | module = types.ModuleType("") 114 | mocker.patch.object(parser, "TRANSFORMERS", [SignatureModifier]) 115 | process(source, "", module) 116 | sig = inspect.signature(module.Foo) 117 | params = sig.parameters 118 | 119 | assert all(p.kind == inspect.Parameter.KEYWORD_ONLY for p in params.values()) 120 | assert params["foo"].default == inspect.Parameter.empty 121 | assert params["bar"].default == 42 122 | 123 | 124 | def test_immutability(mocker): 125 | source = textwrap.dedent( 126 | """ 127 | class Foo: 128 | def __init__(self): 129 | self.baz = 42 130 | self.baz = 24 131 | bar = 42 132 | bar = 24 133 | """ 134 | ) 135 | module = types.ModuleType("") 136 | mocker.patch.object(parser, "TRANSFORMERS", [ImmutabilityGuard]) 137 | process(source, "", module) 138 | 139 | with pytest.raises(TypeError, match="'bar' is read-only"): 140 | module.Foo() 141 | 142 | 143 | def test_definition_wrapper(mocker): 144 | source = textwrap.dedent( 145 | """ 146 | class Foo: 147 | def __init__(self): 148 | ... 149 | """ 150 | ) 151 | module = types.ModuleType("") 152 | mocker.patch.object(parser, "TRANSFORMERS", [DefinitionWrapper]) 153 | process(source, "", module) 154 | 155 | assert issubclass(module.Foo, core.ExternalResource) 156 | 157 | 158 | def test_definition_wrapper_custom(mocker): 159 | source = textwrap.dedent( 160 | """ 161 | class Foo(CustomResource): 162 | Bar: int 163 | """ 164 | ) 165 | module = types.ModuleType("") 166 | mocker.patch.object(parser, "TRANSFORMERS", [DefinitionWrapper]) 167 | with importer.aws_hook(): 168 | from ingraph.aws.aws_cloudformation import CustomResource 169 | 170 | module.CustomResource = CustomResource 171 | process(source, "", module) 172 | 173 | assert issubclass(module.Foo, core.CustomResource) 174 | assert issubclass(module.Foo, CustomResource) 175 | 176 | 177 | def test_type_wrapper(mocker): 178 | source = textwrap.dedent( 179 | """ 180 | class Foo: 181 | def __init__(self, p = 42) -> None: 182 | ... 183 | self.p = p 184 | self.b = True 185 | self.n = 42 186 | self.s = "foo" 187 | self.m = { 188 | "b": True, 189 | "n": 42, 190 | "s": "foo", 191 | "m": {} 192 | } 193 | self.lb = [True, False] 194 | self.ln = [4.2, -42] 195 | self.ls = ["foo", "bar"] 196 | self.lf = [True, [4, "foo"], {"s": "bar"}] 197 | """ 198 | ) 199 | module = types.ModuleType("") 200 | mocker.patch.object(parser, "TRANSFORMERS", [TypeWrapper]) 201 | process(source, "", module) 202 | foo = module.Foo() 203 | 204 | assert isinstance(foo.p, core.Number) 205 | assert isinstance(foo.b, core.Boolean) 206 | assert isinstance(foo.n, core.Number) 207 | assert isinstance(foo.s, core.String) 208 | assert isinstance(foo.m, core.Map) 209 | assert all(isinstance(k, core.String) for k in foo.m._ig_value) 210 | mkvs = [type(v) for v in foo.m._ig_value.values()] 211 | assert mkvs == [core.Boolean, core.Number, core.String, core.Map] 212 | assert isinstance(foo.lb, core.LISTS[core.Boolean]) 213 | assert isinstance(foo.ln, core.LISTS[core.Number]) 214 | assert isinstance(foo.ls, core.LISTS[core.String]) 215 | assert isinstance(foo.lf, core.FreeList) 216 | 217 | 218 | def test_type_wrapper_invalid(mocker): 219 | source = textwrap.dedent( 220 | """ 221 | class Foo: 222 | def __init__(self): 223 | b"" 224 | """ 225 | ) 226 | module = types.ModuleType("") 227 | mocker.patch.object(parser, "TRANSFORMERS", [TypeWrapper]) 228 | 229 | err = "unsupported primitive type 'bytes'" 230 | with pytest.raises(SyntaxError, match=err): 231 | process(source, "", module) 232 | 233 | 234 | def test_location_enricher(mocker): 235 | source = textwrap.dedent( 236 | """ 237 | 238 | class Foo: 239 | def __init__(self): 240 | ... 241 | """ 242 | ) 243 | module = types.ModuleType("") 244 | mocker.patch.object(parser, "TRANSFORMERS", [LocationEnricher]) 245 | process(source, "", module) 246 | 247 | assert module.Foo._ig_filename == "" 248 | assert module.Foo._ig_lines == (3, 5) 249 | assert module.Foo._ig_columns == (0, 11) 250 | assert ( 251 | module.Foo._ig_source 252 | == textwrap.dedent( 253 | """ 254 | class Foo: 255 | def __init__(self): 256 | ... 257 | """ 258 | ).strip() 259 | ) 260 | 261 | 262 | def test_call_wrapper_python(mocker): 263 | for typ in {int, float, str}: 264 | source = textwrap.dedent( 265 | f""" 266 | class Foo: 267 | def __init__(self): 268 | {typ.__name__}() 269 | """ 270 | ) 271 | module = types.ModuleType("") 272 | mocker.patch.object(parser, "TRANSFORMERS", [CallWrapper]) 273 | process(source, "", module) 274 | 275 | with pytest.raises(TypeError, match="invalid call"): 276 | module.Foo() 277 | 278 | 279 | def test_call_wrapper_name_check(mocker): 280 | source = textwrap.dedent( 281 | """ 282 | class Foo: 283 | def __init__(self): 284 | Bar() 285 | 286 | class Bar: 287 | def __init__(self): 288 | ... 289 | """ 290 | ) 291 | module = types.ModuleType("") 292 | mocker.patch.object(parser, "TRANSFORMERS", [DefinitionWrapper, CallWrapper]) 293 | process(source, "", module) 294 | 295 | node = mocker.sentinel.NODE 296 | err = "invalid resource instantiation" 297 | with pytest.raises(TypeError, match=err): 298 | module.Foo(_ig_node=node) 299 | 300 | 301 | def test_call_wrapper_proxy(mocker): 302 | source = textwrap.dedent( 303 | """ 304 | class Foo: 305 | def __init__(self): 306 | bar() 307 | """ 308 | ) 309 | module = types.ModuleType("") 310 | module.bar = mocker.stub() 311 | mocker.patch.object(parser, "TRANSFORMERS", [CallWrapper]) 312 | process(source, "", module) 313 | 314 | module.Foo() 315 | 316 | module.bar.assert_called_once() 317 | 318 | 319 | def test_call_wrapper(mocker): 320 | source = textwrap.dedent( 321 | """ 322 | class Foo: 323 | def __init__(self): 324 | bar = Bar() 325 | 326 | class Bar: 327 | def __init__(self): 328 | ... 329 | """ 330 | ) 331 | module = types.ModuleType("") 332 | mocker.patch.object( 333 | parser, "TRANSFORMERS", [DefinitionWrapper, LocationEnricher, CallWrapper] 334 | ) 335 | process(source, "", module) 336 | node = mocker.sentinel.NODE 337 | 338 | with core.new_graph(module.Foo) as graph: 339 | foo = module.Foo() 340 | 341 | lineages = [n.lineage for n in graph.nodes()] 342 | assert lineages == [core.ROOT, core.ROOT + ("bar",)] 343 | 344 | resources = [n.resource for n in graph.nodes()] 345 | assert isinstance(resources[0], module.Foo) 346 | assert isinstance(resources[1], module.Bar) 347 | 348 | 349 | def test_fstring_wrapper(mocker): 350 | source = textwrap.dedent( 351 | """ 352 | class Foo: 353 | def __init__(self): 354 | n = 42 355 | s = 'baz' 356 | self.bar = f"{n} {s}" 357 | """ 358 | ) 359 | module = types.ModuleType("") 360 | mocker.patch.object(parser, "TRANSFORMERS", [TypeWrapper, FStringWrapper]) 361 | process(source, "", module) 362 | bar = module.Foo().bar 363 | 364 | assert isinstance(bar, core.Sub) 365 | values = [v._ig_value for v in bar._ig_kwargs._ig_value.values()] 366 | assert values == [42, " ", "baz"] 367 | --------------------------------------------------------------------------------