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
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
__pycache__/
.cache/
*.pyc
*.db
.ash_*
.#*
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
__pycache__/
.cache/
env/
*.pyc
*.db
.ash_*
.#*
28 changes: 28 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
FROM python:3.7-alpine

ARG UID=1000
ARG GID=1000

ENV PYTHONUNBUFFERED 1
ENV LANG C.UTF-8
ENV USER shorturl
ENV HOME /app

COPY ./requirements.txt /app/requirements.txt

RUN apk add --no-cache \
--virtual build-deps \
musl-dev postgresql-dev \
build-base python3-dev zlib-dev git \
&& apk add --no-cache \
postgresql-libs libpq \
&& pip install -U pip gunicorn \
&& pip install -r /app/requirements.txt \
&& rm -fr /app/.cache \
&& apk --purge del build-deps \
&& addgroup -S $USER -g $GID \
&& adduser -S -G $USER -u $UID -h $HOME $USER

COPY . /app

WORKDIR /app
45 changes: 45 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# shorturl how to

## Install

### docker

```
$ docker-compose build
```

### virtualenv

```
$ virtualenv -p python3 --system-site-packages env
$ . ./env/bin/activate
(env) $ pip install -r requirements.txt
```

### Run

### docker

```
$ docker-compose up
```

### virtualenv

```
(env) $ ./entrypoint.sh
```

### Test

### docker

```
$ docker-compose -f docker-compose.test.yml up
```

### virtualenv

```
(env) $ python -m unittest discover
```
17 changes: 17 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: '3.1'
services:
web:
build:
context: .
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
image: buoydev-py
container_name: buoydev-test
command: python -m unittest discover
environment:
FLASK_APP: shorturl.py
FLASK_ENV: development
FLASK_DEBUG: 1
volumes:
- .:/app/
36 changes: 36 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
version: '3.1'
services:
db:
image: postgres:9.6-alpine
restart: unless-stopped
container_name: buoydev-db
env_file:
- ./docker.env
ports:
- "5432:5432"
networks:
- buoy_apps
web:
build:
context: .
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
image: buoydev-py
container_name: buoydev-web
restart: unless-stopped
command: ./entrypoint.sh
env_file:
- ./docker.env
volumes:
- .:/app/
ports:
- "5000:5000"
depends_on:
- db
networks:
- buoy_apps

networks:
buoy_apps:
external: true
13 changes: 13 additions & 0 deletions docker.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
POSTGRES_USER=buoy
POSTGRES_PASSWORD=buoy
POSTGRES_DB=buoy

DB_HOST=db

HOST=0.0.0.0
PORT=5000
WORKERS=3

FLASK_APP=shorturl.py
FLASK_ENV=development
FLASK_DEBUG=0
6 changes: 6 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh

flask create-tables && \
gunicorn --bind $HOST:$PORT \
--workers $WORKERS \
shorturl:app
71 changes: 71 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import datetime
from peewee import *
from os import environ as env

db_proxy = Proxy()

class BaseModel(Model):
class Meta:
database = db_proxy

class URL(BaseModel):
url = CharField(max_length=35)
code = CharField(max_length=6, unique=True)

class Meta:
table_name = 'urls'

@property
def serialize(self):
data = {
'id': self.id,
'url': str(self.url).strip(),
'code': str(self.code).strip(),
}

return data

def __repr__(self):
code = self.code
url = self.url
return f"{code} ({url})"


class Stats(BaseModel):
url = ForeignKeyField(URL, backref='url')
created_at = DateTimeField(default=datetime.datetime.now)
last_usage = DateTimeField(null=True)
usage_count = IntegerField()

class Meta:
table_name = 'stats'

@property
def serialize(self):
data = {
'id': self.id,
'url': str(self.url).strip(),
'created_at': str(self.created_at).strip(),
'last_usage': str(self.last_usage).strip(),
'usage_count': str(self.usage_count).strip(),
}

return data

def __repr__(self):
url = self.url
count = self.usage_count
return f"{url} ({count})"



db = SqliteDatabase(':memory:')
if int(env.get('FLASK_DEBUG')) == 0:
db = PostgresqlDatabase(
env.get('POSTGRES_DB', 'postgres'),
user=env.get('POSTGRES_USER', 'postgres'),
password=env.get('POSTGRES_PASSWORD', 'postgres'),
host=env.get('DB_HOST', 'localhost'),
)

db_proxy.initialize(db)
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.0
psycopg2==2.7.3.2
peewee
111 changes: 111 additions & 0 deletions shorturl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import utils
from flask import (
Flask,
jsonify,
request,
redirect,
)


app = Flask(__name__)


##
# Flask commands
# drop-tables
# create-tables
#

@app.cli.command()
def drop_tables():
"""Use PeeWee to drop tables."""
utils.drop_tables()


@app.cli.command()
def create_tables():
"""Use PeeWee to create tables."""
utils.create_tables()


##
# Flask endpoints
# index
# url_get
# url_stats
# url_post
#

@app.route('/', methods=['GET'])
def index():
"""Home"""
endpoints = {
"/urls": "POST -> url:str:required, code:str:optional",
"/<code>": "GET -> code:str:required",
"/<code>/stats": "GET -> code:str:required"
}

return jsonify(endpoints=endpoints)


@app.route('/urls', methods=['POST'])
def url_post():
"""Saves URLs if ok, else wont (?)"""
data = request.json.copy()
if "url" not in list(data.keys()):
return jsonify(error="URL not provided"), 400

else:
if "code" not in list(data.keys()):
code = utils.gen_code()
else:
code = data.get('code')
if not utils.is_valid_code(code):
return jsonify(error="Code not valid"), 409

_, exists = utils.code_exists(code)
if exists:
return jsonify(error="Code in use"), 422
else:
if utils.is_valid_url(data.get('url')):
utils.insert_url(data.get('url'), code)
else:
return jsonify(error="URL not valid"), 409

return jsonify(code=code), 201


@app.route('/<code>/stats', methods=['GET'])
def url_stats(code: str):
"""Shows stats for a given code"""
_, exists = utils.code_exists(code)
if not exists:
return jsonify(error="Code Not Found"), 404
else:
created_at, last_usage, usage_count = utils.get_stats(
code
)

result = {
'created_at': utils.to_iso8601(created_at),
'usage_count': usage_count
}
if last_usage:
result['last_usage'] = utils.to_iso8601(last_usage)

return jsonify(result), 200



@app.route('/<code>', methods=['GET'])
def url_get(code: str):
"""Redirects to a URL given a code else 404"""
q, exists = utils.code_exists(code)
if exists:
url_id, url = q.execute().cursor.fetchone()
utils.bump_stats(url_id)
return redirect(url, code=302)

else:
return jsonify(error="Code Not Found"), 404

Loading