├── .gitignore ├── .travis.yml ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat └── schemagic.rst ├── requirements.txt ├── schemagic ├── __init__.py ├── core.py ├── func.py ├── utils.py ├── validators.py └── web.py ├── setup.cfg ├── setup.py ├── test ├── __init__.py └── test_schemagic.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.idea 2 | *.pyc 3 | build/ 4 | dist/ 5 | schemagic.egg-info/ 6 | schemagic/scratch.py 7 | does-it-work/ 8 | schemagic/__pycache__/ 9 | schemagic-env/ 10 | .coverage 11 | test/__pycache__/ 12 | .cache/ 13 | .tox/ 14 | .eggs/ 15 | docs/_build 16 | /schemagic/schemagic_stuff.py 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | 6 | sudo: required 7 | 8 | install: 9 | - "sudo pip install -r requirements.txt" 10 | - "sudo pip install coverage" 11 | script: 12 | - "coverage run --source=schemagic setup.py test" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | (This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.) 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | {description} 474 | Copyright (C) {year} {fullname} 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | {signature of Ty Coon}, 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Schemagic / Schemagic.web 3 | ========================= 4 | .. image:: https://img.shields.io/badge/pypi-v0.9.1-blue.svg 5 | :target: https://pypi.python.org/pypi/schemagic 6 | .. image:: https://img.shields.io/badge/ReadTheDocs-latest-red.svg 7 | :target: http://schemagic.readthedocs.io/en/latest/schemagic.html 8 | .. image:: https://travis-ci.org/Mechrophile/schemagic.svg?branch=master 9 | :target: https://travis-ci.org/Mechrophile/schemagic/ 10 | Remove the Guesswork from Data Processing 11 | ========================================= 12 | 13 | 14 | Schemagic is a rather utilitarian re-imagining of the wonderful and powerful clojure library `Schema `_! 15 | Schemagic.web is what programmers do when they hate web programming, but want to make their programs accessible to the web. 16 | 17 | 18 | Installation 19 | ------------ 20 | 21 | It's a wheel on Pypi, and it's 2 and 3 compatible. 22 | To install Schemagic, simply: 23 | 24 | .. code-block:: bash 25 | 26 | $ pip install schemagic 27 | 28 | What is schemagic? 29 | ------------------ 30 | 31 | One of the difficulties with large scale, multi-team python efforts is the overhead of understanding the kind of data 32 | (e.g., list of strings, nested map from long to string to double) that a function or a webservice expects and returns. 33 | Python lacks static typing and, moreover, static typing is insufficient to capture and validate custom business types, 34 | which ultimately is what holds back teams from rapidly iterating on each others work.[1] 35 | 36 | To you, the programmer, schemagic is all about three things: 37 | 38 | * data **description** using the simplest python data structures and an easily extensible syntax 39 | * data **communication** between teams, enhancing documentation, giving feedback when something went wrong. 40 | * data **validation** based on descriptions of data that have been documented and communicated. 41 | Comments describing the shape of data are insufficient in real world applications. 42 | Unless the documentation is backed up by programmatic verification, the documentation gets initially ignored, 43 | and ultimately falls behind the actual program behavior. 44 | 45 | In other words, **schemagic is all about data**. 46 | 47 | 48 | Getting Acquainted with Schemagic 49 | --------------------------------- 50 | 51 | Lets build a schema and start using it. 52 | 53 | .. code-block:: python 54 | 55 | >>> import schemagic 56 | >>> list_of_ints = [int] 57 | >>> schemagic.validate_against_schema(list_of_ints, [1, 2, 3]) 58 | [1, 2, 3] 59 | >>> schemagic.validate_against_schema(list_of_ints, ["hello", "my friends"]) 60 | Traceback (most recent call last): 61 | ... 62 | ValueError: invalid literal for int() with base 10: 'hello' 63 | 64 | The error you see here (customizeable) is the error you get when you try to call: 65 | 66 | .. code-block:: python 67 | 68 | >>> int("hello") 69 | Traceback (most recent call last): 70 | ... 71 | ValueError: invalid literal for int() with base 10: 'hello' 72 | 73 | And it occurred because list_of_ints specified that the function to check every member of the list against was int() 74 | 75 | 76 | Basic Schemagic Usage 77 | --------------------- 78 | 79 | Schema checking is quite flexible, and all checks are done recursively. Lets go through some more examples: 80 | 81 | **Map Template**: 82 | *if you only provide a schema with one (callable) key and one value* 83 | 84 | .. code-block:: python 85 | 86 | >>> string_to_int_map = {str:int} 87 | >>> schemagic.validate_against_schema(string_to_int_map, {"hello": 5, "friends": 6}) 88 | {'friends': 6, 'hello': 5} 89 | 90 | **Map with Specific Keys** 91 | *if you provide a schema with strings as keys* 92 | 93 | .. code-block:: python 94 | 95 | >>> friend_record = {"name":str, "age": int} 96 | >>> schemagic.validate_against_schema(friend_record, {"name": "Tyler", "age": 400}) 97 | {'name': 'Tyler', 'age': 400} 98 | 99 | **Sequence Template**: 100 | *if you provide a sequence containing only one item as a schema* 101 | 102 | .. code-block:: python 103 | 104 | >>> list_of_ints = [int] 105 | >>> schemagic.validate_against_schema(list_of_ints, [1, 2, 3, 4]) 106 | [1, 2, 3, 4] 107 | 108 | **Strict Sequence**: 109 | *if you provide a sequence with multiple items as a schema* 110 | 111 | .. code-block:: python 112 | 113 | >>> list_with_3_items_int_str_and_intstrmap = [int, str, {int: str}] 114 | >>> schemagic.validate_against_schema(list_with_3_items_int_str_and_intstrmap, [1, "hello", {5: "friends", 12: "and", 90: "world"}]) 115 | [1, "hello", {5: "friends", 12: "and", 90: "world"}] 116 | 117 | **Validation Function**: 118 | *if you provide a function as a schema* 119 | 120 | .. code-block:: python 121 | 122 | >>> def null(data): 123 | ... if data is not None: 124 | ... raise TypeError("expected Nonetype, got {0}".format(data)) 125 | >>> schemagic.validate_against_schema(null, None) 126 | >>> schemagic.validate_against_schema(null, "hello!") 127 | Traceback (most recent call last): 128 | ... 129 | TypeError: expected Nonetype, got hello 130 | 131 | 132 | **Compose Schema Definitions Recursively Ad Nauseam**: 133 | *this is where the real value lies* 134 | 135 | .. code-block:: python 136 | 137 | >>> def enum(*possible_values): 138 | ... def _validator(data): 139 | ... if not data in possible_values: 140 | ... raise ValueError() 141 | ... return data 142 | ... return _validator 143 | >>> event = { 144 | ... "event_type": enum("PRODUCTION", "DEVELOPMENT"), 145 | ... "event_name": str 146 | ...} 147 | >>> dispatch_request = { 148 | ... "events": [event], 149 | ... "requested_by": str 150 | ...} 151 | >>> schemagic.validate_against_schema(dispatch_request, 152 | ... {"events": [{"event_type": "DEVELOPMENT", 153 | ... "event_name": "demo_business_process"}, 154 | ... {"event_type": "DEVELOPMENT", 155 | ... "event_name": "demo_other_business_process"}], 156 | ... "requested_by": "Tyler Tolton"}) 157 | {"events": [{"event_type": "DEVELOPMENT", "event_name": "demo_business_process"}, {"event_type": "DEVELOPMENT", "event_name": "demo_other_business_process"}], "requested_by": "Tyler Tolton"} 158 | 159 | 160 | Schemagic.validator Usage 161 | ------------------------- 162 | 163 | **Use the Schemagic.validator for increased message clarity and control**: 164 | 165 | .. code-block:: python 166 | 167 | >>> list_of_ints_validator = schemagic.validator([int], "Business Type: list of integers") 168 | >>> list_of_ints_validator([1, "not an int", 3]) 169 | Traceback (most recent call last): 170 | ... 171 | ValueError: Bad value provided for Business Type: list of integers. - error: ValueError: invalid literal for int() with base 10: 'not an int' schema: [] value: [1, 'not an int', 3] 172 | 173 | **Supply predicate to prevent/enable validation conditionally**: 174 | 175 | .. code-block:: python 176 | 177 | >>> __env__ = None 178 | >>> WHEN_IN_DEV_ENV = lambda: __env__ == "DEV" 179 | >>> validate_in_dev = partial(schemagic.validator, validation_predicate=WHEN_IN_DEV_ENV) 180 | >>> list_of_ints_validator = validate_in_dev([int], "integer list") 181 | >>> __env__ = "DEV" 182 | >>> list_of_ints_validator([1, "not an int", 3]) 183 | Traceback (most recent call last): 184 | ... 185 | ValueError: Bad value provided for integer list. - error: ValueError: invalid literal for int() with base 10: 'not an int' schema: [] value: [1, 'not an int', 3] 186 | >>> __env__ = "PROD" 187 | >>> list_of_ints_validator([1, "not an int", 3]) 188 | [1, "not an int", 3] 189 | 190 | 191 | **Coerce data as it is validated**: 192 | *note: validate_against_schema will do this automatically. see docs on validator.* 193 | 194 | .. code-block:: python 195 | 196 | >>> validate_and_coerce = partial(schemagic.validator, coerce_data=True) 197 | >>> list_of_ints_validator_and_coercer = validate_and_coerce([int], "integer list") 198 | >>> list_of_ints_validator_only = schemagic.validator([int], "integer_list") 199 | >>> list_of_ints_validator_only(["1", "2", "3"]) 200 | ["1", "2", "3"] 201 | >>> # Note that the if you pass an integer string to int() it returns an integer. 202 | >>> # this makes it s dual purpose validator and coercer. 203 | >>> list_of_ints_validator_and_coercer(["1", "2", "3"]) 204 | [1, 2, 3] 205 | 206 | 207 | Schemagic.web 208 | ------------- 209 | 210 | Schemagic.web is where rubber meets the road in practical usage. It provides an easy way to communicate between 211 | services, between developers, and between development teams in an agile environment. The webservice business world was 212 | the furnace in which schemagic was forged. Get ready to outsource yourself. 213 | 214 | To demo the schemagic.web workflow, lets assume the roles of the first people in the world to discover a way 215 | to (gasp) compute the fibonacci sequence in python. 216 | 217 | *note: this code is all pulled from Peter Norvig's excellent* `Design of Computer Programs `_ *Udacity class.* 218 | 219 | .. code-block:: python 220 | 221 | def memo(fn): 222 | _cache = {} 223 | def _f(*args): 224 | try: 225 | return _cache[args] 226 | except KeyError: 227 | _cache[args] = result = fn(*args) 228 | return result 229 | except TypeError: 230 | return fn(*args) 231 | _f.cache = _cache 232 | return _f 233 | 234 | @memo 235 | def fib(n): 236 | if n == 0 or n == 1: 237 | return 1 238 | else: 239 | return fib(n - 1) + fib(n - 2) 240 | 241 | >>> fib(30) 242 | 1346269 243 | 244 | Brilliant! Well, now we'll of course want to share this discovery with the world in the form of a microservice, so that 245 | others need not know the inner workings of this complex and dangerous algorithm. 246 | 247 | Lets walk through how we might set up this webservice in flask: 248 | 249 | .. code-block:: python 250 | 251 | from flask import Flask, json 252 | from fibonacci import fib # assuming we implemented the function in fibonnaci.py 253 | 254 | app = Flask(__name__) 255 | 256 | @app.route("/fibonacci/") 257 | def web_fib_endpoint(index): 258 | try: 259 | index = int(index) 260 | except ValueError: 261 | return Response( 262 | status=400, 263 | response="Argument to /fibonacci/ must be an integer" 264 | ) 265 | return Response( 266 | status=200, 267 | response=json.dumps(fib(index)) 268 | ) 269 | 270 | 271 | if __name__ == '__main__': 272 | app.run(port=5000) 273 | 274 | 275 | While this pattern is certainly serviceable, it is rather heavyweight to simply expose a function to the web. 276 | Additionally, the code doesn't lend itself well to easily documenting its input and output. 277 | Lets see an adapted version of this code using schemagic.web utilities. 278 | 279 | .. code-block:: python 280 | 281 | from flask.app import Flask 282 | from fibonacci import fib # assuming we implemented the function in fibonnaci.py 283 | from schemagic.web import service_registry 284 | 285 | app = Flask(__name__) 286 | register_fibonnacci_services = service_registry(app) 287 | 288 | register_fibonnacci_services( 289 | dict(rule="/fibonacci", 290 | input_schema=int, 291 | output_schema=int, 292 | fn=fib)) 293 | 294 | if __name__ == '__main__': 295 | app.run(port=5000) 296 | 297 | There, now we simply *describe* our service with data. 298 | What is the service endpoint, what is the input, what is the output, 299 | and what is the implementation that delivers the contract defined herein. 300 | 301 | #. The webservices all uniformally use POST requests to transmit data. The data supplied to the endpoints comes from the payload of the request. 302 | 303 | How to Contribute 304 | ----------------- 305 | #. This codebase uses the popular `git flow `_ model for version control 306 | #. Fork `the repository`_ and make a branch off of develop, (ideally using the naming convention feature/your-feature) 307 | #. When you've finished your feature, make a pull request back into develop. 308 | #. Once you've made your pull request, email `the maintainer`_ and let me know! 309 | #. Finally, if you ever have any questions about how or what to contribute, feel free to send an email! 310 | 311 | .. _`the repository`: https://github.com/TJTolton/schemagic 312 | .. _`the maintainer`: tjtolton@gmail.com 313 | 314 | Documentation 315 | ============= 316 | 317 | This project autogenerates it's documentation using sphinx and hosts it using readthedocs. It can be viewed `here `_ 318 | 319 | 320 | .. [1] Please note: this description is adapted from the excellently phrased introduction to the `prismatic/schema `_ clojure library this project was based on 321 | -------------------------------------------------------------------------------- /docs/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 = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 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 a 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 " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/schemagic.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/schemagic.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/schemagic" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/schemagic" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | import sys 3 | 4 | try: 5 | release = pkg_resources.get_distribution('schemagic').version 6 | except pkg_resources.DistributionNotFound: 7 | print 'To build the documentation, The distribution information of sandman' 8 | print 'Has to be available. Either install the package into your' 9 | print 'development environment or run "setup.py develop" to setup the' 10 | print 'metadata. A virtualenv is recommended!' 11 | sys.exit(1) 12 | del pkg_resources 13 | 14 | version = '.'.join(release.split('.')[:2]) 15 | # -*- coding: utf-8 -*- 16 | # 17 | # schemagic documentation build configuration file, created by 18 | # sphinx-quickstart on Fri Jun 24 15:31:24 2016. 19 | # 20 | # This file is execfile()d with the current directory set to its 21 | # containing dir. 22 | # 23 | # Note that not all possible configuration values are present in this 24 | # autogenerated file. 25 | # 26 | # All configuration values have a default; values that are commented out 27 | # serve to show the default. 28 | 29 | # If extensions (or modules to document with autodoc) are in another directory, 30 | # add these directories to sys.path here. If the directory is relative to the 31 | # documentation root, use os.path.abspath to make it absolute, like shown here. 32 | # 33 | # import os 34 | # import sys 35 | # sys.path.insert(0, os.path.abspath('.')) 36 | 37 | # -- General configuration ------------------------------------------------ 38 | 39 | # If your documentation needs a minimal Sphinx version, state it here. 40 | # 41 | # needs_sphinx = '1.0' 42 | 43 | # Add any Sphinx extension module names here, as strings. They can be 44 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 45 | # ones. 46 | extensions = [ 47 | 'sphinx.ext.autodoc', 48 | 'sphinx.ext.todo', 49 | 'sphinx.ext.viewcode', 50 | ] 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ['_templates'] 54 | 55 | # The suffix(es) of source filenames. 56 | # You can specify multiple suffix as a list of string: 57 | # 58 | # source_suffix = ['.rst', '.md'] 59 | source_suffix = '.rst' 60 | 61 | # The encoding of source files. 62 | # 63 | # source_encoding = 'utf-8-sig' 64 | 65 | # The master toctree document. 66 | master_doc = 'index' 67 | 68 | # General information about the project. 69 | project = "schemagic" 70 | copyright = '2016, Tyler Tolton' 71 | author = 'Tyler Tolton' 72 | 73 | # The version info for the project you're documenting, acts as replacement for 74 | # |version| and |release|, also used in various other places throughout the 75 | # built documents. 76 | # 77 | # The short X.Y version. 78 | # The full version, including alpha/beta/rc tags. 79 | release = version 80 | 81 | # The language for content autogenerated by Sphinx. Refer to documentation 82 | # for a list of supported languages. 83 | # 84 | # This is also used if you do content translation via gettext catalogs. 85 | # Usually you set "language" from the command line for these cases. 86 | language = 'en' 87 | 88 | # There are two options for replacing |today|: either, you set today to some 89 | # non-false value, then it is used: 90 | # 91 | # today = '' 92 | # 93 | # Else, today_fmt is used as the format for a strftime call. 94 | # 95 | # today_fmt = '%B %d, %Y' 96 | 97 | # List of patterns, relative to source directory, that match files and 98 | # directories to ignore when looking for source files. 99 | # This patterns also effect to html_static_path and html_extra_path 100 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 101 | 102 | # The reST default role (used for this markup: `text`) to use for all 103 | # documents. 104 | # 105 | # default_role = None 106 | 107 | # If true, '()' will be appended to :func: etc. cross-reference text. 108 | # 109 | # add_function_parentheses = True 110 | 111 | # If true, the current module name will be prepended to all description 112 | # unit titles (such as .. function::). 113 | # 114 | # add_module_names = True 115 | 116 | # If true, sectionauthor and moduleauthor directives will be shown in the 117 | # output. They are ignored by default. 118 | # 119 | # show_authors = False 120 | 121 | # The name of the Pygments (syntax highlighting) style to use. 122 | pygments_style = 'sphinx' 123 | 124 | # A list of ignored prefixes for module index sorting. 125 | # modindex_common_prefix = [] 126 | 127 | # If true, keep warnings as "system message" paragraphs in the built documents. 128 | # keep_warnings = False 129 | 130 | # If true, `todo` and `todoList` produce output, else they produce nothing. 131 | todo_include_todos = True 132 | 133 | 134 | # -- Options for HTML output ---------------------------------------------- 135 | 136 | # The theme to use for HTML and HTML Help pages. See the documentation for 137 | # a list of builtin themes. 138 | # 139 | html_theme = 'nature' 140 | 141 | # Theme options are theme-specific and customize the look and feel of a theme 142 | # further. For a list of options available for each theme, see the 143 | # documentation. 144 | # 145 | # html_theme_options = {} 146 | 147 | # Add any paths that contain custom themes here, relative to this directory. 148 | # html_theme_path = [] 149 | 150 | # The name for this set of Sphinx documents. 151 | # " v documentation" by default. 152 | # 153 | # html_title = u'schemagic v' 154 | 155 | # A shorter title for the navigation bar. Default is the same as html_title. 156 | # 157 | # html_short_title = None 158 | 159 | # The name of an image file (relative to this directory) to place at the top 160 | # of the sidebar. 161 | # 162 | # html_logo = None 163 | 164 | # The name of an image file (relative to this directory) to use as a favicon of 165 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 166 | # pixels large. 167 | # 168 | # html_favicon = None 169 | 170 | # Add any paths that contain custom static files (such as style sheets) here, 171 | # relative to this directory. They are copied after the builtin static files, 172 | # so a file named "default.css" will overwrite the builtin "default.css". 173 | html_static_path = ['_static'] 174 | 175 | # Add any extra paths that contain custom files (such as robots.txt or 176 | # .htaccess) here, relative to this directory. These files are copied 177 | # directly to the root of the documentation. 178 | # 179 | # html_extra_path = [] 180 | 181 | # If not None, a 'Last updated on:' timestamp is inserted at every page 182 | # bottom, using the given strftime format. 183 | # The empty string is equivalent to '%b %d, %Y'. 184 | # 185 | # html_last_updated_fmt = None 186 | 187 | # If true, SmartyPants will be used to convert quotes and dashes to 188 | # typographically correct entities. 189 | # 190 | # html_use_smartypants = True 191 | 192 | # Custom sidebar templates, maps document names to template names. 193 | # 194 | # html_sidebars = {} 195 | 196 | # Additional templates that should be rendered to pages, maps page names to 197 | # template names. 198 | # 199 | # html_additional_pages = {} 200 | 201 | # If false, no module index is generated. 202 | # 203 | # html_domain_indices = True 204 | 205 | # If false, no index is generated. 206 | # 207 | # html_use_index = True 208 | 209 | # If true, the index is split into individual pages for each letter. 210 | # 211 | # html_split_index = False 212 | 213 | # If true, links to the reST sources are added to the pages. 214 | # 215 | # html_show_sourcelink = True 216 | 217 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 218 | # 219 | # html_show_sphinx = True 220 | 221 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 222 | # 223 | # html_show_copyright = True 224 | 225 | # If true, an OpenSearch description file will be output, and all pages will 226 | # contain a tag referring to it. The value of this option must be the 227 | # base URL from which the finished HTML is served. 228 | # 229 | # html_use_opensearch = '' 230 | 231 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 232 | # html_file_suffix = None 233 | 234 | # Language to be used for generating the HTML full-text search index. 235 | # Sphinx supports the following languages: 236 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 237 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' 238 | # 239 | # html_search_language = 'en' 240 | 241 | # A dictionary with options for the search language support, empty by default. 242 | # 'ja' uses this config value. 243 | # 'zh' user can custom change `jieba` dictionary path. 244 | # 245 | # html_search_options = {'type': 'default'} 246 | 247 | # The name of a javascript file (relative to the configuration directory) that 248 | # implements a search results scorer. If empty, the default will be used. 249 | # 250 | # html_search_scorer = 'scorer.js' 251 | 252 | # Output file base name for HTML help builder. 253 | htmlhelp_basename = 'schemagicdoc' 254 | 255 | # -- Options for LaTeX output --------------------------------------------- 256 | 257 | latex_elements = { 258 | # The paper size ('letterpaper' or 'a4paper'). 259 | # 260 | # 'papersize': 'letterpaper', 261 | 262 | # The font size ('10pt', '11pt' or '12pt'). 263 | # 264 | # 'pointsize': '10pt', 265 | 266 | # Additional stuff for the LaTeX preamble. 267 | # 268 | # 'preamble': '', 269 | 270 | # Latex figure (float) alignment 271 | # 272 | # 'figure_align': 'htbp', 273 | } 274 | 275 | # Grouping the document tree into LaTeX files. List of tuples 276 | # (source start file, target name, title, 277 | # author, documentclass [howto, manual, or own class]). 278 | latex_documents = [ 279 | (master_doc, 'schemagic.tex', u'schemagic Documentation', 280 | 'Tyler Tolton', 'manual'), 281 | ] 282 | 283 | # The name of an image file (relative to this directory) to place at the top of 284 | # the title page. 285 | # 286 | # latex_logo = None 287 | 288 | # For "manual" documents, if this is true, then toplevel headings are parts, 289 | # not chapters. 290 | # 291 | # latex_use_parts = False 292 | 293 | # If true, show page references after internal links. 294 | # 295 | # latex_show_pagerefs = False 296 | 297 | # If true, show URL addresses after external links. 298 | # 299 | # latex_show_urls = False 300 | 301 | # Documents to append as an appendix to all manuals. 302 | # 303 | # latex_appendices = [] 304 | 305 | # If false, no module index is generated. 306 | # 307 | # latex_domain_indices = True 308 | 309 | 310 | # -- Options for manual page output --------------------------------------- 311 | 312 | # One entry per manual page. List of tuples 313 | # (source start file, name, description, authors, manual section). 314 | man_pages = [ 315 | (master_doc, 'schemagic', u'schemagic Documentation', 316 | [author], 1) 317 | ] 318 | 319 | # If true, show URL addresses after external links. 320 | # 321 | # man_show_urls = False 322 | 323 | 324 | # -- Options for Texinfo output ------------------------------------------- 325 | 326 | # Grouping the document tree into Texinfo files. List of tuples 327 | # (source start file, target name, title, author, 328 | # dir menu entry, description, category) 329 | texinfo_documents = [ 330 | (master_doc, 'schemagic', u'schemagic Documentation', 331 | author, 'schemagic', 'One line description of project.', 332 | 'Miscellaneous'), 333 | ] 334 | 335 | # Documents to append as an appendix to all manuals. 336 | # 337 | # texinfo_appendices = [] 338 | 339 | # If false, no module index is generated. 340 | # 341 | # texinfo_domain_indices = True 342 | 343 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 344 | # 345 | # texinfo_show_urls = 'footnote' 346 | 347 | # If true, do not generate a @detailmenu in the "Top" node's menu. 348 | # 349 | # texinfo_no_detailmenu = False 350 | 351 | 352 | # -- Options for Epub output ---------------------------------------------- 353 | 354 | # Bibliographic Dublin Core info. 355 | epub_title = project 356 | epub_author = author 357 | epub_publisher = author 358 | epub_copyright = copyright 359 | 360 | # The basename for the epub file. It defaults to the project name. 361 | # epub_basename = project 362 | 363 | # The HTML theme for the epub output. Since the default themes are not 364 | # optimized for small screen space, using the same theme for HTML and epub 365 | # output is usually not wise. This defaults to 'epub', a theme designed to save 366 | # visual space. 367 | # 368 | # epub_theme = 'epub' 369 | 370 | # The language of the text. It defaults to the language option 371 | # or 'en' if the language is not set. 372 | # 373 | # epub_language = '' 374 | 375 | # The scheme of the identifier. Typical schemes are ISBN or URL. 376 | # epub_scheme = '' 377 | 378 | # The unique identifier of the text. This can be a ISBN number 379 | # or the project homepage. 380 | # 381 | # epub_identifier = '' 382 | 383 | # A unique identification for the text. 384 | # 385 | # epub_uid = '' 386 | 387 | # A tuple containing the cover image and cover page html template filenames. 388 | # 389 | # epub_cover = () 390 | 391 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 392 | # 393 | # epub_guide = () 394 | 395 | # HTML files that should be inserted before the pages created by sphinx. 396 | # The format is a list of tuples containing the path and title. 397 | # 398 | # epub_pre_files = [] 399 | 400 | # HTML files that should be inserted after the pages created by sphinx. 401 | # The format is a list of tuples containing the path and title. 402 | # 403 | # epub_post_files = [] 404 | 405 | # A list of files that should not be packed into the epub file. 406 | epub_exclude_files = ['search.html'] 407 | 408 | # The depth of the table of contents in toc.ncx. 409 | # 410 | # epub_tocdepth = 3 411 | 412 | # Allow duplicate toc entries. 413 | # 414 | # epub_tocdup = True 415 | 416 | # Choose between 'default' and 'includehidden'. 417 | # 418 | # epub_tocscope = 'default' 419 | 420 | # Fix unsupported image types using the Pillow. 421 | # 422 | # epub_fix_images = False 423 | 424 | # Scale large images. 425 | # 426 | # epub_max_image_width = 0 427 | 428 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 429 | # 430 | # epub_show_urls = 'inline' 431 | 432 | # If false, no index is generated. 433 | # 434 | # epub_use_index = True 435 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. schemagic documentation master file, created by 2 | sphinx-quickstart on Fri Jun 24 15:31:24 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 schemagic's documentation! 7 | ===================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 4 13 | 14 | schemagic 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | 24 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 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. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\schemagic.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\schemagic.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/schemagic.rst: -------------------------------------------------------------------------------- 1 | schemagic package 2 | ================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | schemagic.core module 8 | --------------------- 9 | 10 | .. automodule:: schemagic.core 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | schemagic.func module 16 | --------------------- 17 | 18 | .. automodule:: schemagic.func 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | schemagic.utils module 24 | ---------------------- 25 | 26 | .. automodule:: schemagic.utils 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | schemagic.validators module 32 | --------------------------- 33 | 34 | .. automodule:: schemagic.validators 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | schemagic.web module 40 | -------------------- 41 | 42 | .. automodule:: schemagic.web 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | 48 | Module contents 49 | --------------- 50 | 51 | .. automodule:: schemagic 52 | :members: 53 | :undoc-members: 54 | :show-inheritance: 55 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==6.6 2 | exception==0.1.0 3 | Flask==0.11.1 4 | itsdangerous==0.24 5 | Jinja2==2.8 6 | MarkupSafe==0.23 7 | pluggy==0.3.1 8 | py==1.4.31 9 | simplejson==3.8.2 10 | six==1.10.0 11 | tox==2.3.1 12 | virtualenv==15.0.2 13 | Werkzeug==0.11.10 14 | -------------------------------------------------------------------------------- /schemagic/__init__.py: -------------------------------------------------------------------------------- 1 | from schemagic.core import * 2 | 3 | 4 | __version__ = '0.9.1' 5 | __author__ = 'Tyler Tolton' 6 | __authoremail__ = 'tjtolton@gmail.com' 7 | __url__ = 'https://github.com/Mechrophile/schemagic' 8 | __license__ = "LGPL" 9 | __description__ = 'Define the shape of your data with simple python data structures. Use those data descriptions to validate your application.' -------------------------------------------------------------------------------- /schemagic/core.py: -------------------------------------------------------------------------------- 1 | import collections 2 | from functools import partial 3 | import types 4 | 5 | import itertools 6 | 7 | from schemagic.utils import merge_with, multiple_dispatch_fn, is_string 8 | 9 | _WHEN_DEBUGGING = lambda: __debug__ 10 | 11 | 12 | def validate_map_template(schema, value): 13 | """Ensures all the keys and values of the given data are valid with the schema's key and value validators 14 | 15 | :param schema: 16 | A map template, i.e. a dict with one item, and the key is not a string, 17 | e.g. ``{int: str}`` 18 | :param value: Any data which will be checked to make sure it matches the prescribed pattern 19 | :return: The data after it has been run through its validators. 20 | """ 21 | key_schema, value_schema = list(schema.items())[0] 22 | validate_key_val_pair = lambda key, val: ( 23 | validate_against_schema(key_schema, key), validate_against_schema(value_schema, val)) 24 | return dict(map(validate_key_val_pair, value.keys(), value.values())) 25 | 26 | 27 | def validate_keyed_mapping(schema, value): 28 | """Ensures all required keys are present, and that their corresponding value matches with the schema's prescription 29 | 30 | :param schema: 31 | A map of strings to data definitions, 32 | e.g. ``{"name": str, "age": int}`` 33 | :param value: Any data which will be checked to make sure it matches the prescribed pattern 34 | :return: The data after it has been run through its validators. 35 | """ 36 | missing_keys = set(schema.keys()) - set(value.keys()) 37 | if missing_keys: 38 | raise ValueError("Missing keys {0} for value {1}".format(missing_keys, value)) 39 | return merge_with(validate_against_schema, schema, value) 40 | 41 | 42 | def validate_sequence_template(schema, value): 43 | """Ensures each item of the value is of the patterns specified by the schema['s first element]. 44 | 45 | :param schema: 46 | A sequence of one element, and that element is a data definition, 47 | e.g. ``[int] or [{str: int}]`` 48 | :param value: Any data which will be checked to make sure it matches the prescribed pattern 49 | :return: The data after it has been run through its validators. 50 | """ 51 | return list(map(validate_against_schema, itertools.repeat(schema[0], len(value)), value)) 52 | 53 | 54 | def validate_strict_sequence(schema, value): 55 | """Ensures that the elements of the value are in the same order and valid with the same definitions in the schema. 56 | 57 | :param schema: 58 | A sequence of data definitions, 59 | e.g. ``[int, {str:int}, [str, int, int], {"age": int}]`` 60 | :param value: Any data which will be checked to make sure it matches the prescribed pattern 61 | :return: The data after it has been run through its validators. 62 | """ 63 | if not len(schema) == len(value): 64 | raise ValueError( 65 | "sequence has a different number of elements than its schema prescribes. value: {0}, schema: {1}".format( 66 | value, schema)) 67 | return list(map(validate_against_schema, schema, value)) 68 | 69 | 70 | _is_map_template = lambda schema: isinstance(schema, collections.MutableMapping) and len(schema.items()) is 1 and not is_string(list(schema.keys())[0]) 71 | _is_keyed_mapping = lambda schema: isinstance(schema, collections.MutableMapping) and not _is_map_template(schema) 72 | _is_sequence_template = lambda schema: isinstance(schema, collections.Sequence) and len(schema) is 1 73 | _is_strict_sequence = lambda schema: isinstance(schema, collections.Sequence) and 1 < len(schema) 74 | 75 | _validate_against_schema = multiple_dispatch_fn({ 76 | lambda schema, value: _is_sequence_template(schema): validate_sequence_template, 77 | lambda schema, value: _is_strict_sequence(schema): validate_strict_sequence, 78 | lambda schema, value: _is_map_template(schema): validate_map_template, 79 | lambda schema, value: _is_keyed_mapping(schema): validate_keyed_mapping}, 80 | default=lambda schema, value: schema(value)) 81 | validate_against_schema = lambda schema, value: _validate_against_schema(schema, value) 82 | validate_against_schema.__doc__ = \ 83 | """Testing to ensure changes to docs are getting reflected in generated documentation 84 | Ensures that the data is valid with the given schema 85 | 86 | :param schema: A data definition. This definition can take any of 5 forms -- 87 | 88 | 1. **function**: 89 | the function will be called fn(data) and expected to return the data, if correct, or throw an error 90 | e.g. ``int`` 91 | 2. **map template**: 92 | a dict with one item, where both the key and value are data definitions, 93 | e.g. ``{int: [str]}`` 94 | 3. **keyed mapping**: 95 | A map of strings to data definitions, 96 | e.g. ``{"name": str, "age": int}`` 97 | 4. **sequence template**: 98 | A one element sequence, where the element is a data definition, 99 | e.g. ``[int] or [{str: int}]`` 100 | 5. **strict sequence**: 101 | A sequence of data definitions, 102 | e.g. ``[int, {str:int}, [str, int, int], {"age": int}]`` 103 | 104 | 105 | Notable things that do **not** count as data definitions include primitive data such as strings, integers, or bools. 106 | These data could be used as components of data definition, but should not be used alone in places that expect 107 | data definitions. For instance ``["hello"]`` is not a valid sequence template, because the element in it is not a data 108 | definition. 109 | 110 | I suppose you could also compose custom classes into your data definitions if you wanted. you heathen. ;) 111 | 112 | :param value: Any data which will be checked to make sure it matches the prescribed pattern 113 | :return: The data after it has been run through its validators. 114 | """ 115 | 116 | 117 | def validator(schema, subject_name_str, validation_predicate=None, coerce_data=False, data=None): 118 | """Creates a validation function which conditionally applies validation and coercion to data 119 | 120 | :param schema: a data definition as described in the function ``validate_against_schema`` 121 | :param subject_name_str: 122 | a string which will be passed into the error message if validation fails. 123 | an example of an error message with ``subject_name_str="My Business Type"`` 124 | 125 | .. code-block:: python 126 | 127 | ValueError: Bad value provided for My Business Type. - error: schema: value: 128 | 129 | :param validation_predicate: 130 | a function that takes no arguments and returns a boolean. This will be called before 131 | validation occurs. If it returns False, validation will be skipped. 132 | :param coerce_data: 133 | True or False - indicates whether the validator should return the output of applying 134 | ``validate_against_schema`` to the data, or simply return the original data. 135 | **NOTE**: if your validator applies functions that mutate their inputs, the data may be altered regardless of the 136 | value of this parameter. Mutable state. Not even once. 137 | :param data: 138 | The data which will ultimately be passed into the validator. 139 | :return: 140 | * when data is not supplied - returns a validator function (i.e. it returns a copy of validator with all arguments supplied except data) 141 | * when data is supplied: returns data (possibly modified, depending on the value of ``coerce_data``) if the data 142 | is valid with the given schema, else throws a ValueError. 143 | """ 144 | if data is None: 145 | return partial(validator, schema, subject_name_str, validation_predicate, coerce_data) 146 | validation_predicate = validation_predicate or _WHEN_DEBUGGING 147 | if not validation_predicate(): 148 | return data 149 | try: 150 | coerced_and_validated_data = validate_against_schema(schema, data) 151 | return coerced_and_validated_data if coerce_data else data 152 | except Exception as e: 153 | message_details = { 154 | "subject": subject_name_str, 155 | "error": "{0}: {1}".format(e.__class__.__name__, e), 156 | "value": data, 157 | "schema": schema 158 | } 159 | raise ValueError("Bad value provided for {subject}. - error: {error} schema: {schema} value: {value}".format( 160 | **message_details)) 161 | -------------------------------------------------------------------------------- /schemagic/func.py: -------------------------------------------------------------------------------- 1 | """ 2 | NOTE EVERYTHING IN THIS FILE IS EXPERIMENTAL. DO NOT EXPECT STABILITY OR USABILITY IN CURRENT FORM 3 | """ 4 | 5 | from functools import partial, wraps 6 | 7 | from schemagic.core import validate_against_schema, validator 8 | from schemagic.validators import date_string 9 | 10 | ALWAYS = lambda: True 11 | WHEN_DEBUGGING = lambda: __debug__ 12 | IDENTITY = lambda x: x 13 | 14 | 15 | 16 | def validated(validation_predicate=None, coerce_data=True, input_schema=None, output_schema=None, fn=None): 17 | if fn is None: 18 | return partial(validated, validation_predicate, coerce_data, input_schema, output_schema) 19 | 20 | validation_predicate = validation_predicate or WHEN_DEBUGGING 21 | input_validator = validator(input_schema or IDENTITY, "input to function {0}".format(fn.__name__), validation_predicate=validation_predicate, coerce_data=coerce_data) 22 | output_validator = validator(output_schema or IDENTITY, "output from function {0}".format(fn.__name__), validation_predicate=validation_predicate, coerce_data=coerce_data) 23 | 24 | @wraps(fn) 25 | def _fn(*args, **kwargs): 26 | validated_args, validated_kwargs = validate_function_input(input_validator, args, kwargs) 27 | return output_validator(fn(*validated_args, **validated_kwargs)) 28 | return _fn 29 | 30 | def validate_function_input(input_validator, arg_list, kwarg_dict): 31 | if arg_list and kwarg_dict: 32 | validated_input = input_validator(arg_list + tuple([kwarg_dict])) 33 | args, kwargs = validated_input[:-1], validated_input[-1] 34 | elif kwarg_dict: 35 | validated_input = input_validator(kwarg_dict) 36 | args, kwargs = [], validated_input 37 | elif arg_list and len(arg_list) > 1: 38 | validated_input = input_validator(arg_list) 39 | args, kwargs = validated_input, {} 40 | elif arg_list and len(arg_list) is 1: 41 | validated_input = input_validator(arg_list[0]) 42 | args, kwargs = [validated_input], {} 43 | else: 44 | args, kwargs = arg_list, kwarg_dict 45 | return args, kwargs -------------------------------------------------------------------------------- /schemagic/utils.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import doctest 3 | import functools 4 | from contextlib import contextmanager 5 | from functools import wraps, update_wrapper, partial 6 | 7 | import operator 8 | 9 | import itertools 10 | 11 | 12 | def merge_with(fn, a, b): 13 | """ returns a new dictionary that is the merger of a and b. applies fn to the values of colliding keys. 14 | 15 | >>> merge_with(operator.add, {"a": 1}, {"a": 1, "b": 1}) 16 | {'a': 2, 'b': 1} 17 | """ 18 | fresh_dict = copy.deepcopy(a) 19 | for k, v in b.items(): 20 | if k in fresh_dict: 21 | fresh_dict[k] = fn(fresh_dict[k], v) 22 | else: 23 | fresh_dict[k] = v 24 | return fresh_dict 25 | 26 | merge = partial(merge_with, lambda a, b: b) 27 | merge.__name__ = "merge" 28 | 29 | def multiple_dispatch_fn(dispatch_map, default=None): 30 | """ 31 | creates a multiple dispatch function. 32 | 33 | returns a function whose implementation of the function is based on the arguments passed to it. it decides 34 | what implementation to use by testing the arguments against a series of predicates to detect what situation 35 | is applicable. 36 | 37 | For example, 38 | 39 | >>> add_if_ints_multiply_if_floats = multiple_dispatch_fn( 40 | ... {lambda *nums: all(isinstance(num, int) for num in nums): operator.add, 41 | ... lambda *nums: all(isinstance(num, float) for num in nums): operator.mul}) 42 | >>> add_if_ints_multiply_if_floats(10, 10) 43 | 20 44 | >>> add_if_ints_multiply_if_floats(10.0, 10.0) 45 | 100.0 46 | 47 | You can also provide a default implementation to use if none of the predicates match. 48 | For example, 49 | 50 | >>> add_if_ints_else_return_unmodified = multiple_dispatch_fn( 51 | ... {lambda *nums: all(isinstance(num, int) for num in nums): operator.add}, 52 | ... default=lambda *items: items) 53 | >>> add_if_ints_else_return_unmodified(25, 25) 54 | 50 55 | >>> add_if_ints_else_return_unmodified("hello", None) 56 | ('hello', None) 57 | 58 | :param dispatch_map: mapping {predicate_fn: implementation_fn} 59 | :param default: implementation fn to be used if none of the predicates are satisfied 60 | :return: 61 | """ 62 | dispatch_map = dispatch_map or {} 63 | def _fn(*args, **kwargs): 64 | try: 65 | dispatch_fn = next(itertools.chain([value for key, value in dispatch_map.items() if key(*args, **kwargs)], 66 | [default] if default else [])) 67 | return dispatch_fn(*args, **kwargs) 68 | except StopIteration: 69 | raise ValueError("No dispatch function found. args: {0}, kwargs: {1}".format(args, kwargs)) 70 | return _fn 71 | 72 | def remove_key(dict_, key): 73 | del dict_[key] 74 | return dict_ 75 | 76 | def separate_dict(initial_dict, *keys_to_remove): 77 | """returns 2 new dicts, one with some keys removed, and another with only those keys""" 78 | part1, part2 = copy.copy(initial_dict), {} 79 | for key, val in part1.items(): 80 | if key in keys_to_remove: 81 | part2[key] = val 82 | 83 | return functools.reduce(remove_key, part2.keys(), part1), part2 84 | 85 | def is_string(obj): 86 | try: 87 | return isinstance(obj, basestring) 88 | except NameError: 89 | return isinstance(obj, str) 90 | 91 | @contextmanager 92 | def assert_raises(expected_error=None): 93 | try: 94 | yield 95 | except Exception as e: 96 | if expected_error: 97 | assert isinstance(e, expected_error), "Expected to raise type(s): {0}, raised {1} instead".format(expected_error, e.__class__.__name__) 98 | else: 99 | # No specific error raised, so we're good. 100 | pass 101 | else: 102 | raise AssertionError("No exception raised") 103 | 104 | if __name__ == '__main__': 105 | doctest.testmod() -------------------------------------------------------------------------------- /schemagic/validators.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import partial, update_wrapper 3 | 4 | from schemagic.core import validate_against_schema 5 | from schemagic.utils import merge 6 | 7 | 8 | def predicate_validator(predicate, name=None, coercer=None, message=None, data=None): 9 | """Builds new validator function that tests, and optionally coerces, data against the supplied predicate and coercer 10 | 11 | :param predicate: function that accepts one argument, the data, returns true if data is good, false otherwise. 12 | :param name: name of the supplied predicate. useful when building validators from anonymous functions. 13 | :param coercer: a function that accepts the data and returns a modification of that data. If no coercer is provided, 14 | the data will still be subject to any coercions that occur within the validation. This is to allow for additional 15 | flexibility, for instance, you may want to convert a datetime string into a datatime object before validating it. 16 | :param message: A message that described the problem with the data if it wasn't validated correctly. 17 | This message will be automatically suffixed with a printout of the data recieved by the validator. 18 | If message is not provided, a default message is used that references the predicate by name. 19 | :param data: the data to be validated 20 | :return: if data is not supplied, returns a copy of the predicate validator function with all the other 21 | values "filled in". i.e. it returns a curried function. 22 | If the data is supplied, returns the, possibly transformed, data if it is valid, else throws an error. 23 | """ 24 | predicate.__name__ = name or predicate.__name__ 25 | if data is None: 26 | return update_wrapper(partial(predicate_validator, predicate, name, coercer, message), predicate) 27 | data = coercer(data) if coercer else data 28 | message = (message or "data did not meet requirements of the predicate {0}".format(predicate.__name__)) + "\n value: {0}".format(data) 29 | if not predicate(data): 30 | raise ValueError(message) 31 | return data 32 | 33 | formatted_string = lambda str_format, **kwargs: predicate_validator( 34 | lambda data: re.match(str_format, data), 35 | **merge(dict(name="formatted_string: {0}".format(str_format), 36 | coercer=str, 37 | message="string not of expected format: expected: {0}".format(format)), 38 | kwargs)) 39 | """Stringifies the data, then matches it against the supplied regex string. Valid if match is returned""" 40 | 41 | #: ``formatted_string(r'\d+\-\d+\-\d+')``: checks to see if the data is of the type returned by stringifying a datetime.date object 42 | date_string = formatted_string(r'\d+\-\d+\-\d+') 43 | 44 | #: ``formatted_string(r'\d+\-\d+\-\d+ \d+:\d+:\d+\.\d+')``: checks to see if the data is of the type returned by stringifying a datetime.datetime object 45 | datetime_string = formatted_string(r'\d+\-\d+\-\d+ \d+:\d+:\d+\.\d+') 46 | 47 | #: ``predicate_validator``: Usually composed with or_, checks to see if the data is the value None 48 | null = predicate_validator(lambda val: val is None, name="null") 49 | 50 | or_ = lambda *schemata: predicate_validator( 51 | lambda val: any(validate_against_schema(schema, val) for schema in schemata), 52 | name="any of schema's {0}".format(schemata), 53 | ) 54 | """checks to see if the data is valid with any of the given data definitions""" 55 | 56 | 57 | enum = lambda *possible_vals: predicate_validator( 58 | lambda val: val in possible_vals, 59 | name="enumeration of allowable values: {0}".format(possible_vals), 60 | ) 61 | """checks to see if the data is one of the provided values""" 62 | -------------------------------------------------------------------------------- /schemagic/web.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import simplejson as json 3 | from functools import partial, update_wrapper 4 | import collections 5 | 6 | from flask.globals import request 7 | from flask.wrappers import Response 8 | 9 | from schemagic.core import validate_against_schema, validator 10 | from schemagic.utils import multiple_dispatch_fn, is_string 11 | 12 | _ALWAYS = lambda: True 13 | _WHEN_DEBUGGING = lambda: __debug__ 14 | _IDENTITY = lambda x: x 15 | 16 | _dispatch_to_fn = multiple_dispatch_fn({ 17 | lambda fn, args: is_string(args): lambda fn, arg_list: fn(arg_list), 18 | lambda fn, args: isinstance(args, collections.Sequence): lambda fn, arg_list: fn(*arg_list), 19 | lambda fn, args: isinstance(args, collections.MutableMapping): lambda fn, arg_list: fn(**arg_list)}, 20 | default=lambda fn, arg_list: fn(arg_list) 21 | ) 22 | dispatch_to_fn = lambda fn, args: _dispatch_to_fn(fn, args) 23 | dispatch_to_fn.__doc__ = \ 24 | """Dispatches a json object to a function. The way the data is applied depends on the structure of the data. 25 | * if the data is a sequence, it will unpack it and pass each item into the function, i.e. it will use ``*args`` 26 | * if the data is a mapping, it will unpack it and pass in the items as keywords, i.e. it will use ``**kwargs`` 27 | * if the data is anything else (i.e. it is a primitive, non iterable), it will pass it in directly. 28 | 29 | **NOTE** 30 | an important "gotcha" of this implementation is that a function that expects a single, iterable object 31 | will have to have its argument passed to it by keyword. This causes a lot builtin functions in earlier versions 32 | of python to be ineligible for this kind of dispatch. For instance, the sum function in 2.7 takes a single 33 | iterable argument, and that argument can not be passed by keyword. as such, this function can not be used 34 | to dispatch json to the sum function in 2.7 35 | 36 | :param fn: the function which is to recieve the values from the arg data 37 | :param args: a data structure (usually rehydrated json) that is to be applied piecemeal to the function. see 38 | rules presented above. 39 | """ 40 | 41 | 42 | def _process_error(exception): 43 | """Decomposition of the webservice fn handler. returns 400 if the exception occurred in the input validation 44 | 45 | :param exception: The Exception which occured as a part of processing the request 46 | :return: a flask Response that more specifically identifies the cause of the problem. 47 | """ 48 | if "input" in exception.args[0]: 49 | return Response( 50 | response=exception, 51 | status=400) 52 | return Response( 53 | status=500, 54 | response=exception) 55 | 56 | 57 | def webservice_fn(fn, input_validator, output_validator): 58 | """Handles the minutia of pulling data from the request object and passing it into the function and validators 59 | 60 | :param fn: the function which is supposed to fulfill the contract defined by the input and output schemata 61 | :param input_validator: a ``validator`` as described in the core function of the same name 62 | :param output_validator: a ``validator`` as described in the core function of the same name 63 | :return: a json Flask Response that contains either the requested data or an error. 64 | """ 65 | try: 66 | return Response( 67 | response= functools.reduce(lambda x, y: y(x),[ 68 | json.loads, 69 | partial(validate_against_schema, input_validator), 70 | partial(dispatch_to_fn, fn), 71 | partial(validate_against_schema, output_validator), 72 | json.dumps 73 | ], request.data), 74 | status=200, 75 | mimetype="application/json" 76 | ) 77 | except Exception as e: 78 | return _process_error(e) 79 | 80 | 81 | def service_route(service, validation_pred=None, coerce_data=True, rule=None, input_schema=None, output_schema=None, fn=None): 82 | """Function decorator that registers a ``webservice_fn`` version of the function on the provided service. 83 | 84 | Note that this function is used primarily to register functions en masse with the ``service_registry`` interface. 85 | However, it can be used as a traditional decorator if desired. e.g.: 86 | 87 | .. code_block:: python 88 | 89 | my_service_route = partial(service_route, my_service) 90 | 91 | @my_service_route(rule="/this-route", input_schema=[int], output_schema=int) 92 | def my_sum(*ints): 93 | return sum(ints) 94 | 95 | I find there to be 2 important pitfalls to bear in mind if using the decorator this way: 96 | 1. This makes it seem like the function is being somehow modified, which it is not. 97 | It can confuse people reading your code into thinking they have to make separate, dedicated webservice versions 98 | of the function in order to register them. 99 | 2. It becomes unsafe to use positional arguments when using the decorator like this. If I had defined that decorator 100 | using the canonical flask pattern, e.g. ``@my_service_route("/this-route")`` it would have caused everything to 101 | explode. To get consistent behavior, you MUST specify the arguments by keyword. 102 | 103 | :param service: The service or app which is to have the rule added to it. Must support the ``add_url_rule`` interface 104 | as described in the `flask documentation `_. 105 | 106 | :param validation_pred: see description in ``validator`` fn of the same param. function that returns true or false 107 | The default value for validation on webservice routes is to use the value of ``__debug__`` as a guide. 108 | :param coerce_data: see description in ``validator`` fn of the same param. boolean flag for coercing data. 109 | The default is to coerce data. This is often very helpful in parsing json from a web request. 110 | :param rule: the url route to use when accessing this function 111 | :param input_schema: a data definition as described in the ``validate_against_schema`` fn documentation. 112 | This value is not required. If none is given, no validation will be done on the input. 113 | :param output_schema: a data definition as decribed in the ``validate_against_schema`` fn documentation. 114 | This value is not required. If none is given, no validation will be done on the output. 115 | :param fn: The function intended to implement the request. 116 | :return: the original function, unmodified. 117 | """ 118 | if not rule: 119 | return update_wrapper(partial(service_route, service, validation_pred, coerce_data), service_route) 120 | if fn is None: 121 | return update_wrapper(partial(service_route, service, validation_pred, coerce_data, rule, input_schema, output_schema), service_route) 122 | 123 | validation_pred = validation_pred or _WHEN_DEBUGGING 124 | input_validator = validator(input_schema or _IDENTITY, "input to endpoint {0}".format(rule), validation_predicate=validation_pred, coerce_data=coerce_data) 125 | output_validator = validator(output_schema or _IDENTITY, "output from endpoint {0}".format(rule), validation_predicate=validation_pred, coerce_data=coerce_data) 126 | 127 | service.add_url_rule( 128 | rule=rule, 129 | endpoint=fn.__name__ if hasattr(fn, "__name__") else rule, 130 | view_func=update_wrapper(lambda: webservice_fn(fn, input_validator, output_validator), fn), 131 | methods=['POST'] 132 | ) 133 | return fn 134 | 135 | 136 | def service_registry(service, validation_pred=None, coerce_data=True, *service_definitions): 137 | """Registers all the service descriptions provided on the app specified by the ``service`` parameter. 138 | 139 | :param service: Service to register functions on. see description of same parameter in ``service_route`` documentation. 140 | :param validation_pred: function returning boolean. see description of same parameter in ``service_route`` documentation. 141 | :param coerce_data: boolean. see description of same parameter in ``service_route`` documentation. 142 | :param service_definitions: mappings that contain the keyword args to service_route. 143 | :return: if service definitions not provided, returns the a function that accepts service definitions. 144 | if service definitions provided, returns nothing. 145 | """ 146 | if not service_definitions: 147 | return update_wrapper(partial(service_registry, service, validation_pred, coerce_data), service_registry) 148 | list(map(lambda definition: service_route(service, validation_pred, coerce_data, **definition), service_definitions)) -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setuptools.command.test import test as TestCommand 3 | import schemagic 4 | import sys 5 | 6 | 7 | NAME = schemagic.__name__ 8 | PACKAGES = ['schemagic'] 9 | VERSION = schemagic.__version__ 10 | LICENSE = schemagic.__license__ 11 | DESCRIPTION = schemagic.__description__ 12 | AUTHOR = schemagic.__author__ 13 | AUTHOR_EMAIL = schemagic.__authoremail__ 14 | URL = schemagic.__url__ 15 | KEYWORDS = ['schema', 'schemas', 'schemata', 16 | 'validate', 'validation', 'validator' 17 | 'json', 'REST', 'webservice', 'flask', 'POST' 18 | 'agile'] 19 | 20 | 21 | class Tox(TestCommand): 22 | def finalize_options(self): 23 | TestCommand.finalize_options(self) 24 | self.test_args = [] 25 | self.test_suite = True 26 | def run_tests(self): 27 | #import here, cause outside the eggs aren't loaded 28 | import tox 29 | errcode = tox.cmdline(self.test_args) 30 | sys.exit(errcode) 31 | 32 | if __name__ == "__main__": 33 | setup( 34 | name=NAME, 35 | description=DESCRIPTION, 36 | license=LICENSE, 37 | url=URL, 38 | version=VERSION, 39 | author=AUTHOR, 40 | author_email=AUTHOR_EMAIL, 41 | keywords=KEYWORDS, 42 | packages=PACKAGES, 43 | tests_require=['tox'], 44 | cmdclass={'test': Tox}, 45 | ) -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Defunctionalize/schemagic/4c9c5e1a7eed2f3016511a609f8da7dcb2a4b3cb/test/__init__.py -------------------------------------------------------------------------------- /test/test_schemagic.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from pprint import pprint 3 | from unittest.case import TestCase 4 | from flask import Flask 5 | import itertools 6 | 7 | from schemagic.core import validate_against_schema 8 | from schemagic.utils import separate_dict 9 | from schemagic.validators import formatted_string, null, or_, enum 10 | from schemagic.web import service_registry 11 | 12 | test_cases = { 13 | validate_against_schema: { 14 | "showing an error when given int instead of list of ints": 15 | dict(schema=[int], 16 | value=5, 17 | result=TypeError), 18 | "returning unmodified list of ints when validating [int] template sequence": 19 | dict(schema=[int], 20 | value=[5, 6], 21 | post_process=list, 22 | result=[5, 6]), 23 | "validating with str function with correct data": 24 | dict(schema=str, 25 | value="hello", 26 | result="hello"), 27 | "validating with int function with incorrect data": 28 | dict(schema=int, 29 | value="hello", 30 | result=ValueError), 31 | "validating map template with correct data": 32 | dict(schema={int: str}, 33 | value={1: "hello", 2: "world"}, 34 | result={1: "hello", 2: "world"}), 35 | "validating strict sequence with good data": 36 | dict(schema=[int, int], 37 | value=[1, 2], 38 | post_process=list, 39 | result=[1, 2]), 40 | "validating strict sequence with bad data": 41 | dict(schema=[int, int], 42 | value=[1], 43 | post_process=list, 44 | result=ValueError), 45 | }, 46 | formatted_string(r"\d+"):{ 47 | "throwing error when incorrectly formatted string passed as data": 48 | dict(data="not a digit", 49 | result=ValueError 50 | ), 51 | "returning string when properly formatted": 52 | dict(data="112233", 53 | result="112233"), 54 | "stringifing data before checking - and returning as string": 55 | dict(data=112233, 56 | result="112233") 57 | }, 58 | null:{ 59 | "throws error when recieves string": 60 | dict(data="hello", 61 | result=ValueError) 62 | }, 63 | or_(int, float):{ 64 | "allowing ints": 65 | dict(data=10, 66 | result=10), 67 | "allowing floats": 68 | dict(data=10.5, 69 | result=10.5), 70 | "throwing error when passed string": 71 | dict(data="hello", 72 | result=ValueError) 73 | }, 74 | enum("Hello", 5):{ 75 | "allowing string \"Hello\"": dict(data="Hello", result="Hello"), 76 | "allowing int 5": dict(data=5, result=5), 77 | "rejecting int 6": dict(data=6, result=ValueError), 78 | "rejecting string \"World\"": dict(data="World", result=ValueError) 79 | } 80 | } 81 | 82 | def capture_errors(fn_with_possible_errors): 83 | try: 84 | return fn_with_possible_errors() 85 | except Exception as e: 86 | return e.__class__ 87 | 88 | 89 | def run_tests(test_cases): 90 | test_results = defaultdict(list) 91 | for test_fn, test_definitions in test_cases.items(): 92 | for test_motivation, test_definition in test_definitions.items(): 93 | split_out_test_parameters = separate_dict(test_definition, "result", "post_process") 94 | test_kwargs, expected_result, post_process = split_out_test_parameters[0], split_out_test_parameters[1]["result"], split_out_test_parameters[1].get("post_process", lambda x: x) 95 | test_result = capture_errors(lambda: post_process(test_fn(**test_kwargs))) 96 | test_results[test_fn.__name__].append(test_result == expected_result or "Not {0} as expected. expected: {1} got: {2}".format(test_motivation, expected_result, test_result)) 97 | return test_results 98 | 99 | class SchemagicTests(TestCase): 100 | def test_all_test_cases_passing(self): 101 | test_results = run_tests(test_cases) 102 | if not all(result is True for result in itertools.chain.from_iterable(test_results.values())): 103 | self.fail(test_results) 104 | 105 | class SchemagicWebTest(TestCase): 106 | def setUp(self): 107 | test_app = Flask("testing") 108 | 109 | test_app.config['TESTING'] = True 110 | self.app = test_app 111 | self.test_client = test_app.test_client() 112 | 113 | def test_normal_flask_routing(self): 114 | self.app.add_url_rule("/", view_func=lambda: "Hello") 115 | self.test_client.get('/') 116 | 117 | def test_service_registry(self): 118 | register_test_services = service_registry(self.app) 119 | register_test_services( 120 | dict(rule="/new-route", 121 | input_schema=[int], 122 | output_schema=int, 123 | fn=lambda *args: sum(args) 124 | ) 125 | ) 126 | bad_request = self.test_client.get("/new-route") 127 | self.assertEqual(bad_request._status_code, 405) 128 | 129 | good_request = self.test_client.post("/new-route", data="[1, 2]") 130 | self.assertEqual(good_request._status_code, 200) 131 | self.assertEqual(int(good_request.data), 3) 132 | 133 | bad_schema_request = self.test_client.post("/new-route", data='["Not an Int"]') 134 | self.assertEqual(bad_schema_request._status_code, 400) -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py34 3 | [testenv] 4 | deps= 5 | pytest 6 | Flask==0.11.1 7 | simplejson==3.8.2 8 | commands= py.test --------------------------------------------------------------------------------