├── .gitignore ├── COPYING ├── Makefile ├── README.md ├── aiatools ├── __init__.py ├── aia.py ├── alexa-devices.json ├── algebra.py ├── attributes.py ├── block_types.py ├── common.py ├── component_types.py ├── selectors.py └── simple_components.json ├── doc ├── index.rst └── source │ ├── aiatools.rst │ ├── conf.py │ ├── index.rst │ └── modules.rst ├── make.bat ├── misc └── aia-summarizer2.py ├── plot.py ├── pyproject.toml ├── requirements.txt ├── samples.py ├── setup.cfg └── test_aias ├── LondonCholeraMap.aia ├── ProcedureTest.aia ├── Yahtzee5.aia └── moodring_patched.aia /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | .venv/ 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | ### Vim template 94 | # swap 95 | [._]*.s[a-w][a-z] 96 | [._]s[a-w][a-z] 97 | # session 98 | Session.vim 99 | # temporary 100 | .netrwhist 101 | *~ 102 | # auto-generated tag files 103 | tags 104 | ### JetBrains template 105 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 106 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 107 | 108 | # User-specific stuff: 109 | .idea/workspace.xml 110 | .idea/tasks.xml 111 | 112 | # Sensitive or high-churn files: 113 | .idea/dataSources/ 114 | .idea/dataSources.ids 115 | .idea/dataSources.xml 116 | .idea/dataSources.local.xml 117 | .idea/sqlDataSources.xml 118 | .idea/dynamic.xml 119 | .idea/uiDesigner.xml 120 | 121 | # Gradle: 122 | .idea/gradle.xml 123 | .idea/libraries 124 | 125 | # Mongo Explorer plugin: 126 | .idea/mongoSettings.xml 127 | 128 | ## File-based project format: 129 | *.iws 130 | 131 | ## Plugin-specific files: 132 | 133 | # IntelliJ 134 | /out/ 135 | 136 | # mpeltonen/sbt-idea plugin 137 | .idea_modules/ 138 | 139 | # JIRA plugin 140 | atlassian-ide-plugin.xml 141 | 142 | # Crashlytics plugin (for Android Studio and IntelliJ) 143 | com_crashlytics_export_strings.xml 144 | crashlytics.properties 145 | crashlytics-build.properties 146 | fabric.properties 147 | ### VirtualEnv template 148 | # Virtualenv 149 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 150 | [Bb]in 151 | [Ii]nclude 152 | [Ll]ib 153 | [Ll]ib64 154 | [Ll]ocal 155 | [Ss]cripts 156 | pyvenv.cfg 157 | .venv 158 | pip-selfcheck.json 159 | ### Emacs template 160 | # -*- mode: gitignore; -*- 161 | \#*\# 162 | /.emacs.desktop 163 | /.emacs.desktop.lock 164 | *.elc 165 | auto-save-list 166 | tramp 167 | .\#* 168 | 169 | # Org-mode 170 | .org-id-locations 171 | *_archive 172 | 173 | # flymake-mode 174 | *_flymake.* 175 | 176 | # eshell files 177 | /eshell/history 178 | /eshell/lastdir 179 | 180 | # elpa packages 181 | /elpa/ 182 | 183 | # reftex files 184 | *.rel 185 | 186 | # AUCTeX auto folder 187 | /auto/ 188 | 189 | # cask packages 190 | .cask/ 191 | 192 | # Flycheck 193 | flycheck_*.el 194 | 195 | # server auth directory 196 | /server/ 197 | 198 | # projectiles files 199 | .projectile 200 | 201 | # directory configuration 202 | .dir-locals.el 203 | 204 | doc/source/_static/ 205 | doc/source/_templates/ 206 | 207 | .idea 208 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = doc/build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_elements.papersize=a4 12 | PAPEROPT_letter = -D latex_elements.papersize=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) doc/source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and an HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " lualatexpdf to make LaTeX files and run them through lualatex" 35 | @echo " xelatexpdf to make LaTeX files and run them through xelatex" 36 | @echo " text to make text files" 37 | @echo " man to make manual pages" 38 | @echo " texinfo to make Texinfo files" 39 | @echo " info to make Texinfo files and run them through makeinfo" 40 | @echo " gettext to make PO message catalogs" 41 | @echo " changes to make an overview of all changed/added/deprecated items" 42 | @echo " xml to make Docutils-native XML files" 43 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 44 | @echo " linkcheck to check all external links for integrity" 45 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 46 | @echo " coverage to run coverage check of the documentation (if enabled)" 47 | @echo " dummy to check syntax errors of document sources" 48 | 49 | .PHONY: clean 50 | clean: 51 | rm -rf $(BUILDDIR)/* 52 | 53 | .PHONY: html 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | .PHONY: dirhtml 60 | dirhtml: 61 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 62 | @echo 63 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 64 | 65 | .PHONY: singlehtml 66 | singlehtml: 67 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 68 | @echo 69 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 70 | 71 | .PHONY: pickle 72 | pickle: 73 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 74 | @echo 75 | @echo "Build finished; now you can process the pickle files." 76 | 77 | .PHONY: json 78 | json: 79 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 80 | @echo 81 | @echo "Build finished; now you can process the JSON files." 82 | 83 | .PHONY: htmlhelp 84 | htmlhelp: 85 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 86 | @echo 87 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 88 | ".hhp project file in $(BUILDDIR)/htmlhelp." 89 | 90 | .PHONY: qthelp 91 | qthelp: 92 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 93 | @echo 94 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 95 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 96 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/AIATools.qhcp" 97 | @echo "To view the help file:" 98 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/AIATools.qhc" 99 | 100 | .PHONY: applehelp 101 | applehelp: 102 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 103 | @echo 104 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 105 | @echo "N.B. You won't be able to view it unless you put it in" \ 106 | "~/Library/Documentation/Help or install it in your application" \ 107 | "bundle." 108 | 109 | .PHONY: devhelp 110 | devhelp: 111 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 112 | @echo 113 | @echo "Build finished." 114 | @echo "To view the help file:" 115 | @echo "# mkdir -p $$HOME/.local/share/devhelp/AIATools" 116 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/AIATools" 117 | @echo "# devhelp" 118 | 119 | .PHONY: epub 120 | epub: 121 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 122 | @echo 123 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 124 | 125 | .PHONY: epub3 126 | epub3: 127 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 128 | @echo 129 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 130 | 131 | .PHONY: latex 132 | latex: 133 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 134 | @echo 135 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 136 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 137 | "(use \`make latexpdf' here to do that automatically)." 138 | 139 | .PHONY: latexpdf 140 | latexpdf: 141 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 142 | @echo "Running LaTeX files through pdflatex..." 143 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 144 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 145 | 146 | .PHONY: latexpdfja 147 | latexpdfja: 148 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 149 | @echo "Running LaTeX files through platex and dvipdfmx..." 150 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 151 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 152 | 153 | .PHONY: lualatexpdf 154 | lualatexpdf: 155 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 156 | @echo "Running LaTeX files through lualatex..." 157 | $(MAKE) PDFLATEX=lualatex -C $(BUILDDIR)/latex all-pdf 158 | @echo "lualatex finished; the PDF files are in $(BUILDDIR)/latex." 159 | 160 | .PHONY: xelatexpdf 161 | xelatexpdf: 162 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 163 | @echo "Running LaTeX files through xelatex..." 164 | $(MAKE) PDFLATEX=xelatex -C $(BUILDDIR)/latex all-pdf 165 | @echo "xelatex finished; the PDF files are in $(BUILDDIR)/latex." 166 | 167 | .PHONY: text 168 | text: 169 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 170 | @echo 171 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 172 | 173 | .PHONY: man 174 | man: 175 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 176 | @echo 177 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 178 | 179 | .PHONY: texinfo 180 | texinfo: 181 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 182 | @echo 183 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 184 | @echo "Run \`make' in that directory to run these through makeinfo" \ 185 | "(use \`make info' here to do that automatically)." 186 | 187 | .PHONY: info 188 | info: 189 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 190 | @echo "Running Texinfo files through makeinfo..." 191 | make -C $(BUILDDIR)/texinfo info 192 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 193 | 194 | .PHONY: gettext 195 | gettext: 196 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 197 | @echo 198 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 199 | 200 | .PHONY: changes 201 | changes: 202 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 203 | @echo 204 | @echo "The overview file is in $(BUILDDIR)/changes." 205 | 206 | .PHONY: linkcheck 207 | linkcheck: 208 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 209 | @echo 210 | @echo "Link check complete; look for any errors in the above output " \ 211 | "or in $(BUILDDIR)/linkcheck/output.txt." 212 | 213 | .PHONY: doctest 214 | doctest: 215 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 216 | @echo "Testing of doctests in the sources finished, look at the " \ 217 | "results in $(BUILDDIR)/doctest/output.txt." 218 | 219 | .PHONY: coverage 220 | coverage: 221 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 222 | @echo "Testing of coverage in the sources finished, look at the " \ 223 | "results in $(BUILDDIR)/coverage/python.txt." 224 | 225 | .PHONY: xml 226 | xml: 227 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 228 | @echo 229 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 230 | 231 | .PHONY: pseudoxml 232 | pseudoxml: 233 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 234 | @echo 235 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 236 | 237 | .PHONY: dummy 238 | dummy: 239 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 240 | @echo 241 | @echo "Build finished. Dummy builder generates no files." 242 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AIA Tools 2 | AIA Tools is a Python library for interacting with App Inventor Application (AIA) files in Python. It is useful for opening, summarizing, and analyzing AIA files for research inquiries. The query API is inspired by SQLalchemy 3 | 4 | ## Installing 5 | 6 | ```shell 7 | $ pip install aiatools 8 | ``` 9 | 10 | For development: 11 | 12 | ```shell 13 | $ pyenv install 3.6.3 14 | $ pyenv virtualenv 3.6.3 aiatools 15 | $ pyenv activate aiatools 16 | $ pip install -r requirements.txt 17 | $ pip install . 18 | ``` 19 | 20 | ## Usage Examples 21 | 22 | ```python 23 | from aiatools import AIAFile 24 | 25 | with AIAFile('MyProject.aia') as aia: 26 | print('Number of screens: %d\n' % len(aia.screens)) 27 | print('Number of components: %d\n' % len(aia.screens['Screen1'].componentiter())) 28 | print('Number of blocks: %d\n' % len(aia.screens['Screen1'].blockiter())) 29 | print('Number of event blocks: %d\n' % len(aia.screens['Screen1'].blockiter(type='component_event'))) 30 | aia.screens['Screen1'].blocks(type=='component_event').count(by='event_name') 31 | ``` 32 | 33 | ```python 34 | from aiatools import AIAFile 35 | from aiatools.attributes import event_name, type 36 | from aiatools.block_types import * 37 | from aiatools.component_types import * 38 | 39 | aia = AIAFile('MyProject.aia') 40 | 41 | # Count the number of screens 42 | print len(aia.screens) 43 | 44 | # Count the number of distinct component types used on Screen1 45 | print aia.screens['Screen1'].components().count(group_by=type) 46 | 47 | # Count the number of Button components on Screen1 48 | print aia.screens['Screen1'].components(type==Button).count() 49 | 50 | # Count the number of component_event blocks, grouped by event name 51 | print aia.screens['Screen1'].blocks(type==component_event).count(group_by=event_name) 52 | 53 | # Compute the average depth of the blocks tree in Button.Click handlers 54 | print aia.screens['Screen1'].blocks(type==component_event && event_name == Button.Click).avg(depth) 55 | 56 | # Count the number of blocks referencing a specific component 57 | print aia.screens['Screen1'].components(name=='Button1').blocks().count() 58 | 59 | # Count the number of event handlers where the event opens another screen 60 | print aia.blocks(type==component_event).descendants(type==control_openAnotherScreen).count() 61 | 62 | # Get the screens where the user has included more than one TinyDB 63 | print aia.screens().components(type==TinyDB).count(group_by = Screen.name).filter(lambda k,v: v > 1) 64 | ``` 65 | 66 | ## Selectors 67 | 68 | ```python 69 | project = AIAFile('project.aia') 70 | 71 | project.screens() # Select all screens 72 | project.screens('Screen1') # Select Screen1 73 | project.screens(Button.any) # Select any screen with at least 1 button 74 | project.screens(Control.open_another_screen) # Select any screen containing an open_another_screen block 75 | project.screens(Component.Name == 'TinyDb1') # Select any screen containing a component named TinyDb1 76 | ``` 77 | 78 | ```python 79 | class Block(object): 80 | """ 81 | :py:class:`Block` represents an individual block in the blocks workspace. 82 | 83 | .. Arguments :: 84 | id_ The block ID 85 | type_ :py:class:`BlockType` The block type 86 | """ 87 | def __init__(self, id_, type_): 88 | self.id = id_ 89 | self.type = type_ 90 | self.parent = None 91 | self.children = [] 92 | 93 | 94 | class Component(object): 95 | """ 96 | :py:class:`Component` represents a component in the designer view. 97 | 98 | .. Arguments :: 99 | id_ 100 | type_ :py:class:`ComponentType` 101 | """ 102 | def __init__(self, id_, type_): 103 | self.id = id_ 104 | self.type = type_ 105 | self.properties = {} 106 | 107 | 108 | class ComponentContainer(Component): 109 | def __init__(self, id_, type_): 110 | super(self, ComponentContainer).__init__(id_, type_) 111 | self.components = [] 112 | 113 | 114 | class BlockType(object): 115 | def __init__(self, name): 116 | self.name = name 117 | self.mutators = [] 118 | 119 | 120 | class ComponentType(object): 121 | def __init__(self, name, class_name): 122 | self.name = name 123 | self.class_name = class_name 124 | 125 | 126 | class Screen(object): 127 | def __init__(self, scm=None, bky=None): 128 | self.name = '' 129 | self.properties = {} 130 | self.components = FilterableDict() 131 | self.blocks = FilterableDict() 132 | self.top_blocks = FilterableDict() 133 | if scm is not None: 134 | self._read_scheme(scm) 135 | if bky is not None: 136 | self._read_blocks(bky) 137 | 138 | 139 | class Project(object): 140 | def __init__(self, file=None): 141 | self.name = '' 142 | self.screens = FilterableDict() 143 | self.components = FilterableDict() 144 | self.components.parent = self.screens 145 | self.blocks = FilterableDict() 146 | self.blocks.parent = self.screens 147 | if file is not None: 148 | self.read(file) 149 | 150 | 151 | class FilterableDict(dict): 152 | def __call__(self, filter_): 153 | return FilterableDict([k, v for k, v in self.iteritems() if filter_(v) else None, None]) 154 | 155 | 156 | class Filter(object): 157 | def __call__(self, o): 158 | throw NotImplementedError() 159 | 160 | def __and__(self, right): 161 | return and_(self, right) 162 | 163 | def __or__(self, right): 164 | return or_(self, right) 165 | 166 | def __eq__(self, right): 167 | return eq(self, right) 168 | 169 | def __ne__(self, right): 170 | return ne(self, right) 171 | 172 | def __lt__(self, right): 173 | return lt(self, right) 174 | 175 | def __gt__(self, right): 176 | return gt(self, right) 177 | 178 | def __le__(self, right): 179 | return le(self, right) 180 | 181 | def __ge__(self, right): 182 | return ge(self, right) 183 | 184 | 185 | class AndFilter(Filter): 186 | def __init__(self, l, r): 187 | self.l = l 188 | self.r = r 189 | 190 | def __call__(self, o): 191 | return self.l(o) and self.r(o) 192 | 193 | 194 | class OrFilter(Filter): 195 | def __init__(self, l, r): 196 | self.l = l 197 | self.r = r 198 | 199 | def __call__(self, o): 200 | return self.l(o) or self.r(o) 201 | 202 | 203 | class NotFilter(Filter): 204 | def __init__(self, expression): 205 | self.expression = expression 206 | 207 | def __call__(self, o): 208 | return not self.expression(o) 209 | 210 | 211 | class EqualFilter(Filter): 212 | def __init__(self, l, r): 213 | self.l = l 214 | self.r = r 215 | 216 | def __call__(self, o): 217 | return self.l(o) == self.r(o) 218 | 219 | 220 | class NotEqualFilter(Filter): 221 | def __init__(self, l, r): 222 | self.l = l 223 | self.r = r 224 | 225 | def __call__(self, o): 226 | return self.l(o) != self.r(o) 227 | 228 | 229 | class LessThanFilter(Filter): 230 | def __init__(self, l, r): 231 | self.l = l 232 | self.r = r 233 | 234 | def __call__(self, o): 235 | return self.l(o) < self.r(o) 236 | 237 | 238 | class GreaterThanFilter(Filter): 239 | def __init__(self, l, r): 240 | self.l = l 241 | self.r = r 242 | 243 | def __call__(self, o): 244 | return self.l(o) > self.r(o) 245 | 246 | 247 | class LessThanOrEqualFilter(Filter): 248 | def __init__(self, l, r): 249 | self.l = l 250 | self.r = r 251 | 252 | def __call__(self, o): 253 | return self.l(o) <= self.r(o) 254 | 255 | 256 | class GreaterThanOrEqualFilter(Filter): 257 | def __init__(self, l, r): 258 | self.l = l 259 | self.r = r 260 | 261 | def __call__(self, o): 262 | return self.l(o) <= self.r(o) 263 | 264 | 265 | class ScreenFilter(Filter): 266 | pass 267 | 268 | 269 | class ComponentFilter(Filter): 270 | pass 271 | 272 | 273 | class BlockFilter(Filter): 274 | pass 275 | ``` 276 | 277 | ## Attributes 278 | 279 | `depth` - For a component, this is the depth of the component hierarchy rooted at that component. For components that are not containers this value is always 1. For containers and blocks, this is the longest length of the paths from this root node to any of its leaf nodes. 280 | 281 | `length` - The number of direct descendants of the target. If the target is a component container, it will be the number of direct chidlren. For a block, it will be the number of 282 | 283 | `children` - The list of children for the item(s) in the set. If more than one item is in the set, the children will be provided in the order of their parents. 284 | 285 | `mutators` - If the block has mutations, a list of strings indicating the types of the mutations, e.g. ['if', 'elseif', 'elseif', 'else']. 286 | 287 | `callers` - For procedures, the number of caller blocks in the workspace. For variables and component methods and properties, the number of getter blocks. 288 | 289 | ## Aggregation 290 | 291 | `max` - Maximum value of the filter 292 | 293 | `min` - Minimum value of the filter 294 | 295 | `avg` - Average value of the filter 296 | 297 | `count` - Count of items matching the filter 298 | 299 | `median` - Median value of the attribute 300 | -------------------------------------------------------------------------------- /aiatools/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- mode: python; -*- 3 | 4 | """ 5 | AIA Tools provides a set of Python classes to interface with application sources exported from 6 | `App Inventor `_. 7 | """ 8 | 9 | from aiatools.aia import * 10 | from aiatools.algebra import * 11 | from aiatools.attributes import * 12 | from aiatools.block_types import * 13 | from aiatools.component_types import * 14 | from aiatools.selectors import * 15 | 16 | __author__ = 'Evan W. Patton ' 17 | __version__ = '0.6.1' 18 | 19 | 20 | def _aia_main(): 21 | """ 22 | The main program for aiatools. See aiatool --help for more information. 23 | """ 24 | raise NotImplemented() 25 | 26 | 27 | if __name__ == '__main__': 28 | _aia_main() 29 | -------------------------------------------------------------------------------- /aiatools/aia.py: -------------------------------------------------------------------------------- 1 | # -*- mode: python; coding: utf-8; -*- 2 | # Copyright © 2017 Massachusetts Institute of Technology, All rights reserved. 3 | 4 | """ 5 | 6 | .. testsetup :: 7 | 8 | from aiatools.aia import AIAFile 9 | 10 | The :py:mod:`aiatools.aia` package provides the :py:class:`AIAFile` class for reading App Inventor (.aia) projects. 11 | """ 12 | 13 | import logging 14 | import json 15 | import os 16 | from io import StringIO 17 | from os.path import isdir, join 18 | from zipfile import ZipFile 19 | 20 | import jprops 21 | 22 | from .component_types import Screen, Skill, component_from_descriptor 23 | from .selectors import Selector, NamedCollection, UnionSelector 24 | 25 | __author__ = 'Evan W. Patton ' 26 | 27 | log = logging.getLogger(__name__) 28 | 29 | 30 | class AIAAsset(object): 31 | """ 32 | :py:class:`AIAAsset` provides an interface for reading the contents of assets from an App Inventor project. 33 | """ 34 | 35 | def __init__(self, zipfile, name): 36 | """ 37 | Constructs a new reference to an asset within an AIA file. 38 | 39 | :param zipfile: The zipped project file that is the source of the asset. 40 | :param name: The path of the asset within the project file hierarchy. 41 | """ 42 | self.zipfile = zipfile 43 | self.name = name 44 | 45 | def open(self, mode='r'): 46 | """ 47 | Opens the asset. ``mode`` is an optional file access mode. 48 | 49 | :param mode: The access mode to use when accessing the asset. Must be one of 'r', 'rU', or 'U'. 50 | :type mode: basestring 51 | :return: A file-like for accessing the contents of the asset. 52 | :rtype: zipfile.ZipExtFile 53 | """ 54 | return self.zipfile.open(self.name, mode) 55 | 56 | 57 | class AIAFile(object): 58 | """ 59 | :py:class:`AIAFile` encapsulates an App Inventor project (AIA) file. 60 | 61 | Opens an App Inventor project (AIA) with the given filename. ``filename`` can be any file-like object that is 62 | acceptable to :py:class:`ZipFile`'s constructor, or a path to a directory containing an unzipped project. 63 | 64 | Parameters 65 | ---------- 66 | filename : basestring | file 67 | A string or file-like containing the contents of an App Inventor project. 68 | strict : bool, optional 69 | Process the AIAFile in strict mode, i.e., if a blocks file is missing then it is an error. Default: false 70 | """ 71 | 72 | def __init__(self, filename, strict=False): 73 | if filename is None: 74 | self.zipfile = None 75 | elif not isinstance(filename, str) or (not isdir(filename) and filename[-4:] == '.aia'): 76 | self.zipfile = ZipFile(filename) 77 | else: 78 | self.zipfile = None 79 | 80 | self.assets = [] 81 | """ 82 | A list of assets contained in the project. 83 | 84 | :type: list[aiatools.aia.AIAAsset] 85 | """ 86 | 87 | self.filename = filename 88 | """ 89 | The filename or file-like that is the source of the project. 90 | 91 | :type: basestring or file 92 | """ 93 | 94 | self.properties = {} 95 | """ 96 | The contents of the project.properties file. 97 | 98 | :type: Properties 99 | """ 100 | 101 | self._screens = NamedCollection() 102 | self.screens = Selector(self._screens) 103 | """ 104 | A :py:class:`~aiatools.selectors.Selector` over the components of type 105 | :py:class:`~aiatools.component_types.Screen` in the project. 106 | 107 | For example, if you want to know how many screens are in a project run: 108 | 109 | .. doctest:: 110 | 111 | >>> with AIAFile('test_aias/LondonCholeraMap.aia') as aia: 112 | ... len(aia.screens) 113 | 1 114 | 115 | :type: aiatools.selectors.Selector[aiatools.component_types.Screen] 116 | """ 117 | 118 | self._skills = NamedCollection() 119 | self.skills = Selector(self._skills) 120 | """ 121 | A :py:class:`~aiatools.selectors.Selector` over the components of type 122 | :py:class:`~aiatools.component_types.Skill` in the project. 123 | 124 | For example, if you want to know how many skills are in a project run: 125 | 126 | .. doctest:: 127 | 128 | >>> with AIAFile('test_aias/HelloPurr.aia') as aia: 129 | ... len(aia.skills) 130 | 0 131 | 132 | :type: aiatools.selectors.Selector[aiatools.component_types.Skill] 133 | """ 134 | 135 | self.components = UnionSelector(self._screens, 'components') 136 | """ 137 | A :py:class:`~aiatools.selectors.Selector` over the component instances of all screens defined in the project. 138 | 139 | For example, if you want to know how many component instances are in a project run: 140 | 141 | .. doctest:: 142 | 143 | >>> with AIAFile('test_aias/LondonCholeraMap.aia') as aia: 144 | ... len(aia.components()) # Form, Map, Marker, Button 145 | 4 146 | 147 | :type: aiatools.selectors.Selector[aiatools.common.Component] 148 | """ 149 | 150 | self.blocks = UnionSelector(self._screens, 'blocks') 151 | """ 152 | A :py:class:`~aiatoools.selectors.Selector` over the blocks of all screen defined in the project. 153 | 154 | For example, if you want to know how many blocks are in a project run: 155 | 156 | .. doctest:: 157 | 158 | >>> with AIAFile('test_aias/LondonCholeraMap.aia') as aia: 159 | ... len(aia.blocks()) 160 | 23 161 | 162 | :type: aiatools.selectors.Selector[aiatools.common.Block] 163 | """ 164 | 165 | self._extensions = NamedCollection() 166 | self.extensions = Selector(self._extensions) 167 | 168 | if self.zipfile: 169 | self._process_zip(strict) 170 | elif filename is not None: 171 | self._process_dir(strict) 172 | 173 | def close(self): 174 | if self.zipfile: 175 | self.zipfile.close() 176 | self.zipfile = None 177 | 178 | def __enter__(self): 179 | if self.zipfile: 180 | self.zipfile.__enter__() 181 | return self 182 | 183 | def __exit__(self, exc_type, exc_val, exc_tb): 184 | if self.zipfile: 185 | self.zipfile.__exit__(exc_type, exc_val, exc_tb) 186 | 187 | def _listfiles(self): 188 | names = [] 189 | for dirname, dirs, files in os.walk(self.filename): 190 | names.extend([join(dirname, f) for f in files]) 191 | 192 | return names 193 | 194 | def _process_zip(self, strict): 195 | """ 196 | Processes the contents of an AIA file into Python objects for further operation. 197 | """ 198 | self.assets = [] 199 | processed_components = set() 200 | for name in self.zipfile.namelist(): 201 | if name.startswith('assets/'): 202 | if name.startswith('assets/external_comps/'): 203 | package_name = name.split('/')[2] 204 | if package_name in processed_components: 205 | continue 206 | if name.endswith('components.json'): # V2 extension 207 | components_json = json.load(self.zipfile.open(name)) 208 | self._process_extension(components_json) 209 | processed_components.add(package_name) 210 | elif name.endswith('component.json'): # V1 extension 211 | components_json = json.load(self.zipfile.open(name, 'r')) 212 | self._process_extension([components_json]) 213 | processed_components.add(package_name) 214 | else: 215 | self.assets.append(AIAAsset(self, name)) 216 | elif name.startswith('src/'): 217 | if name.endswith('.scm'): 218 | name = name[:-4] 219 | form = self.zipfile.open('%s.scm' % name, 'r') 220 | try: 221 | blocks = self.zipfile.open('%s.bky' % name, 'r') 222 | except KeyError as e: 223 | if strict: 224 | raise e 225 | else: 226 | blocks = None # older aia without a bky file 227 | screen = Screen(design=form, blocks=blocks, project=self) 228 | self._screens[screen.name] = screen 229 | elif name.startswith('skills/'): 230 | if name.endswith('.alexa'): 231 | name = name[:-6] 232 | skill = self.zipfile.open('%s.alexa' % name, 'r') 233 | try: 234 | blocks = self.zipfile.open('%s.abx' % name, 'r') 235 | except KeyError as e: 236 | if strict: 237 | raise e 238 | else: 239 | blocks = None 240 | alexa = Skill(design=skill, blocks=blocks, project=self) 241 | self._skills[alexa.name] = alexa 242 | elif name.endswith('project.properties'): 243 | with self.zipfile.open(name) as prop_file: 244 | self.properties = jprops.load_properties(prop_file) 245 | else: 246 | log.warning('Ignoring file in AIA: %s' % name) 247 | 248 | def _process_dir(self, strict): 249 | """ 250 | Processes the contents of a directory as if it were an AIA file and converts the content into Python objects 251 | for further operation. 252 | """ 253 | self.assets = [] 254 | asset_path = join(self.filename, 'assets') 255 | external_comps = join(asset_path, 'external_comps') 256 | src_path = join(self.filename, 'src') 257 | processed_components = set() 258 | for name in self._listfiles(): 259 | if name.startswith(asset_path): 260 | if name.startswith(external_comps): 261 | package_name = os.path.split(name[len(external_comps) + 1:])[0] 262 | if package_name in processed_components: 263 | continue 264 | if name.endswith('components.json'): # V2 extension 265 | with open(name, 'r') as f: 266 | components_json = json.load(f) 267 | self._process_extension(components_json) 268 | processed_components.add(package_name) 269 | elif name.endswith('component.json'): # V1 extension 270 | with open(name, 'r') as f: 271 | components_json = json.load(f) 272 | self._process_extension([components_json]) 273 | processed_components.add(package_name) 274 | else: 275 | self.assets.append(AIAAsset(None, name)) 276 | elif name.startswith(src_path) or name.endswith('.scm') or name.endswith('.bky'): 277 | if name.endswith('.scm'): 278 | name = name[:-4] 279 | if strict and not os.path.exists('%s.bky' % name): 280 | raise IOError('Did not find expected blocks file %s.bky' % name) 281 | bky_handle = open('%s.bky' % name) if os.path.exists('%s.bky' % name) else StringIO('') 282 | with open('%s.scm' % name, 'r') as form, bky_handle as blocks: 283 | screen = Screen(design=form, blocks=blocks, project=self) 284 | self._screens[screen.name] = screen 285 | elif name.endswith('project.properties'): 286 | with open(name, 'r') as prop_file: 287 | self.properties = jprops.load_properties(prop_file) 288 | else: 289 | log.warning('Ignoring file in directory: %s' % name) 290 | 291 | def _process_extension(self, descriptors: list[dict]): 292 | for descriptor in descriptors: 293 | component = component_from_descriptor(descriptor) 294 | component.project = self 295 | self._extensions[component.name] = component 296 | -------------------------------------------------------------------------------- /aiatools/alexa-devices.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "name": "Alexa", 3 | "version": "2", 4 | "type": "Alexa", 5 | "categoryString": "BUILT_IN", 6 | "external": "false", 7 | "helpString": "", 8 | "showOnPalette": "false", 9 | "nonVisible": "false", 10 | "iconName": "static/images/alexa.png", 11 | "properties": [{ 12 | "name": "SkillName", 13 | "editorType": "ro-string", 14 | "defaultValue": "" 15 | },{ 16 | "name": "Target", 17 | "editorType": "string", 18 | "defaultValue": "Alexa Dot" 19 | }], 20 | "blockProperties": [{ 21 | "name": "SkillName", 22 | "description": "The name used to reference the skill when communicating with Alexa.", 23 | "rw": "invisible", 24 | "type": "text", 25 | "deprecated": "false", 26 | "category": "Advanced" 27 | }], 28 | "methods": [], 29 | "events": [{ 30 | "name": "Initialize", 31 | "description": "Use the Initialize event to perform actions prior to responding to any intent.", 32 | "deprecated": "false", 33 | "params": [] 34 | }] 35 | },{ 36 | "name": "Intent", 37 | "version": "1", 38 | "type": "Intent", 39 | "categoryString": "BUILT_IN", 40 | "external": "false", 41 | "helpString": "Intents represent a single goal for the speaker, expressed through one or more utterances, such as the many ways one might express the idea \"Good Day.\"", 42 | "showOnPalette": "true", 43 | "nonVisible": "true", 44 | "iconName": "images/intent.png", 45 | "properties": [{ 46 | "name": "Utterances", 47 | "editorType": "text_list", 48 | "defaultValue": "" 49 | },{ 50 | "name": "Confirmation Required", 51 | "editorType": "boolean", 52 | "defaultValue": "False" 53 | },{ 54 | "name": "Confirmation Prompt", 55 | "editorType": "text", 56 | "defaultValue": "" 57 | }], 58 | "blockProperties": [{ 59 | "name": "Utterances", 60 | "description": "Example phrases people can use to perform the intent.", 61 | "type": "list", 62 | "rw": "invisible", 63 | "deprecated": "false", 64 | "category": "Behavior" 65 | },{ 66 | "name": "Confirmation Required", 67 | "description" : "If checked, the user will be asked to confirm this is what they intended to do.", 68 | "type": "boolean", 69 | "rw": "invisible", 70 | "deprecated": "false", 71 | "category": "Advanced" 72 | }, { 73 | "name": "Confirmation Prompt", 74 | "description": "The text to use to prompt the user if a confirmation is required.", 75 | "type": "text", 76 | "rw": "invisible", 77 | "deprecated": "false", 78 | "category": "Advanced" 79 | },{ 80 | "name": "utterances", 81 | "description": "The list of phrases people can use to trigger this Intent.", 82 | "type": "list", 83 | "rw": "write-only", 84 | "deprecated": "true", 85 | "category": "Behavior" 86 | }], 87 | "methods": [], 88 | "events": [{ 89 | "name": "spoken", 90 | "description": "Use the spoken event to process the input and respond accordingly.", 91 | "params": [], 92 | "deprecated": "false" 93 | }] 94 | },{ 95 | "name": "Slot", 96 | "version": "2", 97 | "type": "Slot", 98 | "categoryString": "BUILT_IN", 99 | "external": "false", 100 | "helpString": "Slots capture information within an utterance that your skill can use to do further computation. You can think of them as a fill-in-the-blank in an utterance.", 101 | "showOnPalette": "true", 102 | "nonVisible": "true", 103 | "iconName": "images/slot.png", 104 | "properties": [{ 105 | "name": "SlotEditorUsed", 106 | "editorType": "slot_editor", 107 | "defaultValue": "SlotType" 108 | }, { 109 | "name": "SlotType", 110 | "editorType": "slot_types", 111 | "defaultValue": "AMAZON.SearchQuery" 112 | }, { 113 | "name": "alexaSlotType", 114 | "editorType": "string", 115 | "defaultValue": "" 116 | }, { 117 | "name": "customSlotType", 118 | "editorType": "slot_custom", 119 | "defaultValue": "" 120 | }], 121 | "blockProperties": [{ 122 | "name": "SlotEditorUsed", 123 | "description": "Sets whether to use a built-in slot type or a custom slot type.", 124 | "type": "text", 125 | "rw": "invisible", 126 | "deprecated": "false", 127 | "category": "Advanced" 128 | },{ 129 | "name": "SlotType", 130 | "description": "The type of the content spoken for the slot, such as a Food or Animal.", 131 | "type": "text", 132 | "rw": "invisible", 133 | "deprecated": "false", 134 | "category": "Behavior" 135 | },{ 136 | "name": "alexaSlotType", 137 | "description": "The type of the content spoken for the slot, open to any defined Alexa type.", 138 | "type": "text", 139 | "rw": "invisible", 140 | "deprecated": "false", 141 | "category": "Behavior" 142 | },{ 143 | "name": "customSlotType", 144 | "description": "Define your own slot type using examples of that type.", 145 | "type": "text", 146 | "rw": "invisible", 147 | "deprecated": "false", 148 | "category": "Advanced" 149 | },{ 150 | "name": "value", 151 | "description": "The value of the slot, if it was filled by the user.", 152 | "type": "text", 153 | "rw": "read-only", 154 | "deprecated": "false", 155 | "category": "Behavior" 156 | }, { 157 | "name": "placeholder", 158 | "description": "This block is no longer used.", 159 | "type": "text", 160 | "rw": "read-only", 161 | "deprecated": "true", 162 | "category": "Behavior" 163 | }], 164 | "methods": [], 165 | "events": [] 166 | },{ 167 | "name": "CloudDB", 168 | "version": "2", 169 | "type": "AlexaCloudDB", 170 | "categoryString": "ALEXA", 171 | "external": "false", 172 | "helpString": "Allows Alexa Skills to communicate with App Inventor using CloudDB.", 173 | "showOnPalette": "true", 174 | "nonVisible": "true", 175 | "iconName": "images/cloudDB.png", 176 | "properties": [ 177 | { "name": "ProjectID", "editorType": "string", "defaultValue": "", "editorArgs": []}, 178 | { "name": "RedisPort", "editorType": "integer", "defaultValue": "6381", "editorArgs": []}, 179 | { "name": "RedisServer", "editorType": "string", "defaultValue": "DEFAULT", "editorArgs": []}, 180 | { "name": "Token", "editorType": "string", "defaultValue": "", "editorArgs": []}, 181 | { "name": "UseSSL", "editorType": "boolean", "defaultValue": "True", "editorArgs": []} 182 | ], 183 | "blockProperties": [{ 184 | "name": "ProjectID", 185 | "description": "The project bucket where your data are stored", 186 | "type": "text", 187 | "rw": "invisible", 188 | "deprecated": "false", 189 | "category": "Behavior" 190 | },{ 191 | "name": "RedisPort", 192 | "description": "The TCP port number used to connect to CloudDB", 193 | "type": "number", 194 | "rw": "invisible", 195 | "deprecated": "false", 196 | "category": "Advanced" 197 | },{ 198 | "name": "RedisServer", 199 | "description": "The domain name of the CloudDB server", 200 | "type": "text", 201 | "rw": "invisible", 202 | "deprecated": "false", 203 | "category": "Advanced" 204 | },{ 205 | "name": "Token", 206 | "description": "Authentication token for CloudDB", 207 | "type": "text", 208 | "rw": "invisible", 209 | "deprecated": "false", 210 | "category": "Advanced" 211 | },{ 212 | "name": "UseSSL", 213 | "description": "Enable to securely communicate with CloudDB", 214 | "type": "boolean", 215 | "rw": "invisible", 216 | "deprecated": "false", 217 | "category": "Advanced" 218 | }], 219 | "methods": [ 220 | { "name": "GetValue", "description": "Get the Value for a tag, and DOES returns the value", "deprecated": "false", "params": [{ "name": "tag", "type": "text"},{ "name": "valueIfTagNotThere", "type": "any"}], "returnType": "any"}, 221 | { "name": "StoreValue", "description": "Store a value at a tag.", "deprecated": "false", "params": [{ "name": "tag", "type": "text"},{ "name": "valueToStore", "type": "any"}]} 222 | ], 223 | "events": [] 224 | }] 225 | -------------------------------------------------------------------------------- /aiatools/algebra.py: -------------------------------------------------------------------------------- 1 | # -*- mode: python; coding: utf-8; -*- 2 | # Copyright © 2017 Massachusetts Institute of Technology, All rights reserved. 3 | 4 | """ 5 | aiatools.algebra defines the expressions and evaluation rules for querying the contents of AIA files. 6 | """ 7 | try: 8 | from collections.abc import Callable 9 | except ImportError: 10 | from collections import Callable 11 | 12 | 13 | __author__ = 'Evan W. Patton ' 14 | 15 | 16 | def _reduce_expression(expr, op): 17 | while isinstance(expr, Expression) and not isinstance(expr, Atom): 18 | expr = expr(op) 19 | return expr 20 | 21 | 22 | def identity(x): 23 | """ 24 | Helper function that returns its input. 25 | 26 | >>> identity("string") 27 | 'string' 28 | >>> identity(True) 29 | True 30 | >>> identity(None) 31 | 32 | :param x: any value 33 | :return: x 34 | """ 35 | return x 36 | 37 | 38 | def needs_eval(x): 39 | """ 40 | Tests whether its input needs ot be evaluated. 41 | 42 | >>> needs_eval(Expression()) 43 | True 44 | >>> needs_eval(Atom()) 45 | False 46 | 47 | :param x: the input expression 48 | :type x: Expression|callable 49 | :return: 50 | """ 51 | if isinstance(x, Expression): 52 | return not isinstance(x, Atom) 53 | elif isinstance(x, Callable): 54 | return True 55 | return False 56 | 57 | 58 | class Expression(object): 59 | """ 60 | Base interface for constructing expressions over App Inventor projects. 61 | 62 | Expressions by default support the following operations: 63 | 64 | 1. ``left == right``: Accepts an entity if and only if ``left(entity) == right(entity)`` 65 | 2. ``left != right``: Accepts an entity if and only if ``left(entity) != right(entity)`` 66 | 3. ``left < right``: Accepts an entity if and only if ``left(entity) < right(entity)`` 67 | 4. ``left > right``: Accepts an entity if and only if ``left(entity) > right(entity)`` 68 | 5. ``left <= right``: Accepts an entity if and only if ``left(entity) <= right(entity)`` 69 | 6. ``left >= righ``: Accepts an entity if and only if ``left(entity >= right(entity)`` 70 | 7. ``left & right``: Accepts an entity if and only if ``left(entity) and right(entity)`` is True. 71 | 8. ``left | right``: Accepts an entity if and only if ``left(entity) or right(entity)`` is True. 72 | 9. ``~expr``: Accepts an entity if and only if ``not expr(entity)`` is True 73 | """ 74 | def __eq__(self, other): 75 | """ 76 | Constructs a new EquivalenceExpression with this expression as the left hand side and ``other`` as the right 77 | hand side. 78 | 79 | :param other: The other thing to check for equivalence with this expression when evaluated. 80 | :type other: Expression 81 | :return: A new equivalence expression 82 | :rtype: EquivalenceExpression 83 | """ 84 | return EquivalenceExpression(self, other) 85 | 86 | def __ne__(self, other): 87 | """ 88 | Constructs a new NotnequivalenceExpression with this expression as the left hand side and ``expr`` as the right 89 | hand side. 90 | 91 | :param other: The other expression to check for nonequivalence with this expression when evaluated. 92 | :type other: Expression 93 | :return: 94 | """ 95 | return NonequivalenceExpression(self, other) 96 | 97 | def __hash__(self): 98 | return id(self) 99 | 100 | def __lt__(self, other): 101 | return LessThanExpression(self, other) 102 | 103 | def __gt__(self, other): 104 | return GreaterThanExpression(self, other) 105 | 106 | def __le__(self, other): 107 | return LessThanOrEqualExpression(self, other) 108 | 109 | def __ge__(self, other): 110 | return GreaterThanOrEqualExpression(self, other) 111 | 112 | def __and__(self, other): 113 | return AndExpression(self, other) 114 | 115 | def __or__(self, other): 116 | return OrExpression(self, other) 117 | 118 | def __invert__(self): 119 | return NotExpression(self) 120 | 121 | def __call__(self, operand, *args, **kwargs): 122 | raise NotImplementedError 123 | 124 | 125 | class BinaryExpression(Expression): 126 | """ 127 | Abstract base class for an Expression taking two clauses. 128 | 129 | Concrete implementations of this class must provide an implementation of the ``__call__`` special method to evaluate 130 | the truth value of the left and right hand sides. 131 | 132 | Parameters 133 | ---------- 134 | left : Expression 135 | The left hand side of the binary expression. 136 | right : Expression 137 | The right hand side of the binary expression. 138 | """ 139 | def __init__(self, left, right): 140 | self.left = ComputedAttribute(left) if isinstance(left, Callable) and not isinstance(left, Expression) else left 141 | self.right = ComputedAttribute(right) if isinstance(right, Callable) and not isinstance(right, Expression) else right 142 | 143 | def __call__(self, operand, *args, **kwargs): 144 | raise NotImplementedError 145 | 146 | 147 | class EquivalenceExpression(BinaryExpression): 148 | """ 149 | :py:class:`EquivalenceExpression` compares the output of two expressions for equivalent values, however == is 150 | defined on those values. An EquivalenceExpression is typically constructed by using the == operator on an existing 151 | pair of expressions, for example: 152 | 153 | >>> from aiatools.attributes import name 154 | >>> name == 'Button1' 155 | NamedAttributeTuple(('name', 'instance_name')) == 'Button1' 156 | """ 157 | def __call__(self, operand, *args, **kwargs): 158 | left_val = _reduce_expression(self.left, operand) 159 | right_val = _reduce_expression(self.right, operand) 160 | return (left_val == right_val) is True 161 | 162 | def __repr__(self): 163 | return '%r == %r' % (self.left, self.right) 164 | 165 | 166 | class NonequivalenceExpression(BinaryExpression): 167 | """ 168 | :py:class:`NonequivalenceExpression` compares the output of two expressions for nonequivalent values, however != is 169 | defined on those values. A NonequivalenceExpression is typically constructed by using the != operator on an existing 170 | pair of expressions, for example: 171 | 172 | >>> from aiatools.attributes import name 173 | >>> name != 'Button1' 174 | NamedAttributeTuple(('name', 'instance_name')) != 'Button1' 175 | """ 176 | def __call__(self, operand, *args, **kwargs): 177 | left_val = self.left(operand) if isinstance(self.left, Expression) else self.left 178 | right_val = self.right(operand) if isinstance(self.right, Expression) else self.right 179 | return (left_val != right_val) is True 180 | 181 | def __repr__(self): 182 | return '%r != %r' % (self.left, self.right) 183 | 184 | 185 | class LessThanExpression(BinaryExpression): 186 | """ 187 | :py:class:`LessThanExpression` compares the output of two expressions and returns True if the value of the left hand 188 | side of the expression is less than the value of the right hand expression, for the definition of the less than 189 | operation on the two values. A LessThanExpression is typically constructed by using the < operator on an existing 190 | pair of expressions, for example: 191 | 192 | >>> from aiatools.attributes import version 193 | >>> version < 5 194 | NamedAttribute('version') < 5 195 | """ 196 | def __call__(self, operand, *args, **kwargs): 197 | left_val = self.left(operand) if isinstance(self.left, Expression) else self.left 198 | right_val = self.right(operand) if isinstance(self.right, Expression) else self.right 199 | return (left_val < right_val) is True 200 | 201 | def __repr__(self): 202 | return '%r < %r' % (self.left, self.right) 203 | 204 | 205 | class GreaterThanExpression(BinaryExpression): 206 | """ 207 | :py:class:`GreaterThanExpression` compares the output of two expressions and returns True if the value of the left 208 | handl side of the expression is greater than the value of the right hand expression, for the definition of the 209 | greater than operation on the two values. A GreaterThanExpression is typically constructed by using the > operator 210 | on an existing pair of expressions, for example: 211 | 212 | >>> from aiatools.attributes import version 213 | >>> version > 5 214 | NamedAttribute('version') > 5 215 | """ 216 | def __call__(self, operand, *args, **kwargs): 217 | left_val = self.left(operand) if isinstance(self.left, Expression) else self.left 218 | right_val = self.right(operand) if isinstance(self.right, Expression) else self.right 219 | return (left_val > right_val) is True 220 | 221 | def __repr__(self): 222 | return '%r > %r' % (self.left, self.right) 223 | 224 | 225 | class LessThanOrEqualExpression(BinaryExpression): 226 | def __call__(self, operand, *args, **kwargs): 227 | left_val = self.left(operand) if isinstance(self.left, Expression) else self.left 228 | right_val = self.right(operand) if isinstance(self.right, Expression) else self.right 229 | return (left_val <= right_val) is True 230 | 231 | def __repr__(self): 232 | return '%r <= %r' % (self.left, self.right) 233 | 234 | 235 | class GreaterThanOrEqualExpression(BinaryExpression): 236 | def __call__(self, operand, *args, **kwargs): 237 | left_val = self.left(operand) if isinstance(self.left, Expression) else self.left 238 | right_val = self.right(operand) if isinstance(self.right, Expression) else self.right 239 | return (left_val >= right_val) is True 240 | 241 | def __repr__(self): 242 | return '%r >= %r' % (self.left, self.right) 243 | 244 | 245 | class AndExpression(BinaryExpression): 246 | def __call__(self, operand, *args, **kwargs): 247 | left_val = self.left(operand) if isinstance(self.left, Expression) else self.left 248 | right_val = self.right(operand) if isinstance(self.right, Expression) else self.right 249 | return left_val and right_val 250 | 251 | def __repr__(self): 252 | return '%r & %r' % (self.left, self.right) 253 | 254 | 255 | class OrExpression(BinaryExpression): 256 | """ 257 | 258 | """ 259 | def __call__(self, operand, *args, **kwargs): 260 | left_val = self.left(operand) if isinstance(self.left, Expression) else self.left 261 | right_val = self.right(operand) if isinstance(self.right, Expression) else self.right 262 | return left_val or right_val 263 | 264 | def __repr__(self): 265 | return '%s | %s' % (self.left, self.right) 266 | 267 | 268 | class NotExpression(Expression): 269 | """ 270 | :py:class:`NotExpression` is a unary expression that logically negates the output of the expression it encapsulates. 271 | NotExpressions are typically instantiated by using the unary prefix operator ~ to invert the expression. Note that 272 | ~ binds tightly, so most expressions, unless they are :py:class:`Atom`, must be wrapped in parentheses. 273 | 274 | Note 275 | ---- 276 | NotExpression will optimize its own inversion so that two operators will cancel one another out. For example: 277 | 278 | >>> from aiatools.attributes import disabled 279 | >>> ~disabled 280 | ~NamedAttribute('disabled') 281 | >>> ~~disabled 282 | NamedAttribute('disabled') 283 | 284 | Parameters 285 | ---------- 286 | expr : Expression 287 | The expression to negate. 288 | """ 289 | def __init__(self, expr): 290 | self.expr = ComputedAttribute(expr) if isinstance(expr, Callable) and not isinstance(expr, Expression) else expr 291 | 292 | def __call__(self, operand, *args, **kwargs): 293 | return not (self.expr(operand) if isinstance(self.expr, Expression) else self.expr) 294 | 295 | def __repr__(self): 296 | if isinstance(self.expr, Expression) and not isinstance(self.expr, (Functor, Atom)): 297 | return '~(%r)' % self.expr 298 | return '~%r' % self.expr 299 | 300 | def __invert__(self): 301 | return self.expr 302 | 303 | 304 | class Atom(Expression): 305 | """ 306 | :py:class:`Atom` represents an entity in the grammar, such as a specific component type (Button) or block type 307 | (component_set_get). Atoms cannot be modified and evaluate to themselves. 308 | """ 309 | def __eq__(self, other): 310 | if isinstance(other, Atom): 311 | return other is self 312 | elif isinstance(other, Expression): 313 | return other == self 314 | elif isinstance(other, str): 315 | return other == self() 316 | else: 317 | return False 318 | 319 | def __hash__(self): 320 | return id(self) 321 | 322 | def __ne__(self, other): 323 | if isinstance(other, Atom): 324 | return other is not self 325 | else: 326 | return other != self 327 | 328 | def __call__(self, operand, *args, **kwargs): 329 | return self 330 | 331 | 332 | class Functor(Expression): 333 | """ 334 | :py:class:`Functor` is an abstract base class that serves as the root of the class tree of classes that apply 335 | functions to entities. Unlike most expressions, these typically compute non-Boolean values that may then undergo 336 | further computation. 337 | """ 338 | def __call__(self, obj, *args, **kwargs): 339 | if needs_eval(obj): 340 | return FunctionComposition(self, obj) 341 | raise NotImplemented() 342 | 343 | 344 | class Collection(Atom): 345 | """ 346 | :py:class:`Collection` is an :py:class:`Atom` wrapping a collection, such as a list or tuple, of entities. Calls to 347 | the Collection will filter the collection given an Expression. 348 | 349 | Parameters 350 | ---------- 351 | collection : collections.Iterable[aiatools.common.Component|aiatools.common.Block] 352 | The Python collection of entities to be wrapped into the new atomic collection. 353 | """ 354 | def __init__(self, collection): 355 | self.collection = collection 356 | 357 | def __call__(self, *args, **kwargs): 358 | return Collection(list(filter(args[0], self.collection))) 359 | 360 | 361 | class FunctionComposition(Functor): 362 | """ 363 | :py:class:`ComposedAttribute` is a :py:class:`Functor` that wraps other Functors, Python functions, or lambda 364 | expressions. Functions are evaluated from left to right. 365 | 366 | >>> from aiatools import * 367 | >>> isinstance(root_block(declaration), FunctionComposition) 368 | True 369 | >>> proctest = AIAFile('test_aias/ProcedureTest2.aia') 370 | >>> proctest.blocks(is_procedure).callers(root_block(declaration)).map(fields.PROCNAME) 371 | ['i_am_called'] 372 | 373 | Parameters 374 | ---------- 375 | *args 376 | Functions to compose into a new function 377 | """ 378 | def __init__(self, *args): 379 | self.functors = list(args) + [identity] 380 | 381 | def __call__(self, obj, *args, **kwargs): 382 | if needs_eval(obj): 383 | self.functors = [obj] + self.functors 384 | return self 385 | else: 386 | for op in self.functors: 387 | obj = op(obj) 388 | return obj 389 | 390 | 391 | class ComputedAttribute(Functor): 392 | """ 393 | :py:class:`ComputedAttribute` is a :py:class:`Functor` that wraps a Python function or lambda expression. This 394 | allows for arbitrary computations to be used in evaluating entities in a project. 395 | 396 | Parameters 397 | ---------- 398 | functor : callback 399 | The functor that should be applied to the entity when the ComputedAttribute needs to be computed. Note that the 400 | return value is not memoized, so the given functor should be time efficient as possible when computing its 401 | value. 402 | """ 403 | def __init__(self, functor): 404 | self.functor = functor 405 | 406 | def __call__(self, obj, *args, **kwargs): 407 | if needs_eval(obj): 408 | return FunctionComposition(self, obj) 409 | return self.functor(obj, *args, **kwargs) 410 | 411 | def __hash__(self): 412 | return hash(self.functor) 413 | 414 | def equals(self, other): 415 | return id(self.functor) == id(other.functor) 416 | 417 | def __repr__(self): 418 | return '%s(%r)' % (self.__class__.__name__, self.functor) 419 | 420 | 421 | and_ = AndExpression 422 | or_ = OrExpression 423 | not_ = NotExpression 424 | -------------------------------------------------------------------------------- /aiatools/attributes.py: -------------------------------------------------------------------------------- 1 | # -*- mode: python; coding: utf-8; -*- 2 | # Copyright © 2017 Massachusetts Institute of Technology, All rights reserved. 3 | 4 | """ 5 | The :py:mod:`aiatools.attributes` module provides :py:class:`~aiatools.algebra.Functor` for querying App Inventor 6 | projects. 7 | 8 | .. testsetup:: 9 | 10 | from aiatools.aia import AIAFile 11 | from aiatools.attributes import * 12 | from aiatools.block_types import * 13 | from aiatools.component_types import * 14 | project = AIAFile('test_aias/LondonCholeraMap.aia') 15 | """ 16 | 17 | from aiatools.algebra import ComputedAttribute 18 | from .algebra import Functor, NotExpression 19 | from .common import Block, BlockKind, Component 20 | from .selectors import select 21 | try: 22 | from collections.abc import Callable 23 | except ImportError: 24 | from collections import Callable 25 | 26 | 27 | __author__ = 'Evan W. Patton ' 28 | 29 | 30 | class NamedAttribute(Functor): 31 | """ 32 | The :py:class:`NamedAttribute` is a :py:class:`Functor` that retrieves the value of a specific field on entities in 33 | an App Inventor project. 34 | 35 | Example 36 | _______ 37 | Retrieve any Form entities in the project: 38 | 39 | >>> project.components(NamedAttribute('type') == Form) 40 | [Screen('Screen1')] 41 | 42 | Parameters 43 | __________ 44 | name : basestring 45 | The name of the attribute to be retrieved. 46 | """ 47 | 48 | # noinspection PyShadowingNames 49 | def __init__(self, name): 50 | self.name = name 51 | 52 | def __call__(self, obj, *args, **kwargs): 53 | if hasattr(obj, self.name): 54 | return getattr(obj, self.name) 55 | return None 56 | 57 | def __hash__(self): 58 | return hash(self.name) 59 | 60 | def __eq__(self, other): 61 | if isinstance(other, NamedAttribute): 62 | return self.name == other.name 63 | else: 64 | return super(NamedAttribute, self).__eq__(other) 65 | 66 | def __repr__(self): 67 | return '%s(%s)' % (self.__class__.__name__, repr(self.name)) 68 | 69 | 70 | class NamedAttributeTuple(Functor): 71 | """ 72 | The :py:class:`NamedAttributeTuple` is a :py:class:`Functor` that retrieves the value of a field on entities in an 73 | App Inventor project. Unlike :py:class:`NamedAttribute`, it can take more than one name as a tuple. The given field 74 | names are searched in order until one is found. This is useful for presenting a single :py:class:`Functor` over 75 | synonyms, for example, ``'name'`` gives the name of a :py:class:`Component` whereas ``'component_name'`` gives the 76 | name of a block's component (if any). 77 | 78 | Parameters 79 | ---------- 80 | names : (str, unicode) 81 | The name(s) of the attribute(s) to be retrieved. Giving a 1-tuple is less efficient than defining and using the 82 | equivalent :py:class:`NamedAttribute` instance. 83 | """ 84 | def __init__(self, names): 85 | super(NamedAttributeTuple, self).__init__() 86 | self.names = names 87 | 88 | def __call__(self, obj, *args, **kwargs): 89 | for _name in self.names: 90 | if hasattr(obj, _name): 91 | return getattr(obj, _name) 92 | return None 93 | 94 | def __hash__(self): 95 | return hash(self.names) 96 | 97 | def __eq__(self, other): 98 | if isinstance(other, NamedAttributeTuple): 99 | return self.names == other.names 100 | else: 101 | return super(NamedAttributeTuple, self).__eq__(other) 102 | 103 | def __repr__(self): 104 | return '%s(%r)' % (self.__class__.__name__, self.names) 105 | 106 | 107 | def has_ancestor(target=None): 108 | """ 109 | Constructs a new ComputedAttribute that accepts an entity if and only if the entity has an ancestor that matches the 110 | given ``target`` clause. 111 | 112 | Example 113 | ------- 114 | Count the number of :py:data:`~.block_type.text` blocks with an ancestor that is a 115 | :py:data:`~.block_type.logic_compare` block. 116 | 117 | >>> project.blocks((type == text) & has_ancestor(type == logic_compare)).count() 118 | 1 119 | 120 | Parameters 121 | ---------- 122 | target : Expression 123 | An :py:class:`Expression` to use for testing ancestors. 124 | 125 | Returns 126 | ------- 127 | ComputedAttribute 128 | A new ComputedAttribute that will walk the entity graph using the ``parent`` field and test whether any 129 | ``parent`` matches ``target``. 130 | """ 131 | def checkAncestor(b): 132 | if b is None: 133 | return False 134 | b = b.parent # Skip b 135 | while b is not None: 136 | if target is None: 137 | return True 138 | elif isinstance(target, Callable) and target(b): 139 | return True 140 | elif b is target: 141 | return True 142 | b = b.parent 143 | return False 144 | return ComputedAttribute(checkAncestor) 145 | 146 | 147 | def has_descendant(target=None): 148 | """ 149 | Constructs a new ComputedAttribute that accepts an entity if and only if the entity has a descendant that matches 150 | the given ``target`` clause. 151 | 152 | Example 153 | ------- 154 | Count the number of top-level blocks that have control_if blocks as descendants. 155 | 156 | >>> project.blocks(top_level & has_descendant(type == controls_if)).count() 157 | 1 158 | 159 | Parameters 160 | ---------- 161 | target : Expression 162 | An :py:class:`Expression` to use for testing descendants. 163 | 164 | Returns 165 | ------- 166 | ComputedAttribute 167 | A new ComputedAttribute that will walk the entity graph using the ``children`` field and test whether any 168 | descendant in the subgraph matches ``target``. 169 | """ 170 | def checkDescendant(b): 171 | if b is None: 172 | return False 173 | if not hasattr(b, 'children'): 174 | return False 175 | for child in b.children(): 176 | if target is None: 177 | return True 178 | elif isinstance(target, Callable) and target(child): 179 | return True 180 | elif child is target: 181 | return True 182 | elif checkDescendant(child): 183 | return True 184 | return False 185 | return ComputedAttribute(checkDescendant) 186 | 187 | 188 | def _root_block(block): 189 | """ 190 | Looks up the root of the stack of blocks containing the given ``block``. 191 | 192 | Example 193 | ------- 194 | 195 | Parameters 196 | ---------- 197 | block : Block 198 | The block of interest for which the root block will be obtained. 199 | 200 | Returns 201 | ------- 202 | Block 203 | The block at the root of the block stack containing ``block``. 204 | """ 205 | if not block: 206 | return block 207 | while block.logical_parent: 208 | block = block.logical_parent 209 | return block 210 | 211 | 212 | root_block = ComputedAttribute(_root_block) 213 | 214 | 215 | class HeightAttribute(Functor): 216 | """ 217 | :py:class:`HeightAttribute` class is used to memoize the heights of the forest representing an App Inventor 218 | project. Use :py:data:`aiatools.attributes.height` to benefit from the memoization feature. 219 | 220 | Example 221 | ------- 222 | Get the heights of the block stacks in the project. 223 | 224 | >>> project.blocks(top_level).map(height) 225 | [2, 6] 226 | """ 227 | def __init__(self): 228 | self.precomputed = {} 229 | 230 | # noinspection PyShadowingNames 231 | def __call__(self, *args, **kwargs): 232 | block_or_component = args[0] 233 | if block_or_component in self.precomputed: 234 | return self.precomputed[block_or_component] 235 | else: 236 | try: 237 | height = max(self(x) for x in block_or_component.children()) + 1 238 | except ValueError: 239 | height = 0 240 | self.precomputed[block_or_component] = height 241 | return height 242 | 243 | 244 | class DepthAttribute(Functor): 245 | """ 246 | :py:class:`DepthAttribute` class is used to memoize the depths of entities in the forest representing an App 247 | Inventor project. Use :py:data:`aiatools.attributes.depth` to benefit from the memoization feature. 248 | 249 | Example 250 | ------- 251 | Get the depth of all leaves in the project. 252 | 253 | >>> project.blocks(leaf).map(depth) 254 | [2, 2, 4, 4, 4, 6, 5, 5, 5, 5, 5] 255 | """ 256 | def __init__(self): 257 | self.precomputed = {} 258 | 259 | @staticmethod 260 | def _get_parent(block_or_component): 261 | if hasattr(block_or_component, 'logical_parent'): 262 | return block_or_component.logical_parent 263 | else: 264 | return block_or_component.parent 265 | 266 | # noinspection PyShadowingNames 267 | def __call__(self, *args, **kwargs): 268 | block_or_component = DepthAttribute._get_parent(args[0]) 269 | depth = 0 270 | while block_or_component is not None: 271 | depth += 1 272 | block_or_component = DepthAttribute._get_parent(block_or_component) 273 | return depth 274 | 275 | 276 | class _MutationHelper(Functor): 277 | """ 278 | :py:class:`_MutatorHelper` is a helper class used for generating new :py:class:`Functor` for retrieving mutation 279 | fields on a block. :py:class:`_MutationHelper` interns the instances so that the returned items are the same 280 | instance, that is: 281 | 282 | >>> mutation.component_type is mutation.component_type 283 | True 284 | 285 | :py:class:`_MutationHelper` is accessed through the :py:data:`mutation` instance. 286 | """ 287 | _interned = {} 288 | 289 | def __init__(self, child=None): 290 | self.child = child 291 | 292 | def __call__(self, *args, **kwargs): 293 | if hasattr(args[0], 'mutation'): 294 | _mutation = args[0].mutation 295 | if _mutation is not None: 296 | if self.child is None: 297 | return _mutation 298 | elif self.child in _mutation: 299 | return _mutation[self.child] 300 | return None 301 | 302 | def __getattr__(self, item): 303 | if item not in _MutationHelper._interned: 304 | _MutationHelper._interned[item] = _MutationHelper(item) 305 | return _MutationHelper._interned[item] 306 | 307 | 308 | class _FieldHelper(Functor): 309 | """ 310 | :py:class:`_FieldHelper` is a helper class used for generating new :py:class:`Functor` for retrieving fields of 311 | blocks. :py:class:`_FieldHelper` interns the instances so that the returned items are the same instace, that is: 312 | 313 | >>> fields.OP is fields.OP 314 | True 315 | 316 | :py:class:`_FieldHelper` is accessed through the :py:data:`field` instance. 317 | """ 318 | _interned = {} 319 | 320 | def __init__(self, child=None): 321 | self.child = child 322 | 323 | def __call__(self, *args, **kwargs): 324 | if hasattr(args[0], 'fields'): 325 | _fields = args[0].fields 326 | if _fields is not None: 327 | if self.child is None: 328 | return _fields 329 | elif self.child in _fields: 330 | return _fields[self.child] 331 | return None 332 | 333 | def __getattr__(self, item): 334 | if item not in _FieldHelper._interned: 335 | _FieldHelper._interned[item] = _FieldHelper(item) 336 | return _FieldHelper._interned[item] 337 | 338 | 339 | type = NamedAttributeTuple(('type', 'component_type')) 340 | """Returns the type of the entity.""" 341 | 342 | name = NamedAttributeTuple(('name', 'instance_name')) 343 | """Returns the name of the entity.""" 344 | 345 | 346 | def _kind(block): 347 | try: 348 | return block.kind 349 | except AttributeError: 350 | return None 351 | 352 | 353 | kind = NamedAttribute("kind") 354 | """Returns the kind of the entity.""" 355 | 356 | 357 | def _compute_external(x): 358 | if isinstance(x, Component): 359 | return x.type.external 360 | elif isinstance(x, Block): 361 | if x.has_mutation('component_type'): 362 | comp_typename = x.mutation['component_type'] 363 | comp_type = x.screen.project.extensions[comp_typename] 364 | return comp_type.external if comp_type else False 365 | else: 366 | return False # Cannot determine externality of unknown object 367 | 368 | 369 | external = ComputedAttribute(_compute_external) 370 | """Returns true if the component is an extension.""" 371 | 372 | version = NamedAttribute('version') 373 | """Returns the version number for the entity.""" 374 | 375 | category = NamedAttributeTuple(('category', 'category_string')) 376 | """Returns the category for the entity.""" 377 | 378 | help_string = NamedAttribute('help_string') 379 | """Returns the help string for the entity.""" 380 | 381 | show_on_palette = NamedAttribute('show_on_palette') 382 | """Returns true if the entity is shown in the palette.""" 383 | 384 | visible = NamedAttribute('visible') 385 | """Returns true if the entity is visible.""" 386 | 387 | non_visible = NotExpression(visible) 388 | """Returns true if the entity is nonvisible.""" 389 | 390 | icon_name = NamedAttribute('iconName') 391 | """Returns the icon for the component.""" 392 | 393 | return_type = NamedAttribute('return_type') 394 | """Gets the return type of the block.""" 395 | 396 | generic = NamedAttribute('generic') 397 | """Tests whether the block is a generic component block.""" 398 | 399 | disabled = NamedAttribute('disabled') 400 | """Tests whether the entity is disabled.""" 401 | 402 | logically_disabled = NamedAttribute('logically_disabled') 403 | """Tests whether the block is logically disabled, either because it is explicitly disabled or is contained within a 404 | disabled subtree.""" 405 | 406 | logically_enabled = ~logically_disabled 407 | """Tests whether the block is logically enabled.""" 408 | 409 | enabled = NamedAttribute('Enabled') | ~disabled 410 | """Tests whether the entity is enabled.""" 411 | 412 | top_level = ComputedAttribute(lambda b: isinstance(b, Block) and b.parent is None) 413 | """Tests whether the block is at the top level.""" 414 | 415 | parent = NamedAttribute('parent') 416 | """Gets the parent of the entity.""" 417 | 418 | mutation = _MutationHelper() 419 | """ 420 | Tests whether a block has a mutation specified. One can also use :py:data:`mutation` to obtain accessors for specific 421 | mutation fields, for example: 422 | 423 | .. doctest:: 424 | 425 | >>> project.blocks(mutation.component_type == Button) 426 | [...] 427 | 428 | will retrieve all blocks that have a mutation where the component_type key is the Button type. 429 | """ 430 | 431 | fields = _FieldHelper() 432 | """ 433 | :py:data:`fields` is a generator for :py:class:`Functor` to retrieve the values of fields in a block. For example: 434 | 435 | .. doctest:: 436 | 437 | >>> project.blocks(logic_compare).map(fields.OP) 438 | ['EQ'] 439 | """ 440 | 441 | depth = DepthAttribute() 442 | """ 443 | Computes the depth of the entity with its tree. For components, this will be the number of containers from the Screen. 444 | For blocks, this will be the number of logical ancestors to the top-most block of the block stack. 445 | 446 | .. doctest:: 447 | 448 | >>> project.components(type == Marker).avg(depth) 449 | 2.0 450 | """ 451 | 452 | height = HeightAttribute() 453 | """ 454 | Computes the height of the tree from the given entity. This will be the longest path from the node to one of its 455 | children. For leaf nodes, the height is 0. 456 | 457 | .. doctest:: 458 | 459 | >>> project.blocks(top_level).avg(height) 460 | 4.0 461 | """ 462 | 463 | is_procedure = (type == 'procedures_defreturn') | (type == 'procedures_defnoreturn') 464 | """ 465 | Returns True if the type of a block is a procedure definition block, either :py:data:`procedures_defreturn` or 466 | :py:data:`procedures_defnoreturn` 467 | 468 | .. doctest:: 469 | 470 | >>> with AIAFile('test_aias/ProcedureTest.aia') as proc_project: 471 | ... proc_project.blocks(is_procedure).count() 472 | 7 473 | """ 474 | 475 | is_called = ComputedAttribute(lambda x: not select(x).callers().empty()) 476 | """ 477 | Returns True if the entity in question is called by some other block in the code. 478 | 479 | .. todo:: 480 | 481 | Add a mechanism for describing the call graph internal to components so that, for example, 482 | ``project.blocks(mutation.event_name == 'GotText').callers()`` should be non-empty for any ``Get`` method call 483 | blocks in the screen for the same ``instance_name``. 484 | """ 485 | 486 | leaf = ComputedAttribute(lambda x: len(x.children()) == 0) 487 | """ 488 | Returns True if the entity is a leaf in the tree (i.e., it has no children). 489 | 490 | .. doctest:: 491 | 492 | >>> project.blocks(leaf).count(group_by=type) 493 | {'text': 3, 'lexical_variable_get': 6, 'color_blue': 2} 494 | """ 495 | 496 | declaration = (type == 'component_event') | (type == 'global_declaration') | is_procedure 497 | """ 498 | """ 499 | 500 | statement = kind == BlockKind.STATEMENT 501 | """ 502 | """ 503 | 504 | value = kind == BlockKind.VALUE 505 | 506 | """ 507 | .. testcleanup:: 508 | 509 | project.close() 510 | """ 511 | -------------------------------------------------------------------------------- /aiatools/block_types.py: -------------------------------------------------------------------------------- 1 | # -*- mode: python; coding: utf-8; -*- 2 | # Copyright © 2017 Massachusetts Institute of Technology, All rights reserved. 3 | 4 | """ 5 | 6 | """ 7 | 8 | from .common import BlockCategory, BlockKind, BlockType 9 | 10 | DECL = BlockKind.DECLARATION 11 | STMT = BlockKind.STATEMENT 12 | VAL = BlockKind.VALUE 13 | MUT = BlockKind.MUTATION 14 | 15 | __author__ = 'Evan W. Patton ' 16 | 17 | 18 | def define_block_type(name, category, kind): 19 | globals()[name] = BlockType(name, category, kind) 20 | 21 | 22 | Control = BlockCategory('Control') 23 | Logic = BlockCategory('Logic') 24 | Math = BlockCategory('Math') 25 | Text = BlockCategory('Text') 26 | Lists = BlockCategory('Lists') 27 | Colors = BlockCategory('Colors') 28 | Variables = BlockCategory('Variables') 29 | Procedures = BlockCategory('Procedures') 30 | Dictionaries = BlockCategory('Dictionaries') 31 | # Components is plural to prevent collision with aiatools.common.Component 32 | Components = BlockCategory('Components') 33 | Voice = BlockCategory('Voice') 34 | 35 | # Control category blocks 36 | for _name, _kind in [ 37 | ('controls_if', STMT), 38 | ('controls_forRange', STMT), 39 | ('controls_forEach', STMT), 40 | ('controls_while', STMT), 41 | ('controls_choose', VAL), 42 | ('controls_do_then_return', VAL), 43 | ('controls_eval_but_ignore', STMT), 44 | ('controls_openAnotherScreen', STMT), 45 | ('controls_openAnotherScreenWithStartValue', STMT), 46 | ('controls_getStartValue', VAL), 47 | ('controls_closeScreen', STMT), 48 | ('controls_closeScreenWithValue', STMT), 49 | ('controls_closeApplication', STMT), 50 | ('controls_getPlainStartText', VAL), 51 | ('controls_closeScreenWithPlainText', STMT), 52 | ('controls_break', STMT)]: 53 | define_block_type(_name, Control, _kind) 54 | 55 | # Logic category blocks 56 | for _name in ['logic_boolean', 'logic_false', 'logic_negate', 'logic_compare', 'logic_operation', 'logic_or']: 57 | define_block_type(_name, Logic, VAL) 58 | 59 | # Math category blocks 60 | for _name in ['math_number', 'math_compare', 'math_add', 'math_subtract', 'math_multiply', 'math_division', 61 | 'math_power', 'math_bitwise', 'math_random_int', 'math_random_float', 'math_random_set_seed', 62 | 'math_on_list', 'math_single', 'math_abs', 'math_neg', 'math_round', 'math_ceiling', 'math_floor', 63 | 'math_divide', 'math_trig', 'math_cos', 'math_tan', 'math_atan2', 'math_convert_angles', 64 | 'math_format_as_decimal', 'math_is_a_number', 'math_convert_number']: 65 | define_block_type(_name, Math, STMT if _name == 'math_random_set_seed' else VAL) 66 | 67 | # Text category blocks 68 | for _name in ['text', 'text_join', 'text_length', 'text_isEmpty', 'text_compare', 'text_trim', 'text_changeCase', 69 | 'text_starts_at', 'text_contains', 'text_split', 'text_split_at_spaces', 'text_segment', 70 | 'text_replace_all', 'obfuscated_text', 'text_is_string']: 71 | define_block_type(_name, Text, VAL) 72 | 73 | # renamed block 74 | obsfucated_text = globals()['obfuscated_text'] 75 | 76 | # Lists category blocks 77 | for _name, _kind in [ 78 | ('lists_create_with', VAL), 79 | ('lists_create_with_item', VAL), 80 | ('lists_add_items', STMT), 81 | ('lists_is_in', VAL), 82 | ('lists_length', VAL), 83 | ('lists_is_empty', VAL), 84 | ('lists_pick_random_item', VAL), 85 | ('lists_position_in', VAL), 86 | ('lists_select_item', VAL), 87 | ('lists_insert_item', STMT), 88 | ('lists_replace_item', STMT), 89 | ('lists_remove_item', STMT), 90 | ('lists_append_list', STMT), 91 | ('lists_copy', VAL), 92 | ('lists_is_list', VAL), 93 | ('lists_to_csv_row', VAL), 94 | ('lists_to_csv_table', VAL), 95 | ('lists_from_csv_row', VAL), 96 | ('lists_from_csv_table', VAL), 97 | ('lists_lookup_in_pairs', VAL), 98 | ('lists_join_with_separator', VAL)]: 99 | define_block_type(_name, Lists, _kind) 100 | 101 | # Dictionaries category blocks 102 | for _name, _kind in [ 103 | ('dictionaries_create_with', VAL), 104 | ('pair', VAL), 105 | ('dictionaries_lookup', VAL), 106 | ('dictionaries_set_pair', STMT), 107 | ('dictionaries_delete_pair', STMT), 108 | ('dictionaries_recursive_lookup', VAL), 109 | ('dictionaries_recursive_set', STMT), 110 | ('dictionaries_getters', VAL), 111 | ('dictionaries_get_values', VAL), 112 | ('dictionaries_is_key_in', VAL), 113 | ('dictionaries_length', VAL), 114 | ('dictionaries_alist_to_dict', VAL), 115 | ('dictionaries_dict_to_alist', VAL), 116 | ('dictionaries_copy', VAL), 117 | ('dictionaries_combine_dicts', STMT), 118 | ('dictionaries_walk_tree', VAL), 119 | ('dictionaries_walk_all', VAL), 120 | ('dictionaries_is_dict', VAL)]: 121 | define_block_type(_name, Dictionaries, _kind) 122 | 123 | # Colors category blocks 124 | for _name in ['color_black', 'color_white', 'color_red', 'color_pink', 'color_orange', 'color_yellow', 'color_green', 125 | 'color_cyan', 'color_blue', 'color_magenta', 'color_light_gray', 'color_gray', 'color_dark_gray', 126 | 'color_make_color', 'color_split_color']: 127 | define_block_type(_name, Colors, VAL) 128 | 129 | # Variables category blocks 130 | for _name, _val in [ 131 | ('global_declaration', DECL), 132 | ('lexical_variable_get', VAL), 133 | ('lexical_variable_set', STMT), 134 | ('local_declaration_statement', STMT), 135 | ('local_declaration_expression', VAL)]: 136 | define_block_type(_name, Variables, _val) 137 | 138 | # Procedures category blocks 139 | for _name in ['procedures_defnoreturn', 'procedures_defreturn', 'procedures_callnoreturn', 'procedures_callreturn']: 140 | define_block_type(_name, Procedures, VAL if 'call' in _name else DECL) 141 | 142 | # Component category blocks 143 | for _name, _kind in [('component_event', DECL), ('component_method', MUT), ('component_set_get', MUT), 144 | ('component_component_block', VAL)]: 145 | define_block_type(_name, Components, _kind) 146 | 147 | # Voice category blocks 148 | for _name, _kind in [('voice_say', STMT), ('voice_ask', STMT), ('voice_sound', STMT), ('voice_pause', STMT), 149 | ('voice_send_to_default_token', STMT), ('voice_get_from_default_token', VAL), 150 | ('voice_send_to_app_inv', STMT), ('voice_get_from_app_inv', VAL), ('voice_lstm', VAL), 151 | ('voice_lstm_length', VAL), ('voice_get_slot_value', VAL), ('languages', VAL), 152 | ('voice_aws_model', STMT), ('detect_dominant_language', STMT), ('translate_text', STMT), 153 | ('voice_ssml', STMT)]: 154 | define_block_type(_name, Voice, _kind) 155 | -------------------------------------------------------------------------------- /aiatools/common.py: -------------------------------------------------------------------------------- 1 | # -*- mode: python; coding: utf-8; -*- 2 | # Copyright © 2017 Massachusetts Institute of Technology, All rights reserved. 3 | 4 | """ 5 | .. testsetup:: 6 | 7 | from aiatools import * 8 | project = AIAFile('test_aias/LondonCholeraMap.aia') 9 | 10 | The :py:mod:`aiatools.common` module defines the core data model that is used throughout the aiatools project. In most 11 | cases, users will not construct these objects directly but rather through use of the :py:class:`~aiatools.aia.AIAFile` 12 | class. 13 | """ 14 | 15 | import re 16 | from .algebra import Atom 17 | from functools import reduce 18 | from enum import Enum 19 | 20 | 21 | __author__ = 'Evan W. Patton ' 22 | 23 | 24 | NAMESPACE = re.compile('\\{[^}]+}') 25 | 26 | 27 | def _blockly(tag): 28 | """ 29 | Prefixes the given ``tag`` with the Blockly XML prefix. Block files saved with newer versions of App Inventor will 30 | have this XML namespace prepended when read with the ETree framework. 31 | :param tag: An XML element tag 32 | :type tag: basestring 33 | :return: A new string prefixed with the Blockly XML prefix. 34 | :rtype: str or unicode 35 | """ 36 | return '{https://developers.google.com/blockly/xml}' + tag 37 | 38 | 39 | def _html(tag): 40 | """ 41 | Prefixes the given ``tag`` with the XHTML prefix. Block files saved with newer versions of App Inventor will have 42 | this XML namespace prepended when read with the ETree framework. 43 | :param tag: An XML element tag 44 | :type tag: basestring 45 | :return: A new string prefixed with the XHTML prefix. 46 | :rtype: str or unicode 47 | """ 48 | return '{http://www.w3.org/1999/xhtml}' + tag 49 | 50 | 51 | # noinspection PyShadowingBuiltins 52 | class Block(object): 53 | _CATEGORIES = {'color', 'component', 'controls', 'global', 'lexical', 'lists', 'local', 'logic', 'math', 54 | 'obfuscated', 'dictionaries', 'procedures', 'text', 'pair', 'helpers', 'voice'} 55 | _CATEGORY_MAP = { 56 | 'color': 'Colors', 57 | 'component': 'Components', 58 | 'controls': 'Controls', 59 | 'global': 'Variables', 60 | 'lexical': 'Variables', 61 | 'lists': 'Lists', 62 | 'local': 'Variables', 63 | 'logic': 'Logic', 64 | 'math': 'Math', 65 | 'obfuscated': 'Text', 66 | 'obsfucated': 'Text', 67 | 'dictionaries': 'Dictionaries', 68 | 'pair': 'Dictionaries', 69 | 'procedures': 'Procedures', 70 | 'text': 'Text', 71 | 'helpers': 'Helpers', 72 | 'voice': 'Voice' 73 | } 74 | _ID_COUNT = 0 75 | 76 | def __init__(self, id, type): 77 | self.id = id 78 | self.type = type 79 | parts = type.split('_') 80 | if parts[0] == 'text': 81 | self.category = 'Text' 82 | else: 83 | self.category = Block._CATEGORY_MAP[parts[0]] 84 | self.parent = None 85 | self.logical_parent = None 86 | self.output = None 87 | self.inputs = {} 88 | self.ordered_inputs = [] 89 | self.fields = {} 90 | self.statements = {} 91 | self.values = {} 92 | self.next = None 93 | self.mutation = None 94 | self.x = None 95 | self.y = None 96 | self.inline = False 97 | self.comment = None 98 | self.disabled = False 99 | self.logically_disabled = False 100 | self.screen = None 101 | 102 | @classmethod 103 | def from_xml(cls, screen, xml, lang_ver, siblings=None, parent=None, connection_type=None): 104 | siblings = siblings if siblings is not None else [] 105 | attributes = xml.attrib 106 | type = attributes['type'] 107 | id = attributes['id'] if 'id' in attributes else None 108 | if id is None: 109 | id = Block._ID_COUNT 110 | Block._ID_COUNT += 1 111 | if type == 'procedures_do_then_return': 112 | type = 'controls_do_then_return' 113 | elif type == 'procedure_lexical_variable_get': 114 | type = 'lexical_variable_get' 115 | elif type == 'for_lexical_variable_get': 116 | type = 'lexical_variable_get' 117 | type_parts = type.split('_') 118 | extra_mutations = {} 119 | if type_parts[0] not in Block._CATEGORIES and (lang_ver is None or lang_ver < 17): 120 | # likely old-format blocks code with component names in block types 121 | component = screen.components(lambda x: x.name == type_parts[0]) 122 | if len(component) > 0: 123 | extra_mutations['instance_name'] = type_parts[0] 124 | extra_mutations['component_type'] = component[0].type.name 125 | if type_parts[1] == 'setproperty': 126 | # Old-style property setter 127 | type = 'component_set_get' 128 | extra_mutations['property_name'] = type_parts[1] 129 | extra_mutations['set_or_get'] = 'set' 130 | elif type_parts[1] == 'getproperty': 131 | # Old-style property getter 132 | type = 'component_set_get' 133 | extra_mutations['property_name'] = type_parts[1] 134 | extra_mutations['set_or_get'] = 'get' 135 | elif len(xml) >= 2 and xml[1].tag == 'statement' and 'name' in xml[1].attrib and \ 136 | xml[1].attrib['name'] == 'DO': 137 | # Old-style event handler 138 | type = 'component_event' 139 | extra_mutations['event_name'] = type_parts[1] 140 | else: 141 | # Old-style method call 142 | type = 'component_method' 143 | extra_mutations['method_name'] = type_parts[1] 144 | elif type_parts[0] == 'obsufcated': 145 | type_parts = ['obfuscated', 'text'] 146 | type = '_'.join(type_parts) 147 | else: 148 | raise RuntimeError('Unknown block type: %s' % type_parts[0]) 149 | block = Block(id, type) 150 | screen._blocks[id] = block 151 | block.screen = screen 152 | block.parent = parent 153 | block.logical_parent = parent 154 | siblings.append(block) 155 | if 'x' in attributes and 'y' in attributes: # Top level block 156 | block.x, block.y = attributes['x'], attributes['y'] 157 | if 'inline' in attributes: 158 | block.inline = attributes['inline'] == 'true' 159 | if 'disabled' in attributes and attributes['disabled'] == 'true': 160 | block.disabled = True 161 | block.logically_disabled = True 162 | if connection_type == 'value' or connection_type == 'statement': 163 | block.logically_disabled = block.disabled or parent.logically_disabled 164 | for child in xml: 165 | if child.tag == 'mutation' or child.tag == _html('mutation') or child.tag == _blockly('mutation'): 166 | block.mutation = dict(child.attrib) 167 | block.mutation.update(extra_mutations) 168 | extra_mutations = {} 169 | if type == 'component_all_component_block': 170 | pass 171 | elif type.startswith('component_') and ('is_generic' not in block.mutation or 172 | block.mutation['is_generic'] == 'false'): 173 | block.component = screen.components[block.mutation['instance_name']] 174 | for grandchild in child: 175 | tag = '.' + NAMESPACE.sub('', grandchild.tag) 176 | if tag not in block.mutation: 177 | block.mutation[tag] = [] 178 | block.mutation[tag].append(dict(grandchild.attrib)) 179 | elif child.tag == 'comment' or child.tag == _html('comment') or child.tag == _blockly('comment'): 180 | block.comment = child.text 181 | elif child.tag in {'field', 'title', _html('field'), _html('title'), _blockly('field'), _blockly('title')}: 182 | block.fields[child.attrib['name']] = child.text or '' 183 | elif child.tag == 'value' or child.tag == _html('value') or child.tag == _blockly('value'): 184 | block.inputs[child.attrib['name']] = block.values[child.attrib['name']] = [] 185 | child_block = Block.from_xml(screen, child[0], lang_ver, block.values[child.attrib['name']], 186 | parent=block, connection_type='value') 187 | child_block.output = block 188 | block.ordered_inputs.append(child_block) 189 | elif child.tag == 'statement' or child.tag == _html('statement') or child.tag == _blockly('statement'): 190 | block.inputs[child.attrib['name']] = block.statements[child.attrib['name']] = [] 191 | child_block = Block.from_xml(screen, child[0], lang_ver, block.statements[child.attrib['name']], 192 | parent=block, connection_type='statement') 193 | block.ordered_inputs.append(child_block) 194 | for child_block in block.statements[child.attrib['name']]: 195 | child_block.logical_parent = block 196 | elif child.tag == 'next' or child.tag == _html('next') or child.tag == _blockly('next'): 197 | child_block = Block.from_xml(screen, child[0], lang_ver, siblings=siblings, parent=block, 198 | connection_type='next') 199 | block.next = child_block 200 | if len(extra_mutations) > 0: # mutations were not consumed, so we're missing a tag 201 | block.mutation = extra_mutations 202 | return block 203 | 204 | @property 205 | def return_type(self): 206 | if self.type == 'component_method': 207 | from aiatools import component_types 208 | type = getattr(component_types, self.mutation['component_type']) 209 | assert(isinstance(type, ComponentType)) 210 | return type.methods[self.mutation['method_name']].return_type 211 | return None 212 | 213 | @property 214 | def kind(self): 215 | from aiatools import block_types 216 | type = getattr(block_types, self.type) 217 | if type.kind == BlockKind.MUTATION: 218 | if self.type == 'component_set_get': 219 | return BlockKind.VALUE if self.mutation['set_or_get'] == 'get' else BlockKind.STATEMENT 220 | elif self.type == 'component_method': 221 | return BlockKind.VALUE if self.return_type is not None else BlockKind.STATEMENT 222 | else: 223 | raise ValueError('Unknown type ' + self.type) 224 | else: 225 | return type.kind 226 | 227 | def children(self): 228 | return reduce(list.__add__, iter(self.values.values()), []) + \ 229 | reduce(list.__add__, iter(self.statements.values()), []) 230 | 231 | def has_mutation(self, name): 232 | """ 233 | Tests whether the block has a given mutation. 234 | 235 | :param str name: the name of the mutation to test 236 | :return: True if the mutation is present on the block, otherwise False 237 | :rtype: bool 238 | """ 239 | return False if self.mutation is None else (name in self.mutation) 240 | 241 | def __repr__(self): 242 | return '%s(%r, %r)' % (self.__class__.__name__, self.id, self.type) 243 | 244 | def __str__(self): 245 | description = { 246 | 'id': self.id, 247 | 'type': self.type, 248 | 'disabled': self.disabled 249 | } 250 | if self.x is not None: 251 | description['x'] = self.x 252 | description['y'] = self.y 253 | if self.comment is not None: 254 | description['comment'] = self.comment 255 | if self.mutation is not None: 256 | description['mutation'] = self.mutation 257 | if len(self.fields) > 0: 258 | description['fields'] = self.fields 259 | if len(self.values) > 0: 260 | description['values'] = self.values 261 | if len(self.statements) > 0: 262 | description['statements'] = self.statements 263 | return str(description) 264 | 265 | def _get_is_generic(self): 266 | return self.mutation and 'is_generic' in self.mutation and self.mutation['is_generic'] == 'true' 267 | 268 | def __hash__(self): 269 | return hash(self.id) * 31 + hash(self.type) 270 | 271 | generic = property(_get_is_generic, doc=""" 272 | True if the block is a generic component block (getter, setter, or method call), otherwise False. 273 | 274 | :type: bool 275 | """) 276 | # statements = property(_get_statements, doc=""" 277 | # Access the statements of a block with continuation in an iterable fashion. 278 | # 279 | # :type: list[:class:`Block`] 280 | # """) 281 | 282 | 283 | class BlockKind(Atom, Enum): 284 | DECLARATION = 1 285 | STATEMENT = 2 286 | VALUE = 3 287 | MUTATION = 4 288 | 289 | 290 | class BlockType(Atom): 291 | def __init__(self, name, category, kind): 292 | """ 293 | 294 | :param name: 295 | :type name: str 296 | :param category: 297 | :type category: BlockCategory 298 | :param kind: 299 | :type kind: BlockKind 300 | """ 301 | self.name = name 302 | self.category = category 303 | self.kind = kind 304 | category.add_type(self) 305 | 306 | def __repr__(self): 307 | return 'BlockType(%r, %r, %r)' % (self.name, self.category, self.kind) 308 | 309 | def __call__(self, *args, **kwargs): 310 | return self.name 311 | 312 | 313 | class BlockCategory(Atom): 314 | def __init__(self, name): 315 | self.name = name 316 | self.blocks = {} 317 | 318 | def __repr__(self): 319 | return 'aiatools.block_types.%s' % self.name 320 | 321 | def __call__(self, *args, **kwargs): 322 | return self.name 323 | 324 | def add_type(self, block_type): 325 | """ 326 | 327 | :param block_type: 328 | :type block_type: BlockType 329 | """ 330 | self.blocks[block_type.name] = block_type 331 | 332 | 333 | class Method(Atom): 334 | # noinspection PyPep8Naming 335 | def __init__(self, name, description, deprecated, params, returnType=None, continuation=False): 336 | self.name = name 337 | self.description = description 338 | if isinstance(deprecated, str): 339 | self.deprecated = deprecated == 'true' 340 | else: 341 | self.deprecated = deprecated 342 | self.continuation = continuation == 'true' 343 | self.params = [param if isinstance(param, Parameter) else Parameter(**param) for param in params] 344 | self.return_type = returnType 345 | 346 | def __call__(self, *args, **kwargs): 347 | return self.name 348 | 349 | def __repr__(self): 350 | return 'Method(%s, %s, %s, %s)' % \ 351 | (repr(self.name), repr(self.description), repr(self.deprecated), repr(self.params)) 352 | 353 | 354 | class Property(Atom): 355 | # noinspection PyPep8Naming 356 | def __init__(self, name, editorType=None, defaultValue=None, description=None, type=None, rw=None, 357 | deprecated=False, editorArgs=None, alwaysSend=False, category=None, helper=None): 358 | self.name = name 359 | self.type = type 360 | self.editor_type = editorType 361 | self.default_value = defaultValue 362 | self.category = category 363 | self.description = description 364 | self.helper = helper 365 | self.rw = rw 366 | if isinstance(deprecated, str): 367 | self.deprecated = deprecated == 'true' 368 | else: 369 | self.deprecated = deprecated 370 | self.editor_args = editorArgs 371 | self.always_send = alwaysSend 372 | 373 | def __call__(self, component, *args, **kwargs): 374 | return component.properties[self.name] 375 | 376 | def __repr__(self): 377 | return 'Property(%r, %r, %r, %r, %r, %r, %r)' % \ 378 | (self.name, self.editor_type, self.default_value, self.description, self.type, self.rw, self.deprecated) 379 | 380 | 381 | class Event(Atom): 382 | def __init__(self, name, description, deprecated, params): 383 | self.name = name 384 | self.description = description 385 | if isinstance(deprecated, str): 386 | self.deprecated = deprecated == 'true' 387 | else: 388 | self.deprecated = deprecated 389 | self.params = [param if isinstance(param, Parameter) else Parameter(**param) for param in params] 390 | 391 | def __call__(self, *args, **kwargs): 392 | return self.name 393 | 394 | def __repr__(self): 395 | return 'Event(%r, %r, %r, %r)' % (self.name, self.description, self.deprecated, self.params) 396 | 397 | 398 | class Parameter(Atom): 399 | def __init__(self, name, type, helper=None): 400 | self.name = name 401 | self.type = type 402 | self.helper = helper 403 | 404 | def __call__(self, *args, **kwargs): 405 | return self.name 406 | 407 | def __repr__(self): 408 | return 'Parameter(%r, %r)' % (self.name, self.type) 409 | 410 | 411 | class RecursiveIterator(object): 412 | def __init__(self, container, order='breadth', test=None, skip=None): 413 | self.stack = [container] 414 | self.order = order 415 | self.test = test 416 | self.skip = skip 417 | 418 | def __iter__(self): 419 | while len(self.stack) > 0: 420 | item = self.stack.pop(0) 421 | failed = self.test and not self.test(item) 422 | if self.skip and failed: 423 | continue 424 | if self.order == 'breadth': 425 | self.stack.extend(item.children()) 426 | elif self.order == 'depth': 427 | self.stack = list(filter(lambda x: x, [child if child != item else None for child in item.children()])) + \ 428 | self.stack 429 | else: 430 | raise NotImplementedError(f'Recursive order {self.order} is unknown.') 431 | if not failed: 432 | yield item 433 | 434 | 435 | class ComponentType(Atom): 436 | # noinspection PyShadowingBuiltins 437 | def __init__(self, name, methods=None, events=None, properties=None): 438 | self.name = name 439 | self.type = None 440 | self.external = False 441 | self.version = 1 442 | self.category_string = None 443 | self.help_string = None 444 | self.show_on_palette = True 445 | self.visible = False 446 | self.icon_name = None 447 | self.methods = None 448 | self.events = None 449 | self.properties = None 450 | if methods is not None: 451 | self.methods = {name: Method(**m) if isinstance(m, dict) else m for name, m in methods.items()} 452 | for name, method in self.methods.items(): 453 | setattr(self, method.name, method) 454 | if events is not None: 455 | self.events = {name: Event(**e) if isinstance(e, dict) else e for name, e in events.items()} 456 | for name, event in self.events.items(): 457 | setattr(self, event.name, event) 458 | if properties is not None: 459 | self.properties = {name: Property(**p) if isinstance(p, dict) else p for name, p in properties.items()} 460 | for name, property in self.properties.items(): 461 | setattr(self, property.name, property) 462 | 463 | def __call__(self, *args, **kwargs): 464 | return self.name 465 | 466 | def __repr__(self): 467 | return 'aiatools.component_types.%s' % self.name 468 | 469 | def __str__(self): 470 | return self.name 471 | 472 | def __hash__(self): 473 | return hash(self.name) 474 | 475 | def __eq__(self, other): 476 | if isinstance(other, ComponentType): 477 | if self.type is None or other.type is None: 478 | return self.name == other.name 479 | else: 480 | return self.type == other.type 481 | else: 482 | return super(ComponentType, self).__eq__(other) 483 | 484 | 485 | class Extension(ComponentType): 486 | def __init__(self, *args, **kwargs): 487 | super(Extension, self).__init__(*args, **kwargs) 488 | self.external = True 489 | self.project = None 490 | 491 | def __repr__(self): 492 | return '' % self.type 493 | 494 | 495 | class Component(object): 496 | _DISALLOWED_KEYS = {'$Components', '$Name', '$Type', '$Version', 'Uuid'} 497 | TYPES = {} 498 | 499 | def __init__(self, parent, uuid, type, name, version, properties=None): 500 | """ 501 | 502 | :param parent: 503 | :type parent: ComponentContainer|None 504 | :param uuid: 505 | :param type: 506 | :param name: 507 | :param version: 508 | :param properties: 509 | """ 510 | self.id = uuid 511 | self.parent = parent 512 | self.uuid = uuid 513 | self.type = type 514 | self.name = name 515 | self.version = version 516 | self.properties = properties 517 | self.path = '%s/%s' % (parent.name, self.name) if parent is not None else self.name 518 | 519 | def __repr__(self): 520 | # return '%s(%r, %r, %r, %r, %r, %r)' % (self.__class__.__name__, self.parent, self.uuid, self.type, self.name, 521 | # self.version, self.properties) 522 | return '%s(%r, %r)' % (self.type, self.uuid, self.name) 523 | 524 | def children(self): 525 | return [] 526 | 527 | @classmethod 528 | def from_json(cls, parent, json_repr): 529 | typename = json_repr['$Type'] 530 | type = Component.TYPES[typename] if typename in Component.TYPES else Extension(typename) 531 | properties = {k: v for k, v in json_repr.items() if k not in Component._DISALLOWED_KEYS} 532 | return cls(parent, json_repr['Uuid'], type, json_repr['$Name'], json_repr['$Version'], properties) 533 | 534 | 535 | class FilterableDict(dict): 536 | def filter(self, rule): 537 | if rule is None: 538 | return self 539 | else: 540 | return FilterableDict({k: v for k, v in self.items() if rule(k, v)}) 541 | -------------------------------------------------------------------------------- /aiatools/component_types.py: -------------------------------------------------------------------------------- 1 | # -*- mode: python; coding: utf-8 -*- 2 | # Copyright © 2017 Massachusetts Institute of Technology, All rights reserved. 3 | 4 | """ 5 | The :py:mod:`aiatools.component_types` module defines the components for App Inventor. Component types are 6 | programmatically constructed during module compilation from the simple_components.json used to populate App Inventor's 7 | online development environment. 8 | """ 9 | 10 | import json 11 | import xml.etree.ElementTree as ETree 12 | 13 | import pkg_resources 14 | 15 | from aiatools.selectors import Selector, NamedCollection, Selectors 16 | from .common import * 17 | 18 | __author__ = 'Evan W. Patton ' 19 | 20 | 21 | # noinspection PyShadowingBuiltins 22 | class ComponentContainer(Component, Selectors): 23 | """ 24 | :py:class:`ComponentContainer` models the App Inventor ComponentContainer class. 25 | 26 | Parameters: 27 | parent (ComponentContainer, optional): The parent of the container. May be None for :py:class:`.Screen`. 28 | uuid (basestring): The UUID for the component container. 29 | type (str): The type of the component container. 30 | name (basestring): The name of the component container. 31 | version (str): The version number of the component container's type at the time the project was last saved. 32 | properties (dict[basestring, T], optional): The properties of the component as a dictionary. The values are 33 | dependent on the key (property). 34 | components (list[Component], optional): A list of components contained by the container. 35 | """ 36 | def __init__(self, parent, uuid, type, name, version, properties=None, components=None): 37 | super(ComponentContainer, self).__init__(parent, uuid, type, name, version, properties) 38 | self._children = [] if components is None else list(components) 39 | 40 | def __iter__(self): 41 | """ 42 | Iterate over the children of the container. 43 | 44 | :return: An iterator over the container's children. 45 | :rtype: collections.Iterable[Component] 46 | """ 47 | return iter(self._children) 48 | 49 | def itervalues(self): 50 | """ 51 | Iterate over the values of this container. For :py:class:`ComponentContainer`, the values are the children 52 | of the container. 53 | 54 | :return: Iterator over the container's children. 55 | :rtype: collections.Iterable[Component] 56 | """ 57 | return iter(self) 58 | 59 | def children(self): 60 | """ 61 | Iterate over the child components in the container 62 | 63 | :return: An iterator over the components in the container. 64 | :rtype: collections.Iterable[Component] 65 | 66 | .. versionadded:: 0.1 67 | """ 68 | return self._children 69 | 70 | def _components(self, *args, **kwargs): 71 | items = {item.id: item for item in RecursiveIterator(self)} 72 | items[self.id] = self 73 | return Selector(NamedCollection(items)).components(*args, **kwargs) 74 | 75 | @classmethod 76 | def from_json(cls, parent, json_repr): 77 | """ 78 | Constructs a :py:class:`ComponentContainer` 79 | 80 | :param parent: The parent container of the new container. 81 | :type parent: ComponentContainer 82 | :param json_repr: The JSON representation of the component from the Screen definition. 83 | :type json_repr: dict[str, T] 84 | :return: A newly constructed ComponentContainer from the JSON representation. 85 | :rtype: ComponentContainer 86 | """ 87 | typename = json_repr['$Type'] 88 | if typename in globals(): 89 | type = globals()[typename] 90 | else: 91 | type = Extension(typename) 92 | properties = {k: v for k, v in json_repr.items() if k not in Component._DISALLOWED_KEYS} 93 | container = cls(parent, json_repr['Uuid'], type, json_repr['$Name'], json_repr['$Version'], properties) 94 | for component_description in json_repr['$Components']: 95 | if '$Components' in component_description: 96 | child = ComponentContainer.from_json(container, component_description) 97 | else: 98 | child = Component.from_json(container, component_description) 99 | container._children.append(child) 100 | return container 101 | 102 | components = property(_components, doc=""" 103 | Returns a :py:class:`~aiatools.selectors.Selector` over the components in the container. 104 | 105 | :type: Selector[Component] 106 | """) 107 | 108 | 109 | class DesignerRoot(ComponentContainer): 110 | """ 111 | The :py:class:`Screen` class provides a Python representation of an App Inventor Screen. 112 | 113 | The Screen object encapsulates both its descendant components and the blocks code prescribing the behavior of the 114 | Screen's contents. 115 | 116 | Parameters 117 | ---------- 118 | name : basestring, optional 119 | The name of the screen. 120 | components : list[Component], optional 121 | A list of immediate components that are the children of the screen. 122 | design : string | file, optional 123 | A pathname, string, or file-like that contains a Screen's Scheme (.scm) file. 124 | blocks : string | file, optional 125 | A pathname, string, or file-like that contains a Screen's Blocks (.bky) file. 126 | """ 127 | def __init__(self, name=None, components=None, design=None, blocks=None, project=None, root_type=None): 128 | self.uuid = 0 129 | self.name = name 130 | self.path = name 131 | self._children = components 132 | self._blocks = NamedCollection() 133 | self.ya_version = None 134 | self.blocks_version = None 135 | self.project = project 136 | if design is not None: 137 | form_json = None 138 | if isinstance(design, str): 139 | form_json = json.loads(design) 140 | else: 141 | form_contents = [line.decode('utf-8') if hasattr(line, 'decode') else line for line in design.readlines()] 142 | if len(form_contents) > 2: 143 | if form_contents[1] != '$JSON\n' and form_contents[1] != b'$JSON\n': 144 | raise RuntimeError('Unknown Screen format: %s' % form_contents[1]) 145 | form_json = json.loads(form_contents[2]) 146 | elif len(form_contents) == 1: # Alexa support 147 | form_json = json.loads(form_contents[0]) 148 | 149 | self.name = name or (form_json is not None and form_json['Properties']['$Name']) 150 | super(DesignerRoot, self).__init__(parent=None, 151 | uuid=('0' if form_json is None else form_json['Properties']['Uuid']), 152 | type=root_type, 153 | name=self.name, 154 | version=(None if form_json is None else form_json['Properties']['$Version']), 155 | components=components) 156 | if form_json: 157 | self._process_components_json(form_json['Properties']['$Components'] 158 | if '$Components' in form_json['Properties'] else []) 159 | self.properties = { 160 | key: value for key, value in form_json.items() if key not in Component._DISALLOWED_KEYS 161 | } 162 | self.ya_version = int(form_json['AlexaVersion'] if 'AlexaVersion' in form_json else form_json['YaVersion']) 163 | else: 164 | self.properties = {} 165 | self.ya_version = None 166 | else: 167 | super(DesignerRoot, self).__init__(None, '0', root_type, name or 'Screen1', '20', components=components) 168 | self.id = self.name 169 | if isinstance(blocks, str): 170 | blocks_content = blocks 171 | else: 172 | blocks_content = None if blocks is None else blocks.read() 173 | blocks_content = blocks_content if blocks_content and len(blocks_content) > 0 else None 174 | xml_root = None if blocks_content is None else ETree.fromstring(blocks_content) 175 | if xml_root is not None: 176 | for child in xml_root: 177 | if child.tag.endswith('yacodeblocks'): 178 | self.blocks_version = int(child.attrib['language-version']) 179 | self.ya_version = max(self.ya_version, int(child.attrib['ya-version'])) 180 | else: 181 | block = Block.from_xml(self, child, self.blocks_version) 182 | self._blocks[block.id] = block 183 | self.blocks = Selector(self._blocks) 184 | 185 | def _process_components_json(self, components): 186 | self._children = [] 187 | for component_description in components: 188 | if '$Components' in component_description: # component container 189 | component = ComponentContainer.from_json(self, component_description) 190 | else: 191 | component = Component.from_json(self, component_description) 192 | self._children.append(component) 193 | 194 | def __iter__(self): 195 | for child in RecursiveIterator(self): 196 | yield child 197 | for child in self._blocks.values(): 198 | yield child 199 | 200 | def __str__(self): 201 | return self.name 202 | 203 | 204 | class Screen(DesignerRoot): 205 | def __init__(self, name=None, components=None, design=None, blocks=None, project=None): 206 | super(Screen, self).__init__(name, components, design, blocks, project, Form) 207 | 208 | def __repr__(self): 209 | return "Screen(%s)" % repr(self.name) 210 | 211 | 212 | class Skill(DesignerRoot): 213 | def __init__(self, name=None, components=None, design=None, blocks=None, project=None): 214 | super(Skill, self).__init__(name, components, design, blocks, project, Alexa) 215 | 216 | def __repr__(self): 217 | return "Skill(%s)" % repr(self.name) 218 | 219 | 220 | def list_to_dict(iterable, key='name'): 221 | return {i[key]: i for i in iterable} 222 | 223 | 224 | container_types = { 225 | 'Canvas', 226 | 'Chart', 227 | 'FeatureCollection', 228 | 'Form', 229 | 'Map', 230 | 'HorizontalArrangement', 231 | 'HorizontalScrollArrangement', 232 | 'TableArrangement' 233 | 'VerticalArrangement', 234 | 'VerticalScrollArrangement' 235 | } 236 | 237 | 238 | def component_from_descriptor(descriptor: dict) -> ComponentType: 239 | methods = list_to_dict(descriptor['methods']) 240 | events = list_to_dict(descriptor['events']) 241 | properties = list_to_dict(descriptor['properties']) 242 | for prop in descriptor['blockProperties']: 243 | if prop['name'] in properties: 244 | properties[prop['name']].update(prop) 245 | else: 246 | properties[prop['name']] = prop 247 | prop['editorType'] = None 248 | prop['defaultValue'] = None 249 | external = descriptor['external'] == 'true' 250 | cls = Extension if external else ComponentType 251 | component = cls(descriptor['name'], methods, events, properties) 252 | component.type = descriptor['type'] 253 | component.external = external 254 | component.version = int(descriptor['version']) 255 | component.category_string = descriptor['categoryString'] 256 | component.help_string = descriptor['helpString'] 257 | component.show_on_palette = bool(descriptor['showOnPalette']) 258 | component.visible = not bool(descriptor['nonVisible']) 259 | component.icon_name = descriptor['iconName'] 260 | return component 261 | 262 | 263 | def _load_component_types(filename, use_type=False): 264 | """ 265 | Loads the descriptions of App Inventor components from simple_components.json and populates the module with 266 | instances of ComponentType for each known type. 267 | """ 268 | with open(pkg_resources.resource_filename('aiatools', filename)) as _f: 269 | _component_descriptors = json.load(_f) 270 | for _descriptor in _component_descriptors: 271 | _component = component_from_descriptor(_descriptor) 272 | name = _component.type if use_type else _component.name 273 | globals()[name] = _component 274 | Component.TYPES[name] = _component 275 | 276 | 277 | _load_component_types('simple_components.json') 278 | _load_component_types('alexa-devices.json', use_type=True) 279 | -------------------------------------------------------------------------------- /aiatools/selectors.py: -------------------------------------------------------------------------------- 1 | # -*- mode: python; coding: utf-8; -*- 2 | # Copyright © 2017 Massachusetts Institute of Technology, All rights reserved. 3 | 4 | """ 5 | The :py:mod:`aiatools.selectors` modules provides high level selection and aggregation operations that can be executed 6 | over a project. 7 | 8 | .. testsetup: 9 | from aiatools import * 10 | project = AIAFile('test_aias/LondonCholeraMap.aia') 11 | """ 12 | 13 | 14 | from aiatools.algebra import Expression, AndExpression, identity 15 | from aiatools.common import Block, Component, ComponentType, FilterableDict, RecursiveIterator 16 | from aiatools.block_types import procedures_defnoreturn, procedures_defreturn, procedures_callnoreturn, \ 17 | procedures_callreturn 18 | from functools import reduce 19 | 20 | __author__ = 'Evan W. Patton ' 21 | 22 | 23 | class AggregateOperations: 24 | """ 25 | :py:class:`AggregateOptions` is a mixin class that provides functionality for aggregation operations on selections. 26 | """ 27 | 28 | # noinspection PyTypeChecker 29 | def count(self, group_by=None): 30 | """ 31 | Counts the number of entities in a collection, optionally grouping by the ``group_by`` argument. ``group_by`` 32 | will be called each element in the collection. 33 | 34 | .. doctest:: 35 | 36 | >>> project.components().count() 37 | 4 38 | >>> project.blocks(top_level).count(group_by=type) 39 | {'component_event': 2} 40 | >>> project.blocks().count(group_by=(type, mutation.component_type)) 41 | {('component_event', 'Button'): 1, ('component_method', 'Map'): 2, ('component_event', 'Map'): 1, ('component_set_get', 'Marker'): 4} 42 | 43 | :param group_by: If given, a :py:class:`~aiatools.algebra.Functor` or tuple thereof, the value(s) of which 44 | will be used to group the entities in the collection for counting. 45 | :type group_by: tuple[aiatools.algebra.Functor or callable] or aiatools.algebra.Functor or callable or None 46 | :return: The count of the number of entities or a dictionary mapping unique ``group_by`` values to counts. 47 | :rtype: int or dict[(Atom, str) or Atom or str, int] 48 | """ 49 | if group_by is None: 50 | result = 0 51 | for _ in self: 52 | result += 1 53 | return result 54 | elif not isinstance(group_by, tuple): 55 | group_by = (group_by,) 56 | result = FilterableDict() 57 | for value in self: 58 | attr = tuple(x(value) for x in group_by) 59 | if None not in attr: 60 | if len(attr) == 1: 61 | attr = attr[0] 62 | if attr in result: 63 | result[attr] += 1 64 | else: 65 | result[attr] = 1 66 | return result 67 | 68 | def avg(self, func, group_by=None): 69 | """ 70 | Averages the values of ``func`` applied to the collection, optionally grouping by the ``group_by`` argument. 71 | ``group_by`` will be called on each element in the collection. 72 | 73 | .. doctest:: 74 | 75 | >>> project.blocks(top_level).avg(height) 76 | 4.0 77 | >>> project.blocks().avg(depth) 78 | 3.347826086956522 79 | 80 | :param func: The function to apply to the entities in the collection for which the average will be computed. 81 | :type func: aiatools.algebra.Functor or callable 82 | :param group_by: If given, a :py:class`~aiatools.algebra.Functor` or tuple thereof, the value(s) of which will 83 | be used to group the entities in the collection for averaging. 84 | :type group_by: tuple[aiatools.algebra.Functor or callable] or aiatools.algebra.Functor or callable or None 85 | :return: The average of the values of ``func`` applied to the entities of the collection or a dictionary 86 | mapping the value(s) of ``group_by`` applied to the entities to the average of ``func`` applied to the 87 | entities in the subset identified by the dictionary key. 88 | :rtype: int or dict[(Atom, str) or Atom or str, int] 89 | """ 90 | if group_by is None: 91 | results = list(map(func, self)) 92 | return sum(results) / len(results) 93 | elif not isinstance(group_by, tuple): 94 | group_by = (group_by,) 95 | state = {} 96 | for value in self: 97 | attr = tuple(x(value) for x in group_by) 98 | if None not in attr: 99 | if len(attr) == 1: 100 | attr = attr[0] 101 | if attr in state: 102 | state[attr]['sum'] += func(value) 103 | state[attr]['count'] += 1 104 | else: 105 | state[attr] = {'sum': func(value), 'count': 1} 106 | return FilterableDict({k: v['sum'] / v['count'] for k, v in state}) 107 | 108 | def max(self, func, group_by=None): 109 | """ 110 | Obtains the maximum value of ``func`` applied to the collection, optionally grouping by the ``group_by`` 111 | argument. ``group_by`` will be called on each element in the collection. 112 | 113 | .. doctest:: 114 | 115 | >>> project.blocks(type == component_event).max(height) 116 | 6 117 | 118 | :param func: The function to apply to the entities in the collection for which the maximum will be computed. 119 | :type func: aiatools.algebra.Functor or callable 120 | :param group_by: If given, a :py:class:`~aiatools.algebra.Functor` or tuple thereof, the value(s) of which will 121 | be used to group the entities in the collection for determining the maximum. 122 | :type group_by: 123 | :return: The maximum of the values of ``func`` applied to the entities of the collection or a dictionary 124 | mapping the value(s) of ``group_by`` applied to the entities to the maximum of ``func`` applied to the 125 | entities in the subset identified by the dictionary key. 126 | :rtype: int or dict[(Atom, str) or Atom or str, int] 127 | """ 128 | if group_by is None: 129 | return max(list(map(func, self))) 130 | elif not isinstance(group_by, tuple): 131 | group_by = (group_by,) 132 | result = FilterableDict() 133 | for value in self: 134 | attr = tuple(x(value) for x in group_by) 135 | if None not in attr: 136 | if len(attr) == 1: 137 | attr = attr[0] 138 | attr_value = func(value) 139 | if (attr in result and result[attr] < attr_value) or attr not in result: 140 | result[attr] = attr_value 141 | return result 142 | 143 | def min(self, func, group_by=None): 144 | """ 145 | Obtains the minimum value of ``func`` applied to the collection, optionally grouping by the ``group_by`` 146 | argument. ``group_by`` will be called on each element in the collection. 147 | 148 | .. doctest:: 149 | 150 | >>> project.blocks(type == component_event).min(height) 151 | 2 152 | >>> project.blocks(category == Components).min(height, group_by=type) 153 | {'component_event': 2, 'component_method': 1, 'component_set_get': 1} 154 | >>> project.blocks().min(height, group_by=(type, mutation.component_type)) 155 | {('component_event', 'Button'): 2, ('component_method', 'Map'): 1, ('component_event', 'Map'): 6, ('component_set_get', 'Marker'): 1} 156 | 157 | :param func: The function to apply to the enetities in the collection for which the minimum will be computed. 158 | :type func: aiatools.algebra.Functor or callable 159 | :param group_by: If given, a :py:class:`~aiatools.algebra.Functor` or tuple thereof, the value(s) of which will 160 | be used to group the entities in the collection determining the minimum. 161 | :type group_by: tuple[aiatools. 162 | :return: The minimum of the values of ``func`` applied to the entities of the collection or a dictionary 163 | mapping the value(s) of ``group_by`` applied to the entities to the minimum of ``func`` applied to the 164 | entities in the subset identified by the dictionary key. 165 | :rtype: int or dict[(Atom, str) or Atom or str, int] 166 | """ 167 | if group_by is None: 168 | return min(list(map(func, self))) 169 | elif not isinstance(group_by, tuple): 170 | group_by = (group_by,) 171 | result = FilterableDict() 172 | for value in self: 173 | attr = tuple(x(value) for x in group_by) 174 | if None not in attr: 175 | if len(attr) == 1: 176 | attr = attr[0] 177 | attr_value = func(value) 178 | if (attr in result and result[attr] > attr_value) or attr not in result: 179 | result[attr] = attr_value 180 | return result 181 | 182 | def empty(self): 183 | """ 184 | Tests whether the result of the selector chains up to and including the current selection is empty. 185 | 186 | .. doctest:: 187 | 188 | >>> project.components(type == Voting).empty() 189 | True 190 | >>> project.components(type == Button).empty() 191 | False 192 | 193 | :return: ``True`` if the selection is empty, otherwise ``False``. 194 | :rtype: bool 195 | """ 196 | for _ in self: 197 | return False 198 | return True 199 | 200 | def __iter__(self): 201 | raise NotImplemented() 202 | 203 | 204 | class Selectors: 205 | """ 206 | :py:class:`Selectors` is a mixin class for collections that enables selecting entities related to entities in the 207 | collection. 208 | """ 209 | def __init__(self, *args): 210 | pass 211 | 212 | def screens(self, test=None, *args): 213 | """ 214 | Select the screens containing the elements in the collection. 215 | 216 | Returns 217 | ------- 218 | Selector[Screen] 219 | A selector over the screens containing the entities in the collection. 220 | 221 | Todo 222 | ---- 223 | - (ewpatton) Implement subset selection on screens 224 | """ 225 | test = identity if test is None else test 226 | screens = {item.id: item for item in self 227 | if ((isinstance(item.type, ComponentType) and item.type.name == 'Form') or item.type == 'Form') 228 | and test(item)} 229 | block_screens = {item.screen.id: item.screen for item in self if (isinstance(item, Block) and test(item))} 230 | screens.update(block_screens) 231 | return Selector(screens) 232 | 233 | def components(self, test=None, *args): 234 | """ 235 | Select the subset of entities in the current selection that are :py:class:`Component`. 236 | 237 | Returns 238 | ------- 239 | Selector[Component] 240 | A selector for further selection of entities. The selector will only contain :py:class:`Component`. 241 | 242 | Todo 243 | ---- 244 | - (ewpatton) Implement subset selection on components 245 | """ 246 | test = identity if test is None else test 247 | return Selector({item.id: item for item in self if (isinstance(item, Component) and test(item))}) 248 | 249 | def blocks(self, test=None, *args): 250 | """ 251 | Select blocks under the current node. The following filters can be applied: 252 | 253 | Returns 254 | ------- 255 | Selector[Block] 256 | A selector for further selection of entities. The selector will only contain :py:class:`Block`. 257 | 258 | Todo 259 | ---- 260 | - (ewpatton) Implement subset selection on blocks. 261 | """ 262 | test = identity if test is None else test 263 | return Selector({item.id: item for item in self if (isinstance(item, Block) and test(item))}) 264 | 265 | def callers(self, *args): 266 | """ 267 | Select any blocks that result in a call to any callable blocks in the selection. 268 | 269 | Note 270 | ---- 271 | Only operates on procedures at this time. See TODO about adding support for components. 272 | 273 | Returns 274 | ------- 275 | Selector[Block] 276 | A selector over the callers (if any) of the callable blocks contained in the collection. The selector will 277 | only contain :py:class:`Block`. 278 | 279 | Todo 280 | ---- 281 | - (ewpatton) Implement subset selection on the blocks 282 | - (ewpatton) Add call graph so that component method/event blocks can be included 283 | """ 284 | _filter = args[0] if len(args) > 0 else lambda x: True 285 | return Selector({block.id: block for item in self 286 | if item.type in (procedures_defreturn(), procedures_defnoreturn()) 287 | for block in item.screen.blocks 288 | if block.type in (procedures_callnoreturn(), procedures_callreturn()) and 289 | block.fields['PROCNAME'] == item.fields['NAME'] and _filter(block)}) 290 | 291 | def callees(self, *args, **kwargs): 292 | """ 293 | Select procedure definition blocks (if any) that are called by the procedure call blocks in the collection. 294 | 295 | Note 296 | ---- 297 | Only operates on procedures at this time. See TODO about adding support for components. 298 | 299 | Returns 300 | ------- 301 | Selector[Block] 302 | A selector over the callees (if any) of the caller blocks contained in the collection. The selector will 303 | only container :py:class:`Block`. 304 | 305 | Todo 306 | ---- 307 | - (ewpatton) Implement subset selection on the blocks 308 | - (ewpatton) Add call graph so that component method/event blocks can be included 309 | """ 310 | pass 311 | 312 | def branch(self, branch_id): 313 | """ 314 | Retrieve 315 | 316 | Parameters 317 | ---------- 318 | branch_id : int 319 | Retrieve the __branch_id__th branch of any blocks in this collection that have a statement input. This can 320 | be used to walk an if-elseif-else block, for example. 321 | 322 | Todo 323 | ---- 324 | - (ewpatton) Implementation 325 | """ 326 | pass 327 | 328 | def map(self, functor): 329 | """ 330 | Applies ``functor`` to the entities in the selection. 331 | 332 | Parameters 333 | ---------- 334 | functor : callable 335 | 336 | Returns 337 | ------- 338 | list 339 | A list in the value space of ``functor``. 340 | """ 341 | return [functor(x) for x in self if functor(x) is not None] 342 | 343 | def select(self, selector): 344 | """ 345 | Selects a subset of the entities in the selection for which ``selector(item)`` is True. 346 | 347 | Parameters 348 | ---------- 349 | selector : Expression 350 | The expression to be applied to filter the current selection. 351 | 352 | Returns 353 | ------- 354 | Selector[T] 355 | The subset of the selection. The exact contents of the subset depends on the type of content of the 356 | current selection. 357 | """ 358 | return Selector({item.id: selector(item) for item in self if selector(item) is not None}) 359 | 360 | def descendants(self, test=None, order='natural', skip_failures=False): 361 | """ 362 | Selects all of the descendants of the entities in the current selection. 363 | 364 | Parameters 365 | ---------- 366 | test : callable 367 | An optional test used to filter out items from the iteration. Default: None 368 | 369 | order : str 370 | The order of iteration. Options are 'natural', 'breadth', or 'depth'. Default: 'natural' 371 | 372 | skip_failures : bool 373 | If skip_failures is true and test is provided but fails for an element, the subtree starting at the element 374 | is pruned. 375 | 376 | Returns 377 | ------- 378 | Selector[T] 379 | The descendants of the entities in the current selection, if any. 380 | """ 381 | def order_type(m): 382 | return order if order != 'natural' else ('depth' if isinstance(m, Block) else 'breadth') 383 | return Selector({obj.id: obj if not test or test(obj) else None for match in self 384 | for obj in RecursiveIterator(match, order_type(match), test, skip_failures)}) 385 | 386 | def __iter__(self): 387 | raise NotImplemented() 388 | 389 | 390 | class Selector(AggregateOperations, Selectors): 391 | """ 392 | :py:class:`Selector` provides a lazily computed application of an expression over a collection. Items in the 393 | underlying collection can be accessed in three ways: 394 | 395 | 1. Through use of an iterable. :py:class:`Selector` is an iterable, and iterating over it yields its values. There 396 | is also :py:meth:`iteritems`, which iterates over key, value pairs in the collection similar to how the 397 | iteritems method in :py:obj:`dict` works. 398 | 2. Through an identifier/key. For example, accessing the Component called Button1 can be done with 399 | ``selection['Button1']``. 400 | 3. Through an index. For example, if ``children`` is a :py:class:`Selector, ``children[5]`` will give the 6th 401 | element in the selection. This is not random access, but linear in ``n``, so if accessing elements in sequence is 402 | required using the iteration method is recommended. 403 | 404 | Selectors can also be called, in which case they will return a new Selector whose elements are given by applying 405 | the first argument of the function call to each element in the underlying collection. 406 | 407 | Parameters 408 | ---------- 409 | collection : collections.Iterable[{id}] or dict[{id}] 410 | A collection of objects 411 | """ 412 | def __init__(self, collection): 413 | super(Selector, self).__init__() 414 | if not hasattr(collection, '__iter__'): 415 | collection = NamedCollection({collection.id: collection}) 416 | self._collection = collection 417 | 418 | def __call__(self, *args, **kwargs): 419 | _filter = args[0] if len(args) == 1 else lambda x: True 420 | subset = NamedCollection() 421 | for item in self._collection.values(): 422 | if _filter(item): 423 | subset[item.id] = item 424 | return Selector(subset) 425 | 426 | def __getitem__(self, item): 427 | if isinstance(item, int): 428 | i = item if item >= 0 else item + len(self) 429 | for v in self: 430 | if i == 0: 431 | return v 432 | else: 433 | i -= 1 434 | raise IndexError(item) 435 | try: 436 | return self._collection[item] 437 | except KeyError: 438 | return NamedCollection() 439 | 440 | def __contains__(self, item): 441 | return item in self._collection 442 | 443 | def __setitem__(self, key, value): 444 | raise NotImplementedError('Cannot modify read-only collection') 445 | 446 | def __repr__(self): 447 | return repr(list(self)) 448 | 449 | def __iter__(self): 450 | return iter(self._collection.values()) 451 | 452 | def values(self): 453 | return iter(self._collection.values()) 454 | 455 | def items(self): 456 | return iter(self._collection.items()) 457 | 458 | def __len__(self): 459 | return len(list(iter(self))) 460 | 461 | def __eq__(self, other): 462 | return set(iter(self)) == set(iter(other)) 463 | 464 | 465 | class PrefixedSelector(Selector): 466 | def __init__(self, prefix, collection): 467 | super(PrefixedSelector, self).__init__(collection) 468 | self.prefix = prefix 469 | 470 | def __getitem__(self, item): 471 | try: 472 | return super(PrefixedSelector, self).__getitem__('%s/%s' % (self.prefix, item)) 473 | except KeyError: 474 | # Fallback to non-prefixed implementation 475 | return super(PrefixedSelector, self).__getitem__(item) 476 | 477 | def __contains__(self, item): 478 | return super(PrefixedSelector, self).__contains__('%s/%s' % (self.prefix, item)) or \ 479 | super(PrefixedSelector, self).__contains__(item) 480 | 481 | def __setitem__(self, key, value): 482 | raise NotImplementedError() 483 | 484 | def __iter__(self): 485 | return iter(self._collection.values()) 486 | 487 | def iteritems(self): 488 | for k, v in self._collection.items(): 489 | if not k.startswith(self.prefix): 490 | yield '%s/%s' % (self.prefix, k), v 491 | else: 492 | yield k, v 493 | 494 | def itervalues(self): 495 | return iter(self._collection.values()) 496 | 497 | 498 | class NamedCollection(dict, AggregateOperations, Selectors): 499 | def __call__(self, functor=None, *args, **kwargs): 500 | """ 501 | 502 | :param functor: 503 | :param args: 504 | :param kwargs: 505 | :return: 506 | :rtype: NamedCollectionView 507 | """ 508 | if len(args) > 0: 509 | for i in range(len(args)): 510 | functor = functor & args[i] 511 | return NamedCollectionView(self, functor) 512 | 513 | def __getitem__(self, item): 514 | if isinstance(item, Expression): 515 | return NamedCollectionView(self, item) 516 | elif isinstance(item, tuple): 517 | return NamedCollectionView(self, reduce(AndExpression, item)) 518 | else: 519 | return super(NamedCollection, self).__getitem__(item) 520 | 521 | 522 | class NamedCollectionView(AggregateOperations, Selectors): 523 | def __init__(self, parent, functor): 524 | super(NamedCollectionView, self).__init__() 525 | self.parent = parent 526 | self.functor = functor 527 | 528 | def __iter__(self): 529 | parent, functor = self.parent, self.functor 530 | 531 | if functor is not None: 532 | def generator(): 533 | for key, value in parent.items(): 534 | if functor(value): 535 | yield key, value 536 | raise StopIteration 537 | return generator() 538 | else: 539 | return iter(self.parent.items()) 540 | 541 | def iteritems(self): 542 | return iter(self) 543 | 544 | def filter(self, rule): 545 | return self if rule is None else NamedCollectionView(self, rule) 546 | 547 | def __repr__(self): 548 | return repr(dict(iter(self))) 549 | 550 | 551 | class UnionSelector(AggregateOperations, Selectors): 552 | def __init__(self, collection, field): 553 | super(UnionSelector, self).__init__() 554 | self.collection = collection 555 | self.field = field 556 | 557 | def __iter__(self): 558 | for c in self.collection.values(): 559 | if hasattr(c, self.field): 560 | for v in getattr(c, self.field): 561 | yield v 562 | 563 | def itervalues(self): 564 | for c in self.collection: 565 | if hasattr(c, self.field): 566 | child = getattr(c, self.field) 567 | for v in child.values(): 568 | yield v 569 | 570 | def iteritems(self): 571 | for c in self.collection: 572 | if hasattr(c, self.field): 573 | child = getattr(c, self.field) 574 | for k, v in child.items(): 575 | yield k, v 576 | 577 | def __call__(self, *args, **kwargs): 578 | if len(args) == 1: 579 | functor = args[0] 580 | else: 581 | def functor(_): 582 | return True 583 | if len(kwargs) > 0: 584 | pass 585 | else: 586 | return Selector({v.id: v for v in iter(self) if functor(v)}) 587 | 588 | def __getitem__(self, item): 589 | if isinstance(item, int): 590 | i = item if item >= 0 else item + len(self) 591 | for v in self: 592 | if i == 0: 593 | return v 594 | else: 595 | i -= 1 596 | raise IndexError(item) 597 | else: 598 | for v in self.collection.values(): 599 | if hasattr(v, self.field): 600 | haystack = getattr(v, self.field) 601 | if item in haystack: 602 | return haystack[item] 603 | else: 604 | # TODO(ewpatton): Make support ID paths, e.g. Screen1/Arrange1/Button1 605 | for u in haystack.values(): 606 | if u.name == item: 607 | return u 608 | raise KeyError(item) 609 | 610 | def __len__(self): 611 | return len(list(iter(self))) 612 | 613 | 614 | def select(item): 615 | """ 616 | :py:func:`~aiatools.selectors.select` is a convenience method for creating a selection out of a single item. This 617 | allows one to select a specific component for further exploration 618 | :param item: 619 | :return: 620 | """ 621 | return Selector(item) 622 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. AIA Tools documentation master file, created by 2 | sphinx-quickstart on Wed Dec 28 18:03:08 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to AIA Tools's documentation! 7 | ===================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | .. automodule:: aiatools 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /doc/source/aiatools.rst: -------------------------------------------------------------------------------- 1 | aiatools package 2 | ================ 3 | 4 | .. automodule:: aiatools 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | aiatools.aia module 13 | ------------------- 14 | 15 | .. automodule:: aiatools.aia 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | aiatools.algebra module 21 | ----------------------- 22 | 23 | .. automodule:: aiatools.algebra 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | aiatools.attributes module 29 | -------------------------- 30 | 31 | .. automodule:: aiatools.attributes 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | aiatools.block_types module 37 | --------------------------- 38 | 39 | .. automodule:: aiatools.block_types 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | aiatools.common module 45 | ---------------------- 46 | 47 | .. automodule:: aiatools.common 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | aiatools.component_types module 53 | ------------------------------- 54 | 55 | .. automodule:: aiatools.component_types 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | aiatools.selectors module 61 | ------------------------- 62 | 63 | .. automodule:: aiatools.selectors 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | 68 | 69 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # AIA Tools documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Dec 28 18:19:38 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, u'/Users/ewpatton/Programming/mit/aiatools/aiatools') 22 | 23 | import sphinx_rtd_theme 24 | import sys 25 | import aiatools 26 | 27 | sys.path.extend(('.', '..')) 28 | 29 | # -- General configuration ------------------------------------------------ 30 | 31 | # If your documentation needs a minimal Sphinx version, state it here. 32 | # 33 | # needs_sphinx = '1.0' 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = ['sphinx.ext.autodoc', 39 | 'sphinx.ext.coverage', 40 | 'sphinx.ext.doctest', 41 | 'sphinx.ext.intersphinx', 42 | 'sphinx.ext.napoleon', 43 | 'sphinx.ext.todo', 44 | 'sphinx.ext.viewcode'] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # The suffix(es) of source filenames. 50 | # You can specify multiple suffix as a list of string: 51 | # 52 | # source_suffix = ['.rst', '.md'] 53 | source_suffix = '.rst' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # General information about the project. 59 | project = 'AIA Tools' 60 | copyright = '2016-2023 Evan W. Patton' 61 | author = 'Evan W. Patton' 62 | 63 | # The version info for the project you're documenting, acts as replacement for 64 | # |version| and |release|, also used in various other places throughout the 65 | # built documents. 66 | # 67 | # The short X.Y version. 68 | version = '0.4' 69 | # The full version, including alpha/beta/rc tags. 70 | release = aiatools.__version__ 71 | 72 | # The language for content autogenerated by Sphinx. Refer to documentation 73 | # for a list of supported languages. 74 | # 75 | # This is also used if you do content translation via gettext catalogs. 76 | # Usually you set "language" from the command line for these cases. 77 | language = 'en' 78 | 79 | # List of patterns, relative to source directory, that match files and 80 | # directories to ignore when looking for source files. 81 | # This patterns also effect to html_static_path and html_extra_path 82 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # If true, `todo` and `todoList` produce output, else they produce nothing. 88 | todo_include_todos = True 89 | 90 | numfig = True 91 | autodoc_default_flags = ['inherited-members'] 92 | # napoleon_include_special_with_doc = True 93 | 94 | intersphinx_mapping = {'python': ('https://docs.python.org/3.4', None)} 95 | 96 | # -- Options for HTML output ---------------------------------------------- 97 | 98 | # The theme to use for HTML and HTML Help pages. See the documentation for 99 | # a list of builtin themes. 100 | # 101 | html_theme = 'sphinx_rtd_theme' 102 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 103 | 104 | # Theme options are theme-specific and customize the look and feel of a theme 105 | # further. For a list of options available for each theme, see the 106 | # documentation. 107 | # 108 | # html_theme_options = {} 109 | 110 | # Add any paths that contain custom static files (such as style sheets) here, 111 | # relative to this directory. They are copied after the builtin static files, 112 | # so a file named "default.css" will overwrite the builtin "default.css". 113 | html_static_path = ['_static'] 114 | 115 | 116 | # -- Options for HTMLHelp output ------------------------------------------ 117 | 118 | # Output file base name for HTML help builder. 119 | htmlhelp_basename = 'AIAToolsdoc' 120 | 121 | 122 | # -- Options for LaTeX output --------------------------------------------- 123 | 124 | latex_elements = { 125 | # The paper size ('letterpaper' or 'a4paper'). 126 | # 127 | # 'papersize': 'letterpaper', 128 | 129 | # The font size ('10pt', '11pt' or '12pt'). 130 | # 131 | # 'pointsize': '10pt', 132 | 133 | # Additional stuff for the LaTeX preamble. 134 | # 135 | # 'preamble': '', 136 | 137 | # Latex figure (float) alignment 138 | # 139 | # 'figure_align': 'htbp', 140 | } 141 | 142 | # Grouping the document tree into LaTeX files. List of tuples 143 | # (source start file, target name, title, 144 | # author, documentclass [howto, manual, or own class]). 145 | latex_documents = [ 146 | (master_doc, 'AIATools.tex', 'AIA Tools Documentation', 147 | 'Evan W. Patton', 'manual'), 148 | ] 149 | 150 | 151 | # -- Options for manual page output --------------------------------------- 152 | 153 | # One entry per manual page. List of tuples 154 | # (source start file, name, description, authors, manual section). 155 | man_pages = [ 156 | (master_doc, 'aiatools', 'AIA Tools Documentation', 157 | [author], 1) 158 | ] 159 | 160 | 161 | # -- Options for Texinfo output ------------------------------------------- 162 | 163 | # Grouping the document tree into Texinfo files. List of tuples 164 | # (source start file, target name, title, author, 165 | # dir menu entry, description, category) 166 | texinfo_documents = [ 167 | (master_doc, 'AIATools', 'AIA Tools Documentation', 168 | author, 'AIATools', 'One line description of project.', 169 | 'Miscellaneous'), 170 | ] 171 | 172 | 173 | 174 | # -- Options for Epub output ---------------------------------------------- 175 | 176 | # Bibliographic Dublin Core info. 177 | epub_title = project 178 | epub_author = author 179 | epub_publisher = author 180 | epub_copyright = copyright 181 | 182 | # The unique identifier of the text. This can be a ISBN number 183 | # or the project homepage. 184 | # 185 | # epub_identifier = '' 186 | 187 | # A unique identification for the text. 188 | # 189 | # epub_uid = '' 190 | 191 | # A list of files that should not be packed into the epub file. 192 | epub_exclude_files = ['search.html'] 193 | 194 | 195 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. AIA Tools documentation master file, created by 2 | sphinx-quickstart on Wed Dec 28 18:19:38 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to AIA Tools' documentation! 7 | ==================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 4 11 | :caption: Contents: 12 | 13 | aiatools 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /doc/source/modules.rst: -------------------------------------------------------------------------------- 1 | aiatools 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | aiatools 8 | -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | pushd %~dp0 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set BUILDDIR=_build 11 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 12 | set I18NSPHINXOPTS=%SPHINXOPTS% . 13 | if NOT "%PAPER%" == "" ( 14 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 15 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 16 | ) 17 | 18 | if "%1" == "" goto help 19 | 20 | if "%1" == "help" ( 21 | :help 22 | echo.Please use `make ^` where ^ is one of 23 | echo. html to make standalone HTML files 24 | echo. dirhtml to make HTML files named index.html in directories 25 | echo. singlehtml to make a single large HTML file 26 | echo. pickle to make pickle files 27 | echo. json to make JSON files 28 | echo. htmlhelp to make HTML files and an HTML help project 29 | echo. qthelp to make HTML files and a qthelp project 30 | echo. devhelp to make HTML files and a Devhelp project 31 | echo. epub to make an epub 32 | echo. epub3 to make an epub3 33 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 34 | echo. text to make text files 35 | echo. man to make manual pages 36 | echo. texinfo to make Texinfo files 37 | echo. gettext to make PO message catalogs 38 | echo. changes to make an overview over all changed/added/deprecated items 39 | echo. xml to make Docutils-native XML files 40 | echo. pseudoxml to make pseudoxml-XML files for display purposes 41 | echo. linkcheck to check all external links for integrity 42 | echo. doctest to run all doctests embedded in the documentation if enabled 43 | echo. coverage to run coverage check of the documentation if enabled 44 | echo. dummy to check syntax errors of document sources 45 | goto end 46 | ) 47 | 48 | if "%1" == "clean" ( 49 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 50 | del /q /s %BUILDDIR%\* 51 | goto end 52 | ) 53 | 54 | 55 | REM Check if sphinx-build is available and fallback to Python version if any 56 | %SPHINXBUILD% 1>NUL 2>NUL 57 | if errorlevel 9009 goto sphinx_python 58 | goto sphinx_ok 59 | 60 | :sphinx_python 61 | 62 | set SPHINXBUILD=python -m sphinx.__init__ 63 | %SPHINXBUILD% 2> nul 64 | if errorlevel 9009 ( 65 | echo. 66 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 67 | echo.installed, then set the SPHINXBUILD environment variable to point 68 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 69 | echo.may add the Sphinx directory to PATH. 70 | echo. 71 | echo.If you don't have Sphinx installed, grab it from 72 | echo.http://sphinx-doc.org/ 73 | exit /b 1 74 | ) 75 | 76 | :sphinx_ok 77 | 78 | 79 | if "%1" == "html" ( 80 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 84 | goto end 85 | ) 86 | 87 | if "%1" == "dirhtml" ( 88 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 92 | goto end 93 | ) 94 | 95 | if "%1" == "singlehtml" ( 96 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 100 | goto end 101 | ) 102 | 103 | if "%1" == "pickle" ( 104 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can process the pickle files. 108 | goto end 109 | ) 110 | 111 | if "%1" == "json" ( 112 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 113 | if errorlevel 1 exit /b 1 114 | echo. 115 | echo.Build finished; now you can process the JSON files. 116 | goto end 117 | ) 118 | 119 | if "%1" == "htmlhelp" ( 120 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 121 | if errorlevel 1 exit /b 1 122 | echo. 123 | echo.Build finished; now you can run HTML Help Workshop with the ^ 124 | .hhp project file in %BUILDDIR%/htmlhelp. 125 | goto end 126 | ) 127 | 128 | if "%1" == "qthelp" ( 129 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 133 | .qhcp project file in %BUILDDIR%/qthelp, like this: 134 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\AIATools.qhcp 135 | echo.To view the help file: 136 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\AIATools.ghc 137 | goto end 138 | ) 139 | 140 | if "%1" == "devhelp" ( 141 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. 145 | goto end 146 | ) 147 | 148 | if "%1" == "epub" ( 149 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 153 | goto end 154 | ) 155 | 156 | if "%1" == "epub3" ( 157 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 161 | goto end 162 | ) 163 | 164 | if "%1" == "latex" ( 165 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 169 | goto end 170 | ) 171 | 172 | if "%1" == "latexpdf" ( 173 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 174 | cd %BUILDDIR%/latex 175 | make all-pdf 176 | cd %~dp0 177 | echo. 178 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 179 | goto end 180 | ) 181 | 182 | if "%1" == "latexpdfja" ( 183 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 184 | cd %BUILDDIR%/latex 185 | make all-pdf-ja 186 | cd %~dp0 187 | echo. 188 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 189 | goto end 190 | ) 191 | 192 | if "%1" == "text" ( 193 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The text files are in %BUILDDIR%/text. 197 | goto end 198 | ) 199 | 200 | if "%1" == "man" ( 201 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 205 | goto end 206 | ) 207 | 208 | if "%1" == "texinfo" ( 209 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 213 | goto end 214 | ) 215 | 216 | if "%1" == "gettext" ( 217 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 218 | if errorlevel 1 exit /b 1 219 | echo. 220 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 221 | goto end 222 | ) 223 | 224 | if "%1" == "changes" ( 225 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 226 | if errorlevel 1 exit /b 1 227 | echo. 228 | echo.The overview file is in %BUILDDIR%/changes. 229 | goto end 230 | ) 231 | 232 | if "%1" == "linkcheck" ( 233 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 234 | if errorlevel 1 exit /b 1 235 | echo. 236 | echo.Link check complete; look for any errors in the above output ^ 237 | or in %BUILDDIR%/linkcheck/output.txt. 238 | goto end 239 | ) 240 | 241 | if "%1" == "doctest" ( 242 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 243 | if errorlevel 1 exit /b 1 244 | echo. 245 | echo.Testing of doctests in the sources finished, look at the ^ 246 | results in %BUILDDIR%/doctest/output.txt. 247 | goto end 248 | ) 249 | 250 | if "%1" == "coverage" ( 251 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 252 | if errorlevel 1 exit /b 1 253 | echo. 254 | echo.Testing of coverage in the sources finished, look at the ^ 255 | results in %BUILDDIR%/coverage/python.txt. 256 | goto end 257 | ) 258 | 259 | if "%1" == "xml" ( 260 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 261 | if errorlevel 1 exit /b 1 262 | echo. 263 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 264 | goto end 265 | ) 266 | 267 | if "%1" == "pseudoxml" ( 268 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 269 | if errorlevel 1 exit /b 1 270 | echo. 271 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 272 | goto end 273 | ) 274 | 275 | if "%1" == "dummy" ( 276 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 277 | if errorlevel 1 exit /b 1 278 | echo. 279 | echo.Build finished. Dummy builder generates no files. 280 | goto end 281 | ) 282 | 283 | :end 284 | popd 285 | -------------------------------------------------------------------------------- /misc/aia-summarizer2.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | 4 | 5 | with open('appinventor-project-data.json') as f: 6 | data = json.load(f) 7 | print("projects\t%d" % data['projects']) 8 | for category in data['blocks']: 9 | print("blocks\t%s\t%d" % (category, sum(data['blocks'][category].values()))) 10 | for component in data['designer']['components']: 11 | print("components\t%s\t%d" % (component, data['designer']['components'][component]['count'])) 12 | -------------------------------------------------------------------------------- /plot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sourced from https://matplotlib.org/gallery/api/radar_chart.html#sphx-glr-gallery-api-radar-chart-py 3 | """ 4 | 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | from matplotlib.path import Path 8 | from matplotlib.spines import Spine 9 | from matplotlib.projections.polar import PolarAxes 10 | from matplotlib.projections import register_projection 11 | 12 | 13 | def unit_poly_verts(theta): 14 | """Return vertices of polygon for subplot axes. 15 | 16 | This polygon is circumscribed by a unit circle centered at (0.5, 0.5) 17 | """ 18 | x0, y0, r = [0.5] * 3 19 | verts = [(r*np.cos(t) + x0, r*np.sin(t) + y0) for t in theta] 20 | return verts 21 | 22 | 23 | def radar_factory(num_vars, frame='circle'): 24 | """Create a radar chart with `num_vars` axes. 25 | 26 | This function creates a RadarAxes projection and registers it. 27 | 28 | Parameters 29 | ---------- 30 | num_vars : int 31 | Number of variables for radar chart. 32 | frame : {'circle' | 'polygon'} 33 | Shape of frame surrounding axes. 34 | 35 | """ 36 | # calculate evenly-spaced axis angles 37 | theta = np.linspace(0, 2*np.pi, num_vars, endpoint=False) 38 | 39 | def draw_poly_patch(self): 40 | # rotate theta such that the first axis is at the top 41 | verts = unit_poly_verts(theta + np.pi / 2) 42 | return plt.Polygon(verts, closed=True, edgecolor='k') 43 | 44 | def draw_circle_patch(self): 45 | # unit circle centered on (0.5, 0.5) 46 | return plt.Circle((0.5, 0.5), 0.5) 47 | 48 | patch_dict = {'polygon': draw_poly_patch, 'circle': draw_circle_patch} 49 | if frame not in patch_dict: 50 | raise ValueError('unknown value for `frame`: %s' % frame) 51 | 52 | class RadarAxes(PolarAxes): 53 | 54 | name = 'radar' 55 | # use 1 line segment to connect specified points 56 | RESOLUTION = 1 57 | # define draw_frame method 58 | draw_patch = patch_dict[frame] 59 | 60 | def __init__(self, *args, **kwargs): 61 | super(RadarAxes, self).__init__(*args, **kwargs) 62 | # rotate plot such that the first axis is at the top 63 | self.set_theta_zero_location('N') 64 | 65 | def fill(self, *args, **kwargs): 66 | """Override fill so that line is closed by default""" 67 | closed = kwargs.pop('closed', True) 68 | return super(RadarAxes, self).fill(closed=closed, *args, **kwargs) 69 | 70 | def plot(self, *args, **kwargs): 71 | """Override plot so that line is closed by default""" 72 | lines = super(RadarAxes, self).plot(*args, **kwargs) 73 | for line in lines: 74 | self._close_line(line) 75 | 76 | def _close_line(self, line): 77 | x, y = line.get_data() 78 | # FIXME: markers at x[0], y[0] get doubled-up 79 | if x[0] != x[-1]: 80 | x = np.concatenate((x, [x[0]])) 81 | y = np.concatenate((y, [y[0]])) 82 | line.set_data(x, y) 83 | 84 | def set_varlabels(self, labels): 85 | self.set_thetagrids(np.degrees(theta), labels) 86 | 87 | def _gen_axes_patch(self): 88 | return self.draw_patch() 89 | 90 | def _gen_axes_spines(self): 91 | if frame == 'circle': 92 | return PolarAxes._gen_axes_spines(self) 93 | # The following is a hack to get the spines (i.e. the axes frame) 94 | # to draw correctly for a polygon frame. 95 | 96 | # spine_type must be 'left', 'right', 'top', 'bottom', or `circle`. 97 | spine_type = 'circle' 98 | verts = unit_poly_verts(theta + np.pi / 2) 99 | # close off polygon by repeating first vertex 100 | verts.append(verts[0]) 101 | path = Path(verts) 102 | 103 | spine = Spine(self, spine_type, path) 104 | spine.set_transform(self.transAxes) 105 | return {'polar': spine} 106 | 107 | register_projection(RadarAxes) 108 | return theta 109 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | dynamic = ["version"] 7 | name = "aiatools" 8 | dependencies = ["jprops"] 9 | requires-python = ">= 3.0" 10 | authors = [ 11 | {name = "Evan W. Patton", email = "ewpatton@mit.edu" } 12 | ] 13 | maintainers = [ 14 | {name = "Evan W. Patton", email = "ewpatton@mit.edu" } 15 | ] 16 | description = "Tools for extracting information from App Inventor AIA files" 17 | readme = "README.md" 18 | license = "GPL-3.0-or-later" 19 | license-files = ["COPYING"] 20 | classifiers = [ 21 | 'Development Status :: 3 - Alpha', 22 | 'Environment :: Console', 23 | 'Intended Audience :: Developers', 24 | 'Intended Audience :: Education', 25 | 'Intended Audience :: Science/Research', 26 | 'Natural Language :: English', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python', 29 | 'Topic :: Education', 30 | 'Topic :: Scientific/Engineering :: Information Analysis', 31 | 'Topic :: Software Development', 32 | 'Topic :: Utilities' 33 | ] 34 | keywords = ["App Inventor", "AIA extraction", "analysis", "toolkit"] 35 | 36 | [project.urls] 37 | Repository = "https://github.com/mit-cml/aiatools" 38 | 39 | [tool.setuptools] 40 | packages = ["aiatools"] 41 | 42 | [tool.setuptools.dynamic] 43 | version = {attr = "aiatools.__version__"} 44 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jprops 2 | sphinx 3 | sphinx_rtd_theme 4 | matplotlib 5 | -------------------------------------------------------------------------------- /samples.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- mode: python; coding: utf-8; -*- 3 | # Copyright © 2017 Massachusetts Institute of Technology, All rights reserved. 4 | 5 | """ 6 | Query samples for AIATools 7 | """ 8 | 9 | 10 | from aiatools import * 11 | 12 | 13 | __author__ = 'Evan W. Patton ' 14 | 15 | 16 | def plot(sel, title, y_label): 17 | import matplotlib.pyplot as plt 18 | import numpy as np 19 | width = 0.35 20 | fig, ax = plt.subplots() 21 | ind = np.arange(len(sel)) 22 | ax.bar(ind, list(sel.values()), width, color='r') 23 | ax.set_ylabel(y_label) 24 | ax.set_title(title) 25 | ax.set_xticks(ind) 26 | ax.set_xticklabels(iter(sel.keys())) 27 | ax.margins(x=0.01, y=0.01) 28 | fig.subplots_adjust(bottom=0.35) 29 | plt.xticks(rotation=45, va='top', ha='right') 30 | plt.show() 31 | 32 | 33 | def radar_plot(sel, title): 34 | import matplotlib.pyplot as plt 35 | from plot import radar_factory 36 | num_vars = len(sel) 37 | theta = radar_factory(num_vars) 38 | fig, ax = plt.subplots(subplot_kw=dict(projection='radar')) 39 | labels = list(sel.keys()) 40 | values = list(sel.values()) 41 | min_values = min(values) 42 | max_values = max(values) 43 | the_range = max_values - min_values 44 | if the_range <= 5: 45 | ax.set_rgrids(list(range(min_values, max_values))) 46 | elif the_range <= 10: 47 | ax.set_rgrids(list(range(min_values, max_values, 2))) 48 | elif the_range <= 20: 49 | ax.set_rgrids(list(range(min_values, max_values, 4))) 50 | else: 51 | ax.set_rgrids(list(range(min_values, max_values, round(the_range / 5)))) # I chose round, but int could work 52 | ax.set_title(title, weight='bold') 53 | ax.plot(theta, values) 54 | ax.fill(theta, values, alpha=0.25) 55 | ax.set_varlabels(labels) 56 | plt.show() 57 | 58 | 59 | def is_infinite_recursion(block): 60 | visited = {block} 61 | callers = list(select(block).callers()) 62 | while len(callers) > 0: 63 | caller = root_block(callers.pop(0)) 64 | if caller in visited: 65 | return True 66 | callers.extend(select(caller).callers()) 67 | return False 68 | 69 | 70 | is_proc_def = (type == procedures_defreturn) | (type == procedures_defnoreturn) 71 | 72 | 73 | def main(): 74 | with AIAFile('test_aias/Yahtzee5.aia') as aia: 75 | print('Components = ', aia.components()) 76 | print('Number of components, by type =', aia.components().count(group_by=type)) 77 | print('Number of buttons =', aia.components(type == Button).count()) 78 | print('Number of labels =', aia.components(type == Label).count()) 79 | radar_plot(aia.blocks().count(group_by=category), 'Blocks by Category (Yahtzee5)') 80 | 81 | with AIAFile('test_aias/LondonCholeraMap.aia') as aia: 82 | print('Number of screens =', len(aia.screens)) 83 | print('Number of components on Screen1 =', len(aia.screens['Screen1'].components())) 84 | print('Components on Screen1 =', aia.screens['Screen1'].components()) 85 | print('Number of buttons on Screen1 =', aia.screens['Screen1'].components(type == Button).count()) 86 | print('Is the set of components with FusiontablesControl empty?', 87 | aia.components(type == FusiontablesControl).empty()) 88 | print('First button =', aia.components(type == Button)[0]) 89 | print('Component LoadPumpBtn =', aia.components[name == 'LoadPumpBtn']) 90 | print('All components of type Map =', aia.components(type == Map)) 91 | print('All components, grouped by type =', aia.components().count(group_by=type)) 92 | print('All Logic blocks =', aia.blocks(category == Logic)) 93 | print('All logic comparison blocks with text descendants =', 94 | aia.blocks((type == logic_compare) & has_descendant(type == text))) 95 | print('All blocks for the Map component =', aia.blocks(mutation.component_type == Map)) 96 | print('Count of all Map component blocks =', aia.blocks(mutation.component_type == Map).count()) 97 | print('Count of all Map component blocks, grouped by block type =', 98 | aia.blocks(mutation.component_type == Map).count(group_by=type)) 99 | print('Average depth of the block tree starting at the root blocks =', aia.blocks(top_level).avg(height)) 100 | print('Count of generic blocks, grouped by block type and mutation component type =', 101 | aia.blocks(mutation.is_generic).count(group_by=(type, mutation.component_type))) 102 | print('Descendants of logic blocks =', aia.blocks(category == Logic).descendants()) 103 | plot(aia.blocks().count(group_by=type), 'Blocks by type', 'Count') 104 | radar_plot(aia.blocks().count(group_by=category), 'Blocks by Category (LondonCholeraMap)') 105 | 106 | with AIAFile('test_aias/ProcedureTest.aia') as aia: 107 | print('Number of procedure definitions =', 108 | aia.blocks(is_procedure).count()) 109 | print('Number of callers of VoidFoo =', aia.blocks(fields.NAME == 'VoidFoo').callers().count()) 110 | print('Number of callers of FooStuff = ', aia.blocks(fields.NAME == 'FooStuff').callers().count()) 111 | print('Active callers of disabled procedures =', 112 | aia.blocks(is_proc_def & disabled).callers(~disabled).count()) 113 | print('Callers of disabled procedures =', aia.blocks(is_proc_def & disabled).callers(~disabled)) 114 | print('Recursive functions =', aia.blocks(is_proc_def & is_infinite_recursion).select(fields.NAME)) 115 | print('Uncalled procedures =', aia.blocks(is_proc_def & ~is_called).select(fields.NAME)) 116 | 117 | with AIAFile('test_aias/moodring_patched.aia') as aia: 118 | print('Mood Ring screens =', aia.screens()) 119 | 120 | 121 | if __name__ == '__main__': 122 | main() 123 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [easy_install] 2 | 3 | -------------------------------------------------------------------------------- /test_aias/LondonCholeraMap.aia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-cml/aiatools/cd844a759aecec86390c7415fb59f11e1f7933f7/test_aias/LondonCholeraMap.aia -------------------------------------------------------------------------------- /test_aias/ProcedureTest.aia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-cml/aiatools/cd844a759aecec86390c7415fb59f11e1f7933f7/test_aias/ProcedureTest.aia -------------------------------------------------------------------------------- /test_aias/Yahtzee5.aia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-cml/aiatools/cd844a759aecec86390c7415fb59f11e1f7933f7/test_aias/Yahtzee5.aia -------------------------------------------------------------------------------- /test_aias/moodring_patched.aia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-cml/aiatools/cd844a759aecec86390c7415fb59f11e1f7933f7/test_aias/moodring_patched.aia --------------------------------------------------------------------------------