{ "info": { "author": "Matthieu Gouel", "author_email": "matthieu.gouel@gmail.com", "bugtrack_url": null, "classifiers": [ "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Topic :: Utilities" ], "description": "# Spintest\n\nFunctional scenario interpreter.\n\nSpintest is a library that facilitates the integration and functional test of APIs. It takes a list of URLs in parameter and a list of tasks (also called scenario) that will be executed against the specified URLs.\n\nEach task represents an API call and provides some options in order to validate or react to the response. Indeed, by default the task is in success if the HTTP code returned is between `200` and `299` included, but you can specify the error code or the body you expect. You can also provide a list of rollback tasks (or task references) that is executed if the task fails.\n\nAlso, the response of the API call can be stored in order to be used in a future task.\n\nFinally, you can choose to run the task scenario concurrently on each URL.\n\n## Installation\n\nYou can install the package using PIP.\n\n```\n$ pip install spintest\n```\n\n## URLs and tasks definition\n\nThe url list is just a list of endpoints. A route added here will not be evaluated because the route definition is set on the task.\n\n```\n[\n \"https://foo.com\",\n \"https://bar.com\"\n]\n```\n\nThe task definition is a little bit more complex. A scenario is a list of tasks possibly dependent to each other.\n\nA single task follow this schema :\n\n```\n{\n \"method\": str,\n Optional(\"route\", default=\"/\"): str,\n Optional(\"name\"): str,\n Optional(\"body\"): dict,\n Optional(\n \"headers\",\n default={\"Accept\": \"application/json\", \"Content-Type\": \"application/json\"},\n ): dict,\n Optional(\"output\"): str,\n Optional(\"expected\"): {\n Optional(\"code\"): int,\n Optional(\"body\"): Or(dict, str),\n Optional(\"expected_match\", default=\"strict\"): Or(\"partial\", \"strict\"),\n },\n Optional(\"retry\", default=0): int,\n Optional(\"delay\", default=1): int,\n Optional(\"ignore\", default=False): bool,\n Optional(\"rollback\"): [Or(str, dict)],\n}\n```\n\n- **method** is the HTTP method of the request (GET, POST, DELETE, ...). Only a valid HTTP method is accepted.\n- **route** (optional) is the route to test on the endpoint. It will be appended of the current URL (default is \"/\")\n- **name** (optional) is the name of the task. Mandatory if you want to use that task in a rollback.\n- **body** (optional) is a request body.\n- **header** (optional) is a dictionary of headers. Default is JSON application headers. For Oauth endpoint you don't need to add the appropriate header with the token (if you specify the token).\n- **output** (optional) Variable definition where Spintest put the result of the call. This result can be used later in an other task using Jinja syntax.\n- **expected** (optional) is an expected HTTP response code or response body.\n - **code** (optional) is the expected HTTP code.\n - **body** (optional) is an expected response body. You can put a value to *null* if you don't want to check the value of a key but you will have to set all keys. It also checks nested list and dictionary unless you put \"null\" instead.\n - **expected_match** is an option to check partially the keys present on your response body. By default it is set to strict.\n- **retry** (optional) is the number of retries if it fails (default is 0).\n- **delay** (optional) is the time in second to wait between retries (default is 1).\n- **ignore** (optional) is if you want to continue the scenario in case of error of this task.\n- **rollback** (optional) is a list of task names or tasks that is triggered in case of failure of the task.\n\n\n## Usage\n\nA first example with a single route.\n\n```python\nfrom spintest import spintest\n\nurls = [\"https://test.com\"]\ntasks = [\n {\n \"method\": \"GET\",\n \"route\": \"test\",\n }\n]\n\nresult = spintest(urls, tasks)\nassert True is result\n```\n\nThis test will perform a GET call into `https://test.com/test` and expect a return code between `200` and `299` included.\n\nHere is another example with an interation between two routes :\n\n```python\nfrom spintest import spintest\n\nurls = [\"https://test.com\"]\ntasks = [\n {\n \"method\": \"POST\",\n \"route\": \"test\",\n \"output\": \"test_output\",\n \"body\": {\"name\": \"Disk1\", \"size\": 20},\n },\n {\n \"method\": \"DELETE\",\n \"route\": \"volumes/{{ test_output['id'] }}\",\n \"expected\": {\"code\": 204},\n }\n]\n\nresult = spintest(urls, tasks)\nassert True is result\n```\n\nAs you can see, the first task has a key `output`. This way you can store the output of this first task into a `test_output` variables and be able to use it in following tasks in Jinja templating language.\nMorevoer, the second task has a key `expected`. Here we want to check a specific return code `204`.\n\nFinally a last example that show how to run tasks in several task in parallel.\n\n```python\nfrom spintest import spintest\n\nurls = [\"https://foo.com\", \"https://bar.com\"]\ntasks = [\n {\n \"method\": \"GET\",\n \"route\": \"test\",\n \"expected\": {\n \"body\": {\"result\": None},\n \"expected_match\": \"partial\",\n }\n }\n]\n\nresult = spintest(urls, tasks, parallel=True)\nassert True is result\n```\n\nHere we provided two URLS and we have added the option `parallel` in `spintest` function. Without this option, the scenario will be executed iteratively on every URLS.\n\nBut with this option, the each task of the scenario will be executed concurrently for every URLS.\n\nOne last word on the expected option. Here we want to validate that a certain key (`result`) is present from the output. We don't mind about the value of this key so we just set it to `None`. The option `expected_match` set to `partial` indicates that we don't want to a task failure if there is more key in the API response than expected.\n\n### Token management\n\nYou can automatically include Oauth token into the task headers.\n\n- You can directly hard code token\n\n```python\nurls = [\"http://test.com\"]\ntasks = []\n\nspintest(urls, tasks, token= 'ABC')\n```\n\n- You can give the generate function that create your token :\n\n```python\nurls = [\"http://test.com\"]\ntasks = []\n\nspintest(urls, tasks, token=create_token)\n```\n\n### Rollback actions\n\nYou can specify a list of rollback task that is executed in case of task failure.\n\n```python\nfrom spintest import spintest\n\nurls = [\"https://test.com\"]\ntasks = [\n {\n \"method\": \"POST\",\n \"route\": \"test\",\n \"rollback\": [\n {\n \"method\": \"DELETE\",\n \"route\": \"test,\n }\n ]\n }\n]\n\nspintest(urls, tasks)\n```\n\nYou can also specify the name of a task to avoid rewriting them.\n\n```python\nfrom spintest import spintest\n\nurls = [\"https://test.com\"]\ntasks = [\n {\n \"method\": \"POST\",\n \"route\": \"test\",\n \"rollback\": [\n \"test_delete\"\n ]\n },\n {\n \"name\": \"test_delete\",\n \"method\": \"DELETE\",\n \"route\": \"test,\n }\n]\n\nspintest(urls, tasks)\n```\n\n### Run the tasks one by one\n\n\nYou can also go further to control the flow of the task execution. It can be useful to perform other actions between tasks (clean up, external settings, ...)\n\n\n```python\n\nimport asyncio\n\nfrom spintest import TaskManager\n\n\nurls = [\"http://test.com\"]\ntasks = [{\"method\": \"GET\", \"route\": \"/test\"}]\ntoken = \"90b7aa25-870a-4dda-a1fc-b57cf0fbf278\"\n\nloop = asyncio.get_event_loop()\n\nmanager = TaskManager(urls, tasks, token=token)\nresult = loop.run_until_complete(manager.next())\n\nassert \"SUCCESS\" == result[\"status\"]\n```\n\n\nThe `next()` method throw a `StopAsyncIteration` if there is no task left to execute.\n\nNote: You can use the method `next()` in parallel mode. In that situation the method returns a list with the result of the task against each URLs.\n\n\n### Type convertion\n\nTask template evaluation always returns a string, but sometimes the target API expects a non-string value,\nit can be useful to convert it to the corresponding type.\n\nSpintest provides a set of json value converters that provide such functionality.\n\n- Int -> Converts value to a `int`\n- List -> Converts value to a `list`\n- Float -> Converts value to a `float`\n- Bool -> Converts value to a `bool`\n\n```python\nfrom spintest import spintest\n\nurls = [\"http://test.com\"]\ntasks = [\n {\n \"method\": \"GET\",\n \"route\": \"persons\",\n \"output\": \"value\",\n # Returns\n # {\n # \"age\": 20,\n # \"height\": 1.85,\n # \"alive\": True,\n # }\n },\n {\n \"method\": \"POST\",\n \"route\": \"persons\",\n \"body\": {\n # int convertion\n \"age_str\": \"{{ value.person['age'] }}\", # {\"age_str\": \"20\"},\n \"age\": Int(\"{{ value.person['age'] }}\"), # {\"age\": 20},\n\n # float convertion\n \"height_str\": \"{{ value.person['height'] }}\", # {\"height_str\": \"1.85\"},\n \"height\": Float(\"{{ value.person['height'] }}\"), # {\"height\": 1.85},\n\n # bool convertion\n \"alive_str\": \"{{ value.person['alive'] }}\", # {\"alive_str\": \"True\"},\n \"alive\": Bool(\"{{ value.person['alive'] }}\"), # {\"alive\": true},\n }\n }\n]\n\nspintest(urls, tasks, token=token_write)\n```\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/societe-generale/spintest", "keywords": "", "license": "BSD-3-Clause", "maintainer": "", "maintainer_email": "", "name": "spintest", "package_url": "https://pypi.org/project/spintest/", "platform": "", "project_url": "https://pypi.org/project/spintest/", "project_urls": { "Homepage": "https://github.com/societe-generale/spintest" }, "release_url": "https://pypi.org/project/spintest/0.1.0/", "requires_dist": [ "colorlog", "jinja2", "schema", "requests" ], "requires_python": "", "summary": "Functional scenario interpreter", "version": "0.1.0" }, "last_serial": 5699264, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "989a8daed58c71cd8df9f012d6fdb003", "sha256": "6d90ef37ae0bed8dd53916701a702b67fe7ecd88eee974c4cd769141631932d3" }, "downloads": -1, "filename": "spintest-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "989a8daed58c71cd8df9f012d6fdb003", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 11110, "upload_time": "2019-08-19T16:22:15", "url": "https://files.pythonhosted.org/packages/e0/61/946b23f72549497c7a68f02da1185e6ab084b0d141e1ae6d56e929868485/spintest-0.1.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "43346b192774c4b4eeb87964fc71b251", "sha256": "607f9c212f62002888e2492ed49614be7d5a235b40dbe216bbe28d2dd263888e" }, "downloads": -1, "filename": "spintest-0.1.0.tar.gz", "has_sig": false, "md5_digest": "43346b192774c4b4eeb87964fc71b251", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 15568, "upload_time": "2019-08-19T16:22:17", "url": "https://files.pythonhosted.org/packages/61/82/b1075f15e248dae926bb5ea144265f6233c0866fea5998f8ffd9f5978883/spintest-0.1.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "989a8daed58c71cd8df9f012d6fdb003", "sha256": "6d90ef37ae0bed8dd53916701a702b67fe7ecd88eee974c4cd769141631932d3" }, "downloads": -1, "filename": "spintest-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "989a8daed58c71cd8df9f012d6fdb003", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 11110, "upload_time": "2019-08-19T16:22:15", "url": "https://files.pythonhosted.org/packages/e0/61/946b23f72549497c7a68f02da1185e6ab084b0d141e1ae6d56e929868485/spintest-0.1.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "43346b192774c4b4eeb87964fc71b251", "sha256": "607f9c212f62002888e2492ed49614be7d5a235b40dbe216bbe28d2dd263888e" }, "downloads": -1, "filename": "spintest-0.1.0.tar.gz", "has_sig": false, "md5_digest": "43346b192774c4b4eeb87964fc71b251", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 15568, "upload_time": "2019-08-19T16:22:17", "url": "https://files.pythonhosted.org/packages/61/82/b1075f15e248dae926bb5ea144265f6233c0866fea5998f8ffd9f5978883/spintest-0.1.0.tar.gz" } ] }