diff --git a/README.md b/README.md index 73d26f9..a12c518 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/short_url/.gitignore b/short_url/.gitignore new file mode 100644 index 0000000..1163cb2 --- /dev/null +++ b/short_url/.gitignore @@ -0,0 +1,2 @@ +.idea +*.pyc \ No newline at end of file diff --git a/short_url/api/__init__.py b/short_url/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/short_url/api/api.py b/short_url/api/api.py new file mode 100644 index 0000000..b8f1dc9 --- /dev/null +++ b/short_url/api/api.py @@ -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 diff --git a/short_url/app/__init__.py b/short_url/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/short_url/app/service.py b/short_url/app/service.py new file mode 100644 index 0000000..f609e83 --- /dev/null +++ b/short_url/app/service.py @@ -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('/', 'get_url', view_func=api.api.get_url) + app.add_url_rule('//stats', 'stats', view_func=api.api.get_stats) + app.run(debug=True) + + # server will run indefinitely + while True: + pass + + +if __name__ == "__main__": + main() diff --git a/short_url/app/service.pyc b/short_url/app/service.pyc new file mode 100644 index 0000000..cbf4941 Binary files /dev/null and b/short_url/app/service.pyc differ diff --git a/short_url/test/__init__.py b/short_url/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/short_url/test/test_shorturl.py b/short_url/test/test_shorturl.py new file mode 100644 index 0000000..8bd8d0d --- /dev/null +++ b/short_url/test/test_shorturl.py @@ -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()