Rest API exercise

This commit is contained in:
Xevion
2019-07-14 02:48:34 -05:00
parent 0efaf06cd0
commit fed57ab7ec
4 changed files with 570 additions and 0 deletions

View File

@@ -0,0 +1 @@
{"track":"python","exercise":"rest-api","id":"1c88ea7c565c4133a44bcc020617803d","url":"https://exercism.io/my/solutions/1c88ea7c565c4133a44bcc020617803d","handle":"Xevion","is_requester":true,"auto_approve":false}

84
python/rest-api/README.md Normal file
View File

@@ -0,0 +1,84 @@
# 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 the appropriate command below ([why they are different](https://github.com/pytest-dev/pytest/issues/1629#issue-161422224)):
- Python 2.7: `py.test rest_api_test.py`
- Python 3.4+: `pytest rest_api_test.py`
Alternatively, you can tell Python to run the pytest module (allowing the same command to be used regardless of Python version):
`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.

105
python/rest-api/rest_api.py Normal file
View File

@@ -0,0 +1,105 @@
import json
# Comments on this exercise:
# I honestly kind of hate this problem because you're forced to store the users in a array essentially...
# which means that users are not especially unique unless you add this functionality yourself, which is kind of hassle tbh.
# This could be solved if we just had a dictionary of users where the values were users themselves. Bam. Problem solved.
# This database is kind of useless because it's literally a dictionary with a single key, Users, which has a array, with the problem above.
# Super annoying implementation I have to work with, and sure, 100%, you could just rewrite everything to convert databases from and to, but it's a hassle none the less of
# converting and properly displaying databases with their conventions. >:(
# Addtionally, the optional payloads are annoying.
# Thoughts after solving:
# I have no idea if ValueErrors were tested in this exercise, but I implemented them anyways in order to catch them if they ever appeared,
# as after all, a large portion of the parameters that one could issue in this "Rest API" are optional to a point, and thus I assumed errors
# were most definitely possible.
# Not a bad exercise in the end, but it's annoying how this 'database' is implemented...
class RestAPI(object):
def __init__(self, database=None):
self.database = database or {"users" : []}
def get(self, url, payload=None):
if url == '/users':
if not payload:
return json.dumps(self.database)
data = json.loads(payload)
return json.dumps({"users" : [user for user in self.database["users"] if user['name'] in data['users']]})
else:
raise ValueError(f'Invalid URL \'{url}\'')
def post(self, url, payload=None):
if not payload:
raise("No payload for URL \'{}\'".foramt(url))
elif url == '/add':
return self.add(payload) or json.dumps('?')
elif url == '/iou':
return self.iou(payload) or json.dumps('?')
else:
raise ValueError("Invalid URL \'{}\'".foramt(url))
def get_user(self, name):
found = [user for user in self.database['users'] if user['name'] == name]
if len(found) >= 1:
return found[0]
else:
raise ValueError('User \'{}\' does not exist.'.format(name))
return False
def set_user(self, name, data):
found = [(index, user) for index, user in enumerate(self.database['users']) if user['name'] == name]
if len(found) == 1:
print('User {} set'.format(name))
self.database['users'][found[0][0]] = data
print(self.database['users'][found[0][0]])
else:
raise ValueError('User \'{}\' does not exist.'.format(name))
return False
# Add a user to the database
def add(self, payload):
data = json.loads(payload)
if data['user'] in self.database.keys():
raise ValueError('User {} already exists.'.format(data['name']))
self.database['users'].append({'name' : data['user'], 'owes' : {}, 'owed_by' : {}, 'balance' : 0.0})
return json.dumps(self.get_user(data['user']))
def iou(self, payload):
data = json.loads(payload)
lender, borrower = self.get_user(data['lender']), self.get_user(data['borrower'])
# if the lender owes the borrower money
if data['borrower'] in lender['owes'].keys():
dif = lender['owes'][data['borrower']] - data['amount']
# lender still owes borrower money
if dif > 0:
lender['owes'][data['borrower']] = dif
borrower['owed_by'][data['lender']] = dif
# all ious paid between lender and borrower now
elif dif == 0:
del lender['owes'][data['borrower']]
del borrower['owed_by'][data['lender']]
# lender is now owed money by borrower
elif dif < 0:
del lender['owes'][data['borrower']]
del borrower['owed_by'][data['lender']]
lender['owed_by'][data['borrower']] = -dif
borrower['owes'][data['lender']] = -dif
# if the borrower already owes the lender money
elif data['borrower'] in lender['owed_by'].keys():
lender['owed_by'][data['borrower']] += data['amount']
borrower['owes'][data['lender']] -= data['amount']
# if the borrower and lender have no outstanding ious
else:
lender['owed_by'][data['borrower']] = data['amount']
borrower['owes'][data['lender']] = data['amount']
# Balance operations
lender['balance'] += data['amount']
borrower['balance'] -= data['amount']
self.set_user(data['lender'], lender)
self.set_user(data['borrower'], borrower)
return json.dumps({"users" : [user for user in self.database["users"] if user['name'] in [data['lender'], data['borrower']]]})

View File

@@ -0,0 +1,380 @@
import unittest
import json
from rest_api import RestAPI
# Tests adapted from `problem-specifications//canonical-data.json` @ v1.1.1
class RestAPITest(unittest.TestCase):
def test_no_users(self):
database = {"users": []}
api = RestAPI(database)
response = api.get('/users')
self.assertDictEqual(json.loads(response), database)
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
}
self.assertDictEqual(json.loads(response), expected)
def test_get_single_user(self):
database = {
'users': [
{
'name': 'Adam',
'owes': {},
'owed_by': {},
'balance': 0
},
{
'name': 'Bob',
'owes': {},
'owed_by': {},
'balance': 0
}
]
}
api = RestAPI(database)
payload = json.dumps({
'users': ['Bob']
})
response = api.get('/users', payload)
expected = {
'users': [
{
'name': 'Bob',
'owes': {},
'owed_by': {},
'balance': 0
}
]
}
self.assertDictEqual(json.loads(response), expected)
def test_iou_both_users_have_0_balance(self):
database = {
'users': [
{
'name': 'Adam',
'owes': {},
'owed_by': {},
'balance': 0
},
{
'name': 'Bob',
'owes': {},
'owed_by': {},
'balance': 0
}
]
}
api = RestAPI(database)
payload = json.dumps({
'lender': 'Adam',
'borrower': 'Bob',
'amount': 3
})
response = api.post('/iou', payload)
expected = {
'users': [
{
'name': 'Adam',
'owes': {},
'owed_by': {
'Bob': 3
},
'balance': 3
},
{
'name': 'Bob',
'owes': {
'Adam': 3
},
'owed_by': {},
'balance': -3
}
]
}
self.assertDictEqual(json.loads(response), expected)
def test_borrower_has_negative_balance(self):
database = {
'users': [
{
'name': 'Adam',
'owes': {},
'owed_by': {},
'balance': 0
},
{
'name': 'Bob',
'owes': {
'Chuck': 3
},
'owed_by': {},
'balance': -3
},
{
'name': 'Chuck',
'owes': {},
'owed_by': {
'Bob': 3
},
'balance': 3
}
]
}
api = RestAPI(database)
payload = json.dumps({
'lender': 'Adam',
'borrower': 'Bob',
'amount': 3
})
response = api.post('/iou', payload)
expected = {
'users': [
{
'name': 'Adam',
'owes': {},
'owed_by': {
'Bob': 3
},
'balance': 3
},
{
'name': 'Bob',
'owes': {
'Adam': 3,
'Chuck': 3
},
'owed_by': {},
'balance': -6
}
]
}
self.assertDictEqual(json.loads(response), expected)
def test_lender_has_negative_balance(self):
database = {
'users': [
{
'name': 'Adam',
'owes': {},
'owed_by': {},
'balance': 0
},
{
'name': 'Bob',
'owes': {
'Chuck': 3
},
'owed_by': {},
'balance': -3
},
{
'name': 'Chuck',
'owes': {},
'owed_by': {
'Bob': 3
},
'balance': 3
}
]
}
api = RestAPI(database)
payload = json.dumps({
'lender': 'Bob',
'borrower': 'Adam',
'amount': 3
})
response = api.post('/iou', payload)
expected = {
'users': [
{
'name': 'Adam',
'owes': {
'Bob': 3
},
'owed_by': {},
'balance': -3
},
{
'name': 'Bob',
'owes': {
'Chuck': 3
},
'owed_by': {
'Adam': 3
},
'balance': 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
})
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()