Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,35 @@ Content-Type: "application/json"

* Not Found: If the `shortcode` cannot be found

## Delivery Steps:
## Delivery Steps:

1. Fork this repo to your own Github account.
2. Implement the functionality, including any instructions to setup and run the application.
3. Submit a PR to the `master` branch of this repository.

Thank you and good luck!

-------------------------------------------------------------------------

## About FL solution

### Solution structure:

* api: Contains all functions related to urls management
* app: Contains functions about setting up of service
* test: Contains unit tests for training all api functions


### How to ...

#### Run Short_URL service

PyCharm is used for this purpouse

1. Opens project in PyCharm
2. Performs right click over /app/service.py and select Run

#### Run tests

1. Opens project in PyCharm
2. Performs right click over /test/test_shorturl.py and select Run
2 changes: 2 additions & 0 deletions short_url/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.idea
*.pyc
Empty file added short_url/api/__init__.py
Empty file.
82 changes: 82 additions & 0 deletions short_url/api/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import re
import random
import string
import json
import datetime
from flask import Response, request


# global variables
stored_urls = {}
letters_and_digits = string.ascii_letters + string.digits


def is_valid(short_code):
""" Checks if short_code complies with requirements: alphanumeric and length = 6."""
return re.match(r'[A-Za-z0-9]', short_code) and len(short_code) == 6


def exist(short_code):
""" Checks if short_code is already in use."""
return short_code in stored_urls.keys()


def generate_random_code(string_length=6):
""" Generates a random alphanumeric string. """
global letters_and_digits
return ''.join(random.choice(letters_and_digits) for i in range(string_length))


def short_url():
""" Relates a short alphanumeric code which will represents a long url."""
content = request.get_json()
print json.dumps(content)
# checks if code is missing. If yes, a new code is generated.
if 'code' in content.keys():
# validate short code
if is_valid(content['code']):
if not exist(content['code']):
short_code = content['code']
else:
return "Short code is already in use", 409
else:
return "Precondition failed", 412
else:
short_code = generate_random_code()
print short_code

# creates new element
new_url_item = {
'url': content['url'],
'created_at': datetime.datetime.utcnow().isoformat(),
'last_usage': datetime.datetime.utcnow().isoformat(),
'usage_count': 0}

# stores new element
stored_urls[short_code] = new_url_item

response = {'code': short_code}
return Response(json.dumps(response), status=201, mimetype='application/json')


def get_url(short_name):
""" Returns the long url associated to short code name."""
if short_name in stored_urls.keys():
# update usage count
current_count = stored_urls[short_name]['usage_count']
stored_urls[short_name]['usage_count'] = current_count+1

# update last usage time
stored_urls[short_name]['last_usage'] = datetime.datetime.utcnow().isoformat()
return "Location: {0}".format(stored_urls[short_name]['url']), 302
else:
return "Bad Request.", 400


def get_stats(short_name):
""" Returns additional information related to short code name."""
if short_name in stored_urls.keys():
response = stored_urls[short_name]
return Response(json.dumps(response), status=200, mimetype='application/json')
else:
return "Bad Request.", 400
Empty file added short_url/app/__init__.py
Empty file.
36 changes: 36 additions & 0 deletions short_url/app/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import json
import api.api
from flask import Flask, request

app = Flask(__name__)


# Error handler when path is not available.
@app.errorhandler(404)
def not_found():
message = {
'status': 404,
'message': 'Not Found: ' + request.url,
}
resp = json.dumps(message)
resp.status_code = 404
return resp

# By default, all methods accepts GET requests. We need to specify that short_url() will receive POST requests.
api.api.short_url.methods = ['POST']


def main():
""" Registering main server functions. """
app.add_url_rule('/urls', 'urls', view_func=api.api.short_url)
app.add_url_rule('/<short_name>', 'get_url', view_func=api.api.get_url)
app.add_url_rule('/<short_name>/stats', 'stats', view_func=api.api.get_stats)
app.run(debug=True)

# server will run indefinitely
while True:
pass


if __name__ == "__main__":
main()
Binary file added short_url/app/service.pyc
Binary file not shown.
Empty file added short_url/test/__init__.py
Empty file.
94 changes: 94 additions & 0 deletions short_url/test/test_shorturl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import time
import datetime
import json
import unittest
import psutil as psutil
import requests
from subprocess import Popen, PIPE
from api.api import is_valid


class ShortUrlTest(unittest.TestCase):

server_pid = 0

@classmethod
def setUpClass(cls):
server = Popen(['python', '..\\app\\service.py'], stdout=PIPE, stderr=PIPE)
cls.server_pid = server.pid
time.sleep(3)

@classmethod
def tearDownClass(cls):
time.sleep(3)
parent = psutil.Process(cls.server_pid)
for child in parent.children(recursive=True):
child.kill()
parent.kill()

# Scenarios
def test_valid_short_code(self):
response = requests.post("http://localhost:5000/urls", json={'url': 'http://example.com', 'code': 'test01'})
self.assertEqual(response.status_code, 201)
self.assertDictEqual({'code': 'test01'}, json.loads(response.text))

def test_not_valid_short_code(self):
response = requests.post("http://localhost:5000/urls", json={'url': 'http://example1.com', 'code': 'long_code'})
self.assertEqual(response.status_code, 412)
self.assertFalse(is_valid("long_code"))

def test_existent_short_code(self):
response = requests.post("http://localhost:5000/urls", json={'url': 'http://example2.com', 'code': 'test02'})
self.assertEqual(response.status_code, 201)
self.assertDictEqual({'code': 'test02'}, json.loads(response.text))

response = requests.post("http://localhost:5000/urls", json={'url': 'http://example2.com', 'code': 'test02'})
# short code is already in use.
self.assertEqual(response.status_code, 409)

def test_without_short_code(self):
response = requests.post("http://localhost:5000/urls", json={'url': 'http://example3.com'})
self.assertEqual(response.status_code, 201)
new_code = json.loads(response.text)['code']
self.assertTrue(is_valid(new_code))
self.assertDictEqual({'code': new_code}, json.loads(response.text))

def test_get_url_from_valid_short_code(self):
response = requests.post("http://localhost:5000/urls", json={'url': 'http://example4.com', 'code': 'test04'})
self.assertEqual(response.status_code, 201)
self.assertDictEqual({'code': 'test04'}, json.loads(response.text))
response = requests.get("http://localhost:5000/test04")
self.assertEqual(response.text, "Location: http://example4.com")

def test_get_url_from_not_valid_short_code(self):
response = requests.get("http://localhost:5000/test05")
self.assertEqual(response.status_code, 400)

def test_url_stats(self):
response = requests.post("http://localhost:5000/urls", json={'url': 'http://example6.com', 'code': 'test06'})
created_at = datetime.datetime.utcnow().isoformat()
self.assertEqual(response.status_code, 201)
self.assertDictEqual({'code': 'test06'}, json.loads(response.text))

response = requests.get("http://localhost:5000/test06")
time.sleep(2)
response = requests.get("http://localhost:5000/test06")
time.sleep(2)
response = requests.get("http://localhost:5000/test06")
last_usage = datetime.datetime.utcnow().isoformat()

response = requests.get("http://localhost:5000/test06/stats")
json_resp = json.loads(response.text)

self.assertEqual(response.status_code, 200)
self.assertEqual(json_resp['url'], "http://example6.com")
self.assertEqual(json_resp['usage_count'], 3)
self.assertTrue(json_resp['created_at'] <= created_at)
self.assertTrue(json_resp['last_usage'] <= last_usage)

def test_url_stats_with_wrong_short_code(self):
response = requests.get("http://localhost:5000/invalid_code/stats")
self.assertEqual(response.status_code, 400)

if __name__ == '__main__':
unittest.main()