From 8190c118510f2e46eecd2231b75ed92bc1c3f353 Mon Sep 17 00:00:00 2001 From: Dmitry Kokorin Date: Mon, 28 Jun 2021 19:11:51 +0300 Subject: [PATCH] [WIP] Python: rest_api --- python/rest-api/.exercism/metadata.json | 1 + python/rest-api/README.md | 82 ++++++++++++ python/rest-api/rest_api.py | 51 ++++++++ python/rest-api/rest_api_test.py | 163 ++++++++++++++++++++++++ 4 files changed, 297 insertions(+) create mode 100644 python/rest-api/.exercism/metadata.json create mode 100644 python/rest-api/README.md create mode 100644 python/rest-api/rest_api.py create mode 100644 python/rest-api/rest_api_test.py diff --git a/python/rest-api/.exercism/metadata.json b/python/rest-api/.exercism/metadata.json new file mode 100644 index 0000000..5ecbcea --- /dev/null +++ b/python/rest-api/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"rest-api","id":"f59934ec5ea24dcfa20ccaabf4dc5f4c","url":"https://exercism.io/my/solutions/f59934ec5ea24dcfa20ccaabf4dc5f4c","handle":"DmitryKokorin","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/python/rest-api/README.md b/python/rest-api/README.md new file mode 100644 index 0000000..2da0060 --- /dev/null +++ b/python/rest-api/README.md @@ -0,0 +1,82 @@ +# Rest Api + +Implement a RESTful API for tracking IOUs. + +Four roommates have a habit of borrowing money from each other frequently, and have trouble remembering who owes whom, and how much. + +Your task is to implement a simple [RESTful API](https://en.wikipedia.org/wiki/Representational_state_transfer) that receives [IOU](https://en.wikipedia.org/wiki/IOU)s as POST requests, and can deliver specified summary information via GET requests. + +### API Specification + +#### User object +```json +{ + "name": "Adam", + "owes": { + "Bob": 12.0, + "Chuck": 4.0, + "Dan": 9.5 + }, + "owed_by": { + "Bob": 6.5, + "Dan": 2.75, + }, + "balance": "<(total owed by other users) - (total owed to other users)>" +} +``` + +#### Methods + +| Description | HTTP Method | URL | Payload Format | Response w/o Payload | Response w/ Payload | +| --- | --- | --- | --- | --- | --- | +| List of user information | GET | /users | `{"users":["Adam","Bob"]}` | `{"users":}` | `{"users": (sorted by name)}` | +| Create user | POST | /add | `{"user":}` | N/A | `` | +| Create IOU | POST | /iou | `{"lender":,"borrower":,"amount":5.25}` | N/A | `{"users": and (sorted by name)>}` | + +### Other Resources: +- https://restfulapi.net/ +- Example RESTful APIs + - [GitHub](https://developer.github.com/v3/) + - [Reddit](https://www.reddit.com/dev/api/) + +## Exception messages + +Sometimes it is necessary to raise an exception. When you do this, you should include a meaningful error message to +indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. Not +every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include +a message. + +To raise a message with an exception, just write it as an argument to the exception type. For example, instead of +`raise Exception`, you should write: + +```python +raise Exception("Meaningful message indicating the source of the error") +``` + +## Running the tests + +To run the tests, run `pytest rest_api_test.py` + +Alternatively, you can tell Python to run the pytest module: +`python -m pytest rest_api_test.py` + +### Common `pytest` options + +- `-v` : enable verbose output +- `-x` : stop running tests on first failure +- `--ff` : run failures from previous test before running other test cases + +For other options, see `python -m pytest -h` + +## Submitting Exercises + +Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/rest-api` directory. + +You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`. + +For more detailed information about running tests, code style and linting, +please see [Running the Tests](http://exercism.io/tracks/python/tests). + +## Submitting Incomplete Solutions + +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/python/rest-api/rest_api.py b/python/rest-api/rest_api.py new file mode 100644 index 0000000..5cdc776 --- /dev/null +++ b/python/rest-api/rest_api.py @@ -0,0 +1,51 @@ +import json +from enum import Enum +from functools import wraps + +VALID_HTTP_METHODS = ['GET', 'POST'] + +def register_url(url, http_method): + def actual_decorator(f): + @wraps(f) + def _impl(self, *f_args, **f_kwargs): + RestAPI._register_url(self, f, url, http_method) + return f(self, *f_args, **f_kwargs) + return actual_decorator + +class RestAPI: + def __init__(self, database=None): + self._rest_methods = {} + self._database = database if database else {} + + def get(self, url, payload=None): + methods = self._rest_methods['GET'] + if url in methods.keys(): + return methods[url](payload) + else: + raise Exception('Error 404') + + def post(self, url, payload=None): + methods = self._rest_methods['POST'] + if url in methods.keys(): + return methods[url](payload) + else: + raise Exception('Error 404') + + def _register_url(self, f, url, http_method): + + if http_method not in VALID_HTTP_METHODS: + raise Exception('Invalid Http method') + + self._rest_methods[http_method][url] = f + + @register_url('/users', 'GET') + def users(self, payload=None): + pass + + @register_url('/add', 'POST') + def add(self, payload=None): + pass + + @register_url('/iou', 'POST') + def iou(self, payload=None): + pass diff --git a/python/rest-api/rest_api_test.py b/python/rest-api/rest_api_test.py new file mode 100644 index 0000000..5f53049 --- /dev/null +++ b/python/rest-api/rest_api_test.py @@ -0,0 +1,163 @@ +import unittest + +from rest_api import RestAPI + +# Tests adapted from `problem-specifications//canonical-data.json` +import json + + +class RestApiTest(unittest.TestCase): + def test_no_users(self): + database = {"users": []} + api = RestAPI(database) + + response = api.get("/users") + expected = {"users": []} + self.assertDictEqual(json.loads(response), expected) + + def test_add_user(self): + database = {"users": []} + api = RestAPI(database) + payload = json.dumps({"user": "Adam"}) + response = api.post("/add", payload) + expected = {"name": "Adam", "owes": {}, "owed_by": {}, "balance": 0.0} + self.assertDictEqual(json.loads(response), expected) + + def test_get_single_user(self): + database = { + "users": [ + {"name": "Adam", "owes": {}, "owed_by": {}, "balance": 0.0}, + {"name": "Bob", "owes": {}, "owed_by": {}, "balance": 0.0}, + ] + } + api = RestAPI(database) + payload = json.dumps({"users": ["Bob"]}) + response = api.get("/users", payload) + expected = { + "users": [{"name": "Bob", "owes": {}, "owed_by": {}, "balance": 0.0}] + } + self.assertDictEqual(json.loads(response), expected) + + def test_both_users_have_0_balance(self): + database = { + "users": [ + {"name": "Adam", "owes": {}, "owed_by": {}, "balance": 0.0}, + {"name": "Bob", "owes": {}, "owed_by": {}, "balance": 0.0}, + ] + } + api = RestAPI(database) + payload = json.dumps({"lender": "Adam", "borrower": "Bob", "amount": 3.0}) + response = api.post("/iou", payload) + expected = { + "users": [ + {"name": "Adam", "owes": {}, "owed_by": {"Bob": 3.0}, "balance": 3.0}, + {"name": "Bob", "owes": {"Adam": 3.0}, "owed_by": {}, "balance": -3.0}, + ] + } + self.assertDictEqual(json.loads(response), expected) + + def test_borrower_has_negative_balance(self): + database = { + "users": [ + {"name": "Adam", "owes": {}, "owed_by": {}, "balance": 0.0}, + {"name": "Bob", "owes": {"Chuck": 3.0}, "owed_by": {}, "balance": -3.0}, + {"name": "Chuck", "owes": {}, "owed_by": {"Bob": 3.0}, "balance": 3.0}, + ] + } + api = RestAPI(database) + payload = json.dumps({"lender": "Adam", "borrower": "Bob", "amount": 3.0}) + response = api.post("/iou", payload) + expected = { + "users": [ + {"name": "Adam", "owes": {}, "owed_by": {"Bob": 3.0}, "balance": 3.0}, + { + "name": "Bob", + "owes": {"Adam": 3.0, "Chuck": 3.0}, + "owed_by": {}, + "balance": -6.0, + }, + ] + } + self.assertDictEqual(json.loads(response), expected) + + def test_lender_has_negative_balance(self): + database = { + "users": [ + {"name": "Adam", "owes": {}, "owed_by": {}, "balance": 0.0}, + {"name": "Bob", "owes": {"Chuck": 3.0}, "owed_by": {}, "balance": -3.0}, + {"name": "Chuck", "owes": {}, "owed_by": {"Bob": 3.0}, "balance": 3.0}, + ] + } + api = RestAPI(database) + payload = json.dumps({"lender": "Bob", "borrower": "Adam", "amount": 3.0}) + response = api.post("/iou", payload) + expected = { + "users": [ + {"name": "Adam", "owes": {"Bob": 3.0}, "owed_by": {}, "balance": -3.0}, + { + "name": "Bob", + "owes": {"Chuck": 3.0}, + "owed_by": {"Adam": 3.0}, + "balance": 0.0, + }, + ] + } + self.assertDictEqual(json.loads(response), expected) + + def test_lender_owes_borrower(self): + database = { + "users": [ + {"name": "Adam", "owes": {"Bob": 3.0}, "owed_by": {}, "balance": -3.0}, + {"name": "Bob", "owes": {}, "owed_by": {"Adam": 3.0}, "balance": 3.0}, + ] + } + api = RestAPI(database) + payload = json.dumps({"lender": "Adam", "borrower": "Bob", "amount": 2.0}) + response = api.post("/iou", payload) + expected = { + "users": [ + {"name": "Adam", "owes": {"Bob": 1.0}, "owed_by": {}, "balance": -1.0}, + {"name": "Bob", "owes": {}, "owed_by": {"Adam": 1.0}, "balance": 1.0}, + ] + } + self.assertDictEqual(json.loads(response), expected) + + def test_lender_owes_borrower_less_than_new_loan(self): + database = { + "users": [ + {"name": "Adam", "owes": {"Bob": 3.0}, "owed_by": {}, "balance": -3.0}, + {"name": "Bob", "owes": {}, "owed_by": {"Adam": 3.0}, "balance": 3.0}, + ] + } + api = RestAPI(database) + payload = json.dumps({"lender": "Adam", "borrower": "Bob", "amount": 4.0}) + response = api.post("/iou", payload) + expected = { + "users": [ + {"name": "Adam", "owes": {}, "owed_by": {"Bob": 1.0}, "balance": 1.0}, + {"name": "Bob", "owes": {"Adam": 1.0}, "owed_by": {}, "balance": -1.0}, + ] + } + self.assertDictEqual(json.loads(response), expected) + + def test_lender_owes_borrower_same_as_new_loan(self): + database = { + "users": [ + {"name": "Adam", "owes": {"Bob": 3.0}, "owed_by": {}, "balance": -3.0}, + {"name": "Bob", "owes": {}, "owed_by": {"Adam": 3.0}, "balance": 3.0}, + ] + } + api = RestAPI(database) + payload = json.dumps({"lender": "Adam", "borrower": "Bob", "amount": 3.0}) + response = api.post("/iou", payload) + expected = { + "users": [ + {"name": "Adam", "owes": {}, "owed_by": {}, "balance": 0.0}, + {"name": "Bob", "owes": {}, "owed_by": {}, "balance": 0.0}, + ] + } + self.assertDictEqual(json.loads(response), expected) + + +if __name__ == "__main__": + unittest.main()