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 | 
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 |
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 |
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 |
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 |
155 |
156 |
157 |
158 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/docs_backup/img/home/undraw_game_world_0o6q.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs_backup/img/home/undraw_good_team_m7uu.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs_backup/img/ingraph/undraw_deliveries_131a.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs_backup/img/ingraph/undraw_dev_focus_b9xo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------