{ "info": { "author": "Bruno Dupuis", "author_email": "lisael@lisael.org", "bugtrack_url": null, "classifiers": [ "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Natural Language :: English", "Programming Language :: Python :: 3.6" ], "description": "[![pipeline status](https://gitlab.com/lisael/santa/badges/master/pipeline.svg)](https://gitlab.com/lisael/santa/commits/master)\n[![coverage report](https://gitlab.com/lisael/santa/badges/master/coverage.svg)](https://gitlab.com/lisael/santa/commits/master)\n\n# Santa\n\nDeclarative JSON-REST api test framework, so simple, it's hard to belive.\n\n- Santa is a concept a 5yo can understand.\n- Santa brings so much magic in your life, it's hard to believe before you see.\n- Santa loves you all equally. QA people, POs, developers, everyone.\n- Writing your wish-list to Santa doesn't involve any Electron-ic or Atom-ic stuff. Just a Notepad.\n- Santa gifts are not limited to what's available on the internet. You can craft your own or\n let the elves fulfill your sweetest dreams.\n- Don't tell Santa he's my fantasy Postman, he'd get upset.\n- You'd better not upset Him.\n\n## Features\n\n- Declarative, git-friendly, YAML syntax\n- Ansible-like architecture and lazy jinja templating\n- Validator/extractor/context_processors\n- Leverage asyncio (with aiohttp client) to parallelize runs\n- Interactive test case builder\n- Almost everything is easily customisable\n - custom validators\n - custom extractors\n - custom context processors\n - custom tasks\n - custom jinja functions\n\n## Quick start\n\n### Install\n\n```\npip install santa-rest-test\n```\n\n### Developer's install\n\nIf you want to contribute to Santa or you wish to run the quick start project,\nyou should install Santa from source:\n\n```sh\nvirtualenv --python=/usr/bin/python3 santa\ncd santa\n. bin/activate\ngit clone https://gitlab.com/lisael/santa.git\ncd santa\npip install -e .\n```\n\n### Test project\n\nTo run the testing project (a modifed version of DRF tutorial):\n\n```sh\nmake testproj_run\n```\n\nAlternatively, use the docker image:\n\n```sh\nmake docker_run_testproj\n```\n\n### Create a project\n\n```sh\nmkdir myproject\ncd myproject\nmkdir {suite,context}\n```\n\n### Add a context\n\nA context is a nested map that is passed from one task to the next one. Each\ntask may use and/or modify the context. \n\n\n```sh\n$EDITOR context/local.yml\n```\n\n```yaml\n# myproject/context/local.yml\n---\ndomain: localhost\nscheme: http\n\nurls:\n default:\n port: 8080\n scheme: \"{{ scheme }}\"\n host: \"{{ domain }}\"\n headers:\n Authorization: \"{{ basic_auth(users.admin.username, users.admin.password) }}\"\n Content-Type: \"application/json\"\n\nusers:\n admin:\n username: admin\n password: admin\n```\n\nThis defines a context that is available in all tests.\n\nThe context layout is free, the only requirements are:\n\n1. The root object is a map\n2. keys of the root map do not start with `_` (reserved for internal use)\n\nHowever, some good practices may emerge and this kind of inventory per type of\nobjects we are to manipulate has proven useful.\n\nThe values are templated. It's important to understand that the templating is lazy\nand is applied when the value is read, and re-evaluated each time the value is read.\n\nThe laziness has two interesting consequences:\n\n1. Not all variable have to be defined when the first test starts.\n - However, it must be defined at the time it's used\n2. When variables are changed, the value of dependent variables is re-evaluated\n - e.g. if `users.admin.password` changes, `urls.default.headers.Authorization` is\n still correct\n\n### Add a test suite\n\n```sh\n$EDITOR suite/snippets.yml\n```\n\n```yaml\n---\n- test:\n name: List snippets\n url:\n $<: urls.default\n path: snippets/\n```\n\nThe URL is not fully defined here, as it extends the context variable `url.default` using\nthe special `$<` key. Only the API endpoint (aka. `path`) is missing in `url.default`.\n\nNo method is defined in our `test`, santa assumes `GET`.\n\n### Run the tests\n\nThe basic syntax of the command is `santa test `\n\n```sh\nsanta test local snippets\n```\n\nThe result shows one test run and did not fail:\n\n```\nsuite/snippets.yml: List snippets: OK\n\nSummary\n**************************************************\ntests: 1 success: 1 skipped: 0 failed: 0\n**************************************************\n```\n\n### Reusing the context\n\nThis local test is nice (albeit small, we're going to expand the test coverage later), but\nwe'd like to run this tests against multiple environments.\n\nHopefully, the context can be extended. This provides a clean way to reuse parts of a base\ncontext in child contexts.\n\nLet's define a base context:\n\n```sh\n$EDITOR context/base.yml\n```\n\n```yaml\n---\ndomain: \"{{ undefined('Domain must be set') }}\"\nscheme: https\n\nurls:\n default:\n scheme: \"{{ scheme }}\"\n host: \"{{ domain }}\"\n headers:\n Authorization: \"{{ basic_auth(users.admin.username, users.admin.password) }}\"\n Content-Type: \"application/json\"\n\nusers:\n admin:\n username: \"{{ undefined('Admin username') }}\"\n password: \"{{ undefined('Admin passord') }}\"\n```\n\n`undefined` is a builtin template function (as is `basic_auth`) that raises an\n`UndefinedError` if the value is accessed. These values are mandatory in\ninherited contexts.\n\nNow we can re-write our context to extend base.yml\n\n```sh\n$EDITOR context/local.yml\n```\n\n```yaml\n---\nextends: base\ndomain: localhost\nscheme: http\n\nurls:\n default:\n port: 8080\n\nusers:\n admin:\n username: admin\n password: admin\n```\n\nRun this:\n\n```sh\nsanta test local snippets\nsuite/snippets.yml: List snippets: OK\n\nSummary\n**************************************************\ntests: 1 success: 1 skipped: 0 failed: 0\n**************************************************\n```\n\n### Assertions\n\nSo far, we did call our API endpoint, but apparently we did not check anything in the result\nof the call. Well actually, we did. The `test` task validate the HTTP status of the response\nagainst sensible defaults (depending on the method).\n\nWe can add a simple validator that checks if expected keys are present in the result:\n\n```sh\n$EDITOR suite/snippets.yml\n```\n\n```yaml\n- test:\n name: List snippets\n url:\n $<: urls.default\n path: snippets/\n validate:\n - json:\n contains:\n - count\n - next\n - previous\n - results\n```\n\nValidators are listed in the `test` argument `validate`\n\n```sh\nsanta test local snippets \nsuite/snippets.yml: List snippets: OK\n\nSummary\n**************************************************\ntests: 1 success: 1 skipped: 0 failed: 0\n**************************************************\n```\n\nNote that the default HTTP status validator is still run. To override the default\nstatus validator, just add your own:\n\n```yaml\n validate:\n - status: [417]\n```\n\n### Going postal\n\nPosting an new snippet is as simple as:\n\n```yaml\n- test:\n name: Post test snippet\n method: POST\n url:\n $<: urls.default\n path: snippets/\n json_body:\n title: test.py\n code: |\n from math import sin, pi\n\n sin(pi/2)\n linenos: false\n language: python\n style: vim\n```\n\nThis works, but we're repeating ourselves a bit, don't we?\n\nThe `url` part is the same as in our \"List snippets\" test. And what if we decide to\nadd a `/blog/` in our API and to move snippets under `/pastebin/` for namespacing\nsake ? Would we change each and every tests that call `/snippets/` to `/pastebin/snippets` ?\n\nWe also want to test many ways to call snippets, each one uses all or a part of the `json_body`\nwe defined here. It would be nice to re-use a json payload with sensible default.\n\nIt's time to leverage the context to enforce the DRY principle. We add the `endpoints` mapping\nthat defines all our endpoints (URLs and payloads). Note that these endpoint are shared amongst\nall environments, so we define them once in the base context.\n\n```sh\n$EDITOR context/base.yml\n```\n\n```yaml\n---\ndomain: \"{{ undefined(\"Domain must be set\") }}\"\nscheme: https\n\nurls:\n default:\n scheme: \"{{ scheme }}\"\n host: \"{{ domain }}\"\n headers:\n Authorization: \"{{ basic_auth(users.admin.username, users.admin.password) }}\"\n Content-Type: \"application/json\"\n\nusers:\n admin:\n username: \"{{ undefined(\"Admin username\") }}\"\n password: \"{{ undefined(\"Admin passord\") }}\"\n\nendpoints:\n snippets:\n url:\n $<: urls.default\n path: snippets/\n json:\n title: \"\"\n code: \"\"\n linenos: false\n language: null\n style: vim\n```\n\nChange the suite accordingly:\n\n\n```sh\n$EDITOR suite/snippets.yml\n```\n\n```yaml\n- test:\n name: List snippets\n url:\n $<: endpoints.snippets.url\n validate:\n - json:\n contains:\n - count\n - next\n - previous\n - results\n\n- test:\n name: Post test snippet\n method: POST\n url:\n $<: endpoints.snippets.url\n json_body:\n $<: endpoints.snippets.json\n title: test.py\n code: |\n from math import sin, pi\n\n sin(pi/2)\n language: python\n```\n\n-- BEGIN TROLL --\n\nIt may seem strange to some users to define JSON values in yaml. It's also harder to copy and\npaste real payloads.\n\nI, for myself, think that JSON is not a human language and should never be used as a\nhuman-to-machine format.\n\nAnyway, remember that JSON is valid YAML ;).\n\n-- END OF TROLL (quickly resolved, hopefully) --\n\n```sh\nsanta test local snippets \n```\n\n```\nsuite/snippets.yml: List snippets: OK\nsuite/snippets.yml: Post test snippet: OK\n\nSummary\n**************************************************\ntests: 2 success: 2 skipped: 0 failed: 0\n**************************************************\n```\n\n### Manipulating the context\n\nThere are 4 ways to update a context:\n\n1. Define another context that `extends` the first one\n2. Pass an `--extra-var` option at the invocation of the suite\n3. Extract data from the result JSON in a test\n4. Manipulate the context with `context_processors` defined in the suite.\n\nWe've already show the first way and the second is pretty clear. These two methods are\nstatic in the sense that they update the context before the suite is run. The third and\nfourth methods, on the other hand are dynamic. They are defined in the suite itself and\nreact to events occuring during the tests.\n\n#### Extractors\n\nSay that we want to keep track of the snippet we've just posted. On a POST, the API\ngive us the id of the created snippet in the result JSON. Using an extractor, we can\nbind a context variable to any value. Let's add an extractor to the POST:\n\n```sh\n$EDITOR suite/snippets.yml\n```\n\n```yaml\n\n# ...\n\n- test:\n name: Post test snippet\n method: POST\n url:\n $<: endpoints.snippets.url\n json_body:\n $<: endpoints.snippets.json\n title: test.py\n code: |\n from math import sin, pi\n\n sin(pi/2)\n language: python\n extract:\n - jq:\n pattern: .id\n bind: snippets.first.id\n - jq:\n pattern: .url\n bind: snippets.first.full_url\n```\n\nWe use the jq extractor that binds a context variable to the result of the given\njq pattern.\n\nNote that if a key is not found on the path of the bond variable, it is simply\ncreated, e.g. those extractors created `snippets`, `snippets.first`, `snippets.first.id`\n(bound to the new snippet id) and `snippets.first.full_url` (bound to the new snippet\nurl)\n\n\n#### Context processors\n\nIf we are to call the new created snippet, we now have to define the URL like this\n\n```yaml\n- test:\n name: Get first test\n url:\n $<: endpoints.snippets.url\n path: \"{{ endpoints.snippets.url}}{{ snippets.first.id }}/\"\n```\n\nFor the sake or DRYness, we can register this url in `snippets.first` context mapping.\n\nThat's exactly what context processors are for:\n\n```sh\n$EDITOR suite/snippets.yml\n```\n\n```yaml\n\n# ...\n\n- context:\n - copy:\n src: endpoints.snippets.url\n dest: snippets.first.url\n - update:\n snippets.first.url.path: \"{{ endpoints.snippets.url.path }}{{ snippets.first.id }}/\"\n\n```\n\n`context` is a new task type, that accesses the context. It's a list of context processors that\nare run in the order of definition. Here, at first we copy the snippets URL into the new snippet's\ninfo mapping and then we update the path to point to the id of the snippet.\n\nWe can check that the contex processor did its job using the `message` task that prints nice formated\nmessages or raw yaml representing the context:\n\n```sh\n$EDITOR suite/snippets.yml\n```\n\n```yaml\n\n# ...\n\n- message:\n message: The full url is {{ snippets.first.full_url }}\n context: \n - snippets\n```\n\n```sh\nsanta test local snippets\n```\n\n```\nsuite/snippets.yml: List snippets: OK\nsuite/snippets.yml: Post test snippet: OK\nsuite/snippets.yml: context: OK\n\nThe full url is http://localhost:8080/snippets/15/\n snippets:\n first:\n full_url: http://localhost:8080/snippets/15/\n id: 15\n url:\n headers:\n Authorization: 'Basic YWRtaW46YWRtaW4='\n Content-Type: application/json\n host: localhost\n path: snippets/15/\n port: 8080\n scheme: http\n\nsuite/snippets.yml: : OK\n\nSummary\n**************************************************\ntests: 2 success: 2 skipped: 0 failed: 0\n**************************************************\n```\n\nNow we can use the API to manipulate the first snippet with less boilerplate YAML:\n\n```sh\n$EDITOR suite/snippets.yml\n```\n\n```yaml\n\n# ... (remove the message to avoid log pollution)\n\n\n- test:\n name: Get new snippet\n method: GET\n url:\n $<: snippets.first.url\n validate:\n - json:\n partial:\n language: python\n```\n\n```sh\nsanta test local snippets \n```\n\n```\nsuite/snippets.yml: List snippets: OK\nsuite/snippets.yml: Post test snippet: OK\nsuite/snippets.yml: context: OK\nsuite/snippets.yml: Get new snippet: OK\n\nSummary\n**************************************************\ntests: 3 success: 3 skipped: 0 failed: 0\n**************************************************\n```\n\n### More context tricks.\n\nThe snippets API is open to multiple users with various permissions. For example, a snippet\nhas an `owner` field. Only the owner of a snippet can edit the snippet. To tests this we need to\nimpersonate multiple users. The authentication is HTTP basic auth, and we defined the `default` URL\n(which in turn is the base of all URLs) like this:\n\n```yaml\n# context/base.yml\n---\n\n# ...\n\nurls:\n default:\n port: 8080\n scheme: \"{{ scheme }}\"\n host: \"{{ domain }}\"\n headers:\n Authorization: \"{{ basic_auth(users.admin.username, users.admin.password) }}\"\n Content-Type: \"application/json\"\n\n# ...\n\n```\n\nThe problem that arise here is that the authenticated user is hard-coded in the context: `admin`.\n\nThe solution is to use the laziness and late evaluation of the context:\n\n```sh\n$EDITOR context/base.yml\n```\n\n```yaml\n# ...\n\nurls:\n default:\n port: 8080\n scheme: \"{{ scheme }}\"\n host: \"{{ domain }}\"\n headers:\n Authorization: \"{{ basic_auth(users.current.username, users.current.password) }}\"\n Content-Type: \"application/json\"\n\n# ...\n```\n\nThe trick consists in using the undefined user `current` to generate the authentication\nheader. Now we have to set this user to any defined user in a `context` task and all\nsubsequent requests will correctly authenticate. \n\nWe have to add the `context` task at the top of the snippets suite:\n\n\n```sh\n$EDITOR suite/snippets.yml\n```\n\n```yaml\n---\n- context:\n - copy:\n src: users.admin\n dest: users.current\n# ...\n```\n\n### More endpoints\n\nLet's test the other endpoint, `/users/`. With all we've already setup it's easy.\n\nFirst, we add the endpoint definition in the base context\n\n```sh\n$EDITOR context/base.yml\n```\n\n```yaml\n# ...\n\nendpoints:\n snippets:\n url:\n $<: urls.default\n path: snippets/\n json:\n title: \"\"\n code: \"\"\n linenos: false\n language: null\n style: vim\n users:\n url:\n $<: urls.default\n path: users/\n```\n\nThen we create a suite:\n\n```sh\n$EDITOR suite/users.yml\n```\n\n```yaml\n---\n- context:\n - copy:\n src: users.admin\n dest: users.current\n\n- test:\n name: Get users\n url:\n $<: endpoints.users.url\n```\n\nWait a minute... That's not DRY, we're repeating the copy of the user `admin` to\n`current`. There must be a way to avoid this.\n\nSuites can include each other. We can use this feature to avoid duplication of code.\n\nCreate a suite called `log_as/admin`:\n\n```sh\n$EDITOR suite/log_as/admin.yml\n```\n\n```yaml\n---\n- context:\n - copy:\n src: users.admin\n dest: users.current\n```\n\nInclude this suite in `users.yml` and `snippets.yml`\n\n```sh\n$EDITOR suite/users.yml\n```\n\n```yaml\n---\n- include: log_as/admin\n\n- test:\n name: Get users\n url:\n $<: endpoints.users.url\n```\n\n```sh\n$EDITOR suite/snippets.yml\n```\n\n```yaml\n---\n- include: log_as/admin\n\n- test:\n# ...\n```\n\nAvoiding code duplication is more future-proof. At the moment, the API implements\nonly basic HTTP authentication. If we decide to implement token authentication or even\nOAuth dance, we only have to change the implementation of `suite/log_as/admin.yml`, and\nthe `include` task abstract the login logic away.\n\nWe can try the new endpoint tests: \n\n```sh\nsanta test local users \n```\n\n```\nsuite/log_as/admin.yml: context: OK\nsuite/users.yml: include: OK\nsuite/users.yml: Get users: OK\n\nSummary\n**************************************************\ntests: 1 success: 1 skipped: 0 failed: 0\n**************************************************\n```\n\nNote that the log lines are printed when the task is completed. That's why the log is\nsomewhat hard to follow. The first line is the included context processor, the second\none tels that the inclusion did complete, and the third is the actual test.\n\nThe logging logic may change in the future. However, if you have to perform a fine\nanalysis of a suite result, use alternate outputs like json or yaml:\n\n```sh\nsanta test local users -o yaml\n```\n\n### Run all the suites at once\n\nWhen we are writing a suite, it's nice to be able to run only what we're currently\nworking at. (e.g. users or snippets). But when the tests are run, we want to run them\nall at once. We can use the include task to create a collection of suites:\n\n\n```sh\n$EDITOR suite/all.yml\n```\n\n```yaml\n---\n- include: snippets\n\n- include: users\n```\n\nRun this...\n\n```sh\nsanta test local all\n```\n\nGuess what... We're somehow repeating ourselves. Each of the included suite\nincludes `log_as/admin` repeating the context update. It's not that\nproblematic here as we use basic HTTP auth and `log_as/admin` only access the\nlocal memory through the context, but logging as an admin user may require a\nHTTP roundtrip or two.\n\nThe naive solution is to include `log_as/admin` at the top of `all.yml` and to\nremove it from `snippets.yml` and `users.yml`. It works, but we would loose the\nability to run the suites in isolation.\n\nThe `require` task is what we need. `require` works like include except that it\nkeeps track of what suite was required and don't repeat the suite.\n\n```sh\n$EDITOR suite/users.yml\n```\n\n```yaml\n---\n- require: log_as/admin\n\n- test:\n# ...\n```\n\n```sh\n$EDITOR suite/snippets.yml\n```\n\n```yaml\n---\n- require: log_as/admin\n\n- test:\n# ...\n```\n\nRun this...\n\n```sh\nsanta test local all\n```\n\n```\nsuite/log_as/admin.yml: context: OK\nsuite/snippets.yml: require: OK\nsuite/snippets.yml: List snippets: OK\nsuite/snippets.yml: Post test snippet: OK\nsuite/snippets.yml: context: OK\nsuite/snippets.yml: Get new snippet: OK\nsuite/all.yml: include: OK\nsuite/users.yml: require: SKIPPED\n suite/log_as/admin.yml already completed\nsuite/users.yml: Get users: OK\nsuite/all.yml: include: OK\n\nSummary\n**************************************************\ntests: 4 success: 4 skipped: 0 failed: 0\n**************************************************\n```\n\nInternally the completed required suite are kept into the context. This should prevent\nfrom login as user1 -> login as user2 -> login as user1 again. `require` accepts a\nsecond argument that resets previously required suites:\n\n```yaml\n- require:\n suite: log_as/user1\n reset: log_as/*\n```\n\n`reset` accept globs, but excludes it's own `suite` from the expanded names.\n\n### Run suites in parallel\n\nIn continuous integration process, the tests must be fast and should raise issues\nas soon as possible. Large functional test suite are slow. To mitigate this issue,\nsanta allows parallel tests.\n\nNow we two have problems.\n\nParallel execution is never simple to handle and this feature must be used with much\nprecautions. To help the user to wrap their head around parallel tests, santa uses\na straightforward technique, the context is simply duplicated and the new context is \npassed to the new \"thread\", with no possibility to share context values among threads.\n\nThis implementation is limiting, and may force the user to repeat themselves but it's\nmuch safer and avoids a whole class of client-side race conditions.\n\nHowever it's not a silver bullet and the user must care about server-side race\nconditions. For example, if a thread counts the snippets in the database while\nanother one creates snippets and a third thread delete snippets, the first thread is \nin trouble to get consistent and reproducible results. We can't do anything on the\nclient side to mitigate this uncertainty.\n\nThat said, with careful implementation of isolated threads, concurrency may substantially\nspeed up your test suite.\n\nTo run tests in parallel, just replace `include` with `fork`:\n\n```sh\n$EDITOR suite/all.yml\n```\n\n```yaml\n---\n- fork: snippets\n\n- fork: users\n```\n\n```sh\nsanta test local all \n```\n\n```\nsuite/log_as/admin.yml: context: OK\nsuite/snippets.yml: require: OK\nsuite/log_as/admin.yml: context: OK\nsuite/users.yml: require: OK\nsuite/snippets.yml: List snippets: OK\nsuite/users.yml: Get users: OK\nsuite/snippets.yml: Post test snippet: OK\nsuite/snippets.yml: context: OK\nsuite/snippets.yml: Get new snippet: OK\nsuite/all.yml: fork: OK\nsuite/all.yml: fork: OK\n\nSummary\n**************************************************\ntests: 4 success: 4 skipped: 0 failed: 0\n**************************************************\n```\n\nBut... `require` did not work as intended, it looks like the second requirement\nof `log_as/admin` was not skipped.\n\nRemember that the context is forked in `fork` and also that `require` keeps track\nof completed suites in the context.\n\nThe context in the suite `users` is not aware of the completion of the requirements\nin `snippets`. Moreover, there is no guarantee about the order of execution of the\ntwo `require` tasks.\n\nThe solution is to require `log_as/admin` in the umbrella suite:\n\n```sh\n$EDITOR suite/all.yml\n```\n\n```yaml\n---\n- require: log_as/admin\n\n- fork: snippets\n\n- fork: users\n```\n\nNow, the admin is logged in at the time the contexts fork and the completed\nrequired suites are known in forked contexts:\n\n```sh\nsanta test local all \n```\n\n```\nsuite/log_as/admin.yml: context: OK\nsuite/all.yml: require: OK\nsuite/snippets.yml: require: SKIPPED\n suite/log_as/admin.yml already completed\nsuite/users.yml: require: SKIPPED\n suite/log_as/admin.yml already completed\nsuite/snippets.yml: List snippets: OK\nsuite/users.yml: Get users: OK\nsuite/snippets.yml: Post test snippet: OK\nsuite/snippets.yml: context: OK\nsuite/snippets.yml: Get new snippet: OK\nsuite/all.yml: fork: OK\nsuite/all.yml: fork: OK\n\nSummary\n**************************************************\ntests: 4 success: 4 skipped: 0 failed: 0\n**************************************************\n```\n\nRight. Both thread did skip the admin login as it was already done.\n\n## Customization\n\nCreate a plugin package\n\n```sh\nmkdir plugins\ntouch plugins/__init__.py\n```\n\nAdd a custom extractor\n\n```\n# the name of the file is not important. Just put stuff where it belong\n$EDITOR plugins/extractor.py\n```\n\n```python\nfrom santa.extractor import Extractor\nfrom santa.parser import IntField\n\nclass Accumulate(Extractor):\n \"\"\" Given an array of int as `src` extract the sum of its values \"\"\"\n # if not set, the yaml name defaults to the lowered classname\n # __yaml_name__ = \"accumulate\"\n src = StringField()\n bind = ContextValueField()\n\n async def __call__(self, response, ctx):\n data = await response.json()\n array = data[self.src]\n ctx.bind(self.bind, sum(array), create=True)\n```\n\nThis class defines an extractor as well as the yaml code required to\ncall it.\n\n```yaml\n---\n- test:\n name: Get the sum of stuff\n method: GET\n url:\n extends: urls.default\n path: /api/v12/stuff\n extract:\n - accumulate:\n src: stuff\n bind: stuff.count\n```\n\nCustom validators and context processors can be implemented the same\nway.\n\n\n\n\n", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/lisael/santa", "keywords": "santa", "license": "GNU General Public License v3", "maintainer": "", "maintainer_email": "", "name": "santa-rest-test", "package_url": "https://pypi.org/project/santa-rest-test/", "platform": "", "project_url": "https://pypi.org/project/santa-rest-test/", "project_urls": { "Homepage": "https://github.com/lisael/santa" }, "release_url": "https://pypi.org/project/santa-rest-test/0.1.0/", "requires_dist": [ "Click (>=6.0)", "pyaml", "Jinja2", "jq", "aiohttp", "ruamel.yaml" ], "requires_python": "", "summary": "Easy API testing.", "version": "0.1.0" }, "last_serial": 4721395, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "032950fc0dec6827736857913b79be37", "sha256": "3fc65b2049ad036b9f59b1890e6b1767795986858d5863d8c40831ee149f5a78" }, "downloads": -1, "filename": "santa_rest_test-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "032950fc0dec6827736857913b79be37", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 40379, "upload_time": "2019-01-21T12:27:11", "url": "https://files.pythonhosted.org/packages/d9/b7/5551c21f9aa11a240a416e05a238117d21b1e350990244f895c18131f51c/santa_rest_test-0.1.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "51a01f910de626071b34f348ad0e8614", "sha256": "b44041d8d3983aaa86ad61b1c7e08eb7ce31f054dd0e2cf80d1b03f4d9dcf11b" }, "downloads": -1, "filename": "santa-rest-test-0.1.0.tar.gz", "has_sig": false, "md5_digest": "51a01f910de626071b34f348ad0e8614", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 30574, "upload_time": "2019-01-21T12:27:12", "url": "https://files.pythonhosted.org/packages/37/ae/108c79457b6792b240e2772c2816104b1a8b387b4a7691345e37cd11d5ba/santa-rest-test-0.1.0.tar.gz" } ], "0.1.0a2": [ { "comment_text": "", "digests": { "md5": "397abf55e9a0a702b79eec8185cafe60", "sha256": "deec946922f110048797564765908882c0789b482f58dd64a325ae1b35b5e128" }, "downloads": -1, "filename": "santa_rest_test-0.1.0a2-py3-none-any.whl", "has_sig": false, "md5_digest": "397abf55e9a0a702b79eec8185cafe60", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 31077, "upload_time": "2019-01-21T09:04:49", "url": "https://files.pythonhosted.org/packages/b7/66/21553ad27df170b96b015fb99bb8a23c3115ad82d2b2d6e535090e29daf6/santa_rest_test-0.1.0a2-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "42658d8b2c7663cc7650307fc17307e3", "sha256": "4942b4d22ff41e031e921b12898caf928ce2ffdf06aae6862c76ef93a6a3dedf" }, "downloads": -1, "filename": "santa-rest-test-0.1.0a2.tar.gz", "has_sig": false, "md5_digest": "42658d8b2c7663cc7650307fc17307e3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 23592, "upload_time": "2019-01-21T09:04:52", "url": "https://files.pythonhosted.org/packages/01/fa/3e50c9f0ce9210e8e1ffcd0c1adc41a62f29adc62079cf75fd456e686fde/santa-rest-test-0.1.0a2.tar.gz" } ], "0.1.0a3": [ { "comment_text": "", "digests": { "md5": "749bd0041ac17aa5c46a0c9a496fd8dd", "sha256": "3472e4eb4967923438c66f66a8e23f32a72f67733d4540f6de6433ea38bfb50a" }, "downloads": -1, "filename": "santa_rest_test-0.1.0a3-py3-none-any.whl", "has_sig": false, "md5_digest": "749bd0041ac17aa5c46a0c9a496fd8dd", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 40396, "upload_time": "2019-01-21T09:31:58", "url": "https://files.pythonhosted.org/packages/39/28/b025208e6db826cc6402633ae37a53faa6ae53ef8a92c5530f200bde63f7/santa_rest_test-0.1.0a3-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "bd549a0cd50be1f9cad7d6a80a4c52af", "sha256": "1a8f8cfa8486481cd077f109820376739b272f10d49ecb0dd4ff61829f7809e8" }, "downloads": -1, "filename": "santa-rest-test-0.1.0a3.tar.gz", "has_sig": false, "md5_digest": "bd549a0cd50be1f9cad7d6a80a4c52af", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 30564, "upload_time": "2019-01-21T09:32:00", "url": "https://files.pythonhosted.org/packages/4f/53/104edfb26f33468cade4782978edac1dc37d81df6b94a93da60edd46af2f/santa-rest-test-0.1.0a3.tar.gz" } ], "0.1.0a5": [ { "comment_text": "", "digests": { "md5": "73524e026c6063efdf4ecb0277bcfff6", "sha256": "bfbac85efe18b1cd6bf19924017cd61facebb92e2798f9305181f02d98a141f1" }, "downloads": -1, "filename": "santa_rest_test-0.1.0a5-py3-none-any.whl", "has_sig": false, "md5_digest": "73524e026c6063efdf4ecb0277bcfff6", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 40406, "upload_time": "2019-01-21T09:34:13", "url": "https://files.pythonhosted.org/packages/a2/bf/2a044a7fb80d042416784c6482c5eabb3112cf6022fea98c5040a0ba146b/santa_rest_test-0.1.0a5-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "005d5f5c67293e488c006bbbb7a38043", "sha256": "79ee187485cc7a13b7be308360666f8e1d708bb6601a9fe5f5e8faaa69194a47" }, "downloads": -1, "filename": "santa-rest-test-0.1.0a5.tar.gz", "has_sig": false, "md5_digest": "005d5f5c67293e488c006bbbb7a38043", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 30573, "upload_time": "2019-01-21T09:34:14", "url": "https://files.pythonhosted.org/packages/1c/4a/c0a3522beb580ea2ffbcfe59f46fb0b53a3b5ec8b104fb39b39aa712cfb7/santa-rest-test-0.1.0a5.tar.gz" } ], "0.1.0a6": [ { "comment_text": "", "digests": { "md5": "d88020a5f3213de155d3237dd8aae564", "sha256": "5c47c6e92b6a52a2e85a525e393412d3ea6c5f53bdbb14cbb10b5c2b90343971" }, "downloads": -1, "filename": "santa_rest_test-0.1.0a6-py3-none-any.whl", "has_sig": false, "md5_digest": "d88020a5f3213de155d3237dd8aae564", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 40405, "upload_time": "2019-01-21T09:37:09", "url": "https://files.pythonhosted.org/packages/5d/b4/3cd2edd8f3951c1dd16ea2263dfe3d50ee8b85b6105bcfb09c393c34698a/santa_rest_test-0.1.0a6-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "ab64931674e4993d0e3f3a3fc2d3622f", "sha256": "55f3490e4cfbd29c3522ed423e536f6aa85a1e2668478d713da2d67574e83ee9" }, "downloads": -1, "filename": "santa-rest-test-0.1.0a6.tar.gz", "has_sig": false, "md5_digest": "ab64931674e4993d0e3f3a3fc2d3622f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 30572, "upload_time": "2019-01-21T09:37:11", "url": "https://files.pythonhosted.org/packages/e6/55/d973c1a325ec4040d92c7557292c05aaf65f2bbf39f00234d6b3be0a1474/santa-rest-test-0.1.0a6.tar.gz" } ], "0.1.0a7": [ { "comment_text": "", "digests": { "md5": "0f1e9db3c0b63bc8505b32c9dddafb79", "sha256": "df30d92d1206af8c1a14802681d4c1834e2b1ecf70d16ed4a1cd886bb8ce0a0e" }, "downloads": -1, "filename": "santa_rest_test-0.1.0a7-py3-none-any.whl", "has_sig": false, "md5_digest": "0f1e9db3c0b63bc8505b32c9dddafb79", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 40407, "upload_time": "2019-01-21T10:21:31", "url": "https://files.pythonhosted.org/packages/b5/20/6f12325651779d57264d5470f522b555e76e4f9753064d1b3b0de6d9a48a/santa_rest_test-0.1.0a7-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "4c6142189328f9d65ad4bfd183a531f4", "sha256": "cad3f33210786686a79b816ee3ec35445e6e22ff6d47b158efa4aabf239ed092" }, "downloads": -1, "filename": "santa-rest-test-0.1.0a7.tar.gz", "has_sig": false, "md5_digest": "4c6142189328f9d65ad4bfd183a531f4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 30572, "upload_time": "2019-01-21T10:21:32", "url": "https://files.pythonhosted.org/packages/a1/ce/c822e617eed4ba1ec601532276bf8f3bbddaa1ca3bf396b32b787dab69c6/santa-rest-test-0.1.0a7.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "032950fc0dec6827736857913b79be37", "sha256": "3fc65b2049ad036b9f59b1890e6b1767795986858d5863d8c40831ee149f5a78" }, "downloads": -1, "filename": "santa_rest_test-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "032950fc0dec6827736857913b79be37", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 40379, "upload_time": "2019-01-21T12:27:11", "url": "https://files.pythonhosted.org/packages/d9/b7/5551c21f9aa11a240a416e05a238117d21b1e350990244f895c18131f51c/santa_rest_test-0.1.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "51a01f910de626071b34f348ad0e8614", "sha256": "b44041d8d3983aaa86ad61b1c7e08eb7ce31f054dd0e2cf80d1b03f4d9dcf11b" }, "downloads": -1, "filename": "santa-rest-test-0.1.0.tar.gz", "has_sig": false, "md5_digest": "51a01f910de626071b34f348ad0e8614", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 30574, "upload_time": "2019-01-21T12:27:12", "url": "https://files.pythonhosted.org/packages/37/ae/108c79457b6792b240e2772c2816104b1a8b387b4a7691345e37cd11d5ba/santa-rest-test-0.1.0.tar.gz" } ] }