[WIP] Python: rest_api

This commit is contained in:
Dmitry Kokorin 2021-06-28 19:11:51 +03:00
parent 77b3a6a689
commit 8190c11851
4 changed files with 297 additions and 0 deletions

View file

@ -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}

82
python/rest-api/README.md Normal file
View file

@ -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":<List of all User objects>}` | `{"users":<List of User objects for <users> (sorted by name)}` |
| Create user | POST | /add | `{"user":<name of new user (unique)>}` | N/A | `<User object for new user>` |
| Create IOU | POST | /iou | `{"lender":<name of lender>,"borrower":<name of borrower>,"amount":5.25}` | N/A | `{"users":<updated User objects for <lender> and <borrower> (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.

View file

@ -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

View file

@ -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()