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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Folders

venv
.vscode
.history
__pycache__

# Files

test.db
29 changes: 29 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Code Challenge for BuoyDevelopment by Mauricio Bergallo

## Description

The API in this repo is used to shorten urls. Is an API created in Python 3 with Flask Framework; the Application uses a Database SQLite.

## PreRequisites

- Virtual Environment should be installed and active
- To Activate in Windows ``` λ venv\Scripts\activate.bat ```; in Unix / MacOS ```$ source ./env/bin/activate```
- Install Dependencies ```$ pip install -r .\requirements.txt```

## Steps to Run Locally

- Set the Environment Variable: ``` λ set APP_SETTINGS=src.config.LocalConfig ``` (Windows) ``` $ export APP_SETTINGS=src.config.LocalConfig ```
- Start the Server: ``` py manage.py run ```
- Test:
```
curl -X POST \
http://localhost:5000/urls \
-H 'Content-Type: application/json' \
-d '{
"url": "http://example.com",
"code": "Ncr8p7"
}'
```

## Steps to Run Unit Test
- Execute the Test: ``` py manage.py test ```
12 changes: 12 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from flask import Flask

app = Flask(__name__)


@app.route('/')
def hello_world():
return 'Hello World'

app.run(port=5050)

# __name__ = "__main__"
30 changes: 30 additions & 0 deletions manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# manage.py

import unittest
from flask.cli import FlaskGroup
from src import create_app, db
# from project.api.models import Product, Price
app = create_app()
cli = FlaskGroup(create_app=create_app)

@cli.command()
def recreate_db():
""" Recreates the DataBase """
db.drop_all()
db.create_all()
db.session.commit()

@cli.command()
def test():
""" Runs the tests without code coverage """
tests = unittest.TestLoader().discover('src/test/', pattern='*_test.py')
result = unittest.TextTestRunner(verbosity=2).run(tests)

if result.wasSuccessful():
return 0

return 1

if __name__ == '__main__':
db.create_all()
cli()
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Flask==1.1.1
Flask-Testing==0.7.1
Flask-SQLAlchemy==2.4.0
40 changes: 40 additions & 0 deletions src/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# src/__init__.py

import os
import datetime
from flask import Flask, jsonify
from flask_sqlalchemy import SQLAlchemy

#from project.api.models import Product

#instantiate the app
app = Flask(__name__)

#set config
app_settings = os.getenv('APP_SETTINGS')
app.config.from_object(app_settings)

#instantiate the db
db = SQLAlchemy(app)

def create_app(script_info=None):
#instiantiate the app
app = Flask(__name__)

#set config
app_settings = os.getenv('APP_SETTINGS')
app.config.from_object(app_settings)

#set up extensions
db.init_app(app)

#register blueprints
from src.api.urls import urls_blueprint
app.register_blueprint(urls_blueprint)
# from project.api.products import products_blueprint
# app.register_blueprint(products_blueprint)

# shell context for flask cli
app.shell_context_processor({'app': app, 'db': db})

return app
60 changes: 60 additions & 0 deletions src/api/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#! src/api/models.py

from src import db
import datetime

class Url(db.Model):
__tablename__ = "url"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
url = db.Column(db.String(256), nullable=False)
code = db.Column(db.String(6), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow(), nullable=False)
last_usage = db.Column(db.DateTime, nullable=True)
usage_count = db.Column(db.Integer, default=0, nullable=False)

def to_json(self):
return {
'url': self.url,
'code': self.code
}

def stats_to_json(self):
result = {
'created_at': self.created_at.isoformat(),
'usage_count': self.usage_count
}

if self.last_usage is not None:
result['last_usage'] = self.last_usage.isoformat()

return result

def save(self):
db.session.add(self)
db.session.commit()


def get_url_by_code(value):
if already_existant_code(value) == False:
return False

url = Url.query.filter_by(code=value).first()
url.last_usage = datetime.datetime.utcnow()
url.usage_count = url.usage_count + 1
url.save()

return url


def get_url_stats_by_code(value):
if already_existant_code(value) == False:
return False

url = Url.query.filter_by(code=value).first()
return url


def already_existant_code(value):
url = Url.query.filter_by(code=value).first()

return (url is not None)
80 changes: 80 additions & 0 deletions src/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# src/api/products.py

import random
import string
from flask import Blueprint, jsonify, request
from src.api.models import Url, already_existant_code, get_url_by_code, get_url_stats_by_code
from src import db
from sqlalchemy import exc

urls_blueprint = Blueprint('urls', __name__)

@urls_blueprint.route('/urls', methods=['POST'])
def add_url():
"""
Endpoint to Add a New URL
"""
post_data = request.get_json()

if not post_data:
return jsonify(response_object), 400

url = post_data.get('url')
code = post_data.get('code')

if url is None or len(url) == 0:
return {}, 400

if code is None or len(code) == 0:
code = generate_random_code()
else:
status = validate_code(code)
if status != 201:
return {}, status

newUrl = Url()
newUrl.url = url
newUrl.code = code

newUrl.save()

return jsonify({ 'code': newUrl.code }), 201

@urls_blueprint.route('/<code>', methods=['GET'])
def get_by_code(code):
"""
Endpoint to Retrieve an existant URL
"""
url = get_url_by_code(code)

if url == False:
return {}, 404

return {}, 302, { 'location': url.url }

@urls_blueprint.route('/<code>/stats', methods=['GET'])
def get_stats_by_code(code):
"""
Endpoint to retrieve the Statistics of this particular CODE
"""
url = get_url_stats_by_code(code)

if url == False:
return {}, 404

return url.stats_to_json(), 200

def validate_code(code):
if len(code) != 6:
return 422
elif any(char in string.punctuation for char in code):
return 422
else:
if already_existant_code(code) == True:
return 409
else:
return 201

def generate_random_code():
letters = string.ascii_lowercase + string.ascii_uppercase + string.digits
return ''.join(random.choice(letters) for i in range(6))
21 changes: 21 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# src/config.py

import os

class BaseConfig:
"""Base configuration"""
TESTING = False
SQLALCHEMY_TRACK_MODIFICATIONS = False
SECRET_KEY = 'my_precious'
IS_MEMORY_DB = False

class LocalConfig(BaseConfig):
"""Development configuration"""
SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db'
# IS_MEMORY_DB = True
IS_MEMORY_DB = False

class TestingConfig(BaseConfig):
"""Testing configuration"""
TESTING = True
IS_MEMORY_DB = True
1 change: 1 addition & 0 deletions src/test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# src/test/__init__.py
19 changes: 19 additions & 0 deletions src/test/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# src/test/base.py

from flask_testing import TestCase
from src import create_app, db

app = create_app()

class BaseTestCase(TestCase):
def create_app(self):
app.config.from_object('project.config.TestingConfig')
return app

def setUp(self):
db.create_all()
db.session.commit()

def tearDown(self):
db.session.remove()
db.drop_all()
34 changes: 34 additions & 0 deletions src/test/config_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# src/test/config_test.py

import os
import unittest

from flask import current_app
from flask_testing import TestCase
from src import create_app

app = create_app()

class TestLocalConfig(TestCase):
def create_app(self):
app.config.from_object('src.config.LocalConfig')
return app

def test_app_is_local(self):
self.assertTrue(app.config['SECRET_KEY'] == 'my_precious')
self.assertFalse(current_app is None)
self.assertTrue(app.config['SQLALCHEMY_DATABASE_URI'] == 'sqlite:///test.db')

class TestTestingConfig(TestCase):
def create_app(self):
app.config.from_object('src.config.TestingConfig')
return app

def test_app_is_testing(self):
self.assertTrue(app.config['SECRET_KEY'] == 'my_precious')
self.assertTrue(app.config['TESTING'])
self.assertFalse(app.config['PRESERVE_CONTEXT_ON_EXCEPTION'])
self.assertTrue(app.config['IS_MEMORY_DB'])

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