summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormar77i <mar77i@protonmail.ch>2020-01-12 20:33:47 +0100
committermar77i <mar77i@protonmail.ch>2020-01-12 20:33:47 +0100
commit59874762163701ef494f3f741bad9eacdde25d67 (patch)
treec5aba6eecc2f7945fb247038c5bbece7f4dc522c
initial commit
-rw-r--r--.coveragerc6
-rw-r--r--.gitignore5
-rwxr-xr-xmanage.sh140
-rw-r--r--requirements.txt0
-rwxr-xr-xshorturl/api.py25
-rw-r--r--tests.py25
6 files changed, 201 insertions, 0 deletions
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..8183af2
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,6 @@
+[run]
+omit =
+ venv*
+[report]
+exclude_lines =
+ pragma: no cover
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..456817a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.coverage
+htmlcov
+__pycache__/
+urls.sqlite
+venv/
diff --git a/manage.sh b/manage.sh
new file mode 100755
index 0000000..5eee356
--- /dev/null
+++ b/manage.sh
@@ -0,0 +1,140 @@
+#!/bin/bash
+
+usage() {
+ echo "${1} --help"
+ echo " Display this help message"
+ echo "${1} env"
+ echo " Create and/or activate virtual environment"
+ echo "${1} shell"
+ echo " Run python shell in the environment"
+ echo " This will activate the virtual environment if it's not active"
+ echo "${1} test"
+ echo " Run unit tests for project"
+ echo "${1} serve"
+ echo " Serve api in debug mode on localhost:8000"
+}
+
+setup_venv() {
+ local venvs=() venv=
+ while read -r -d ''; do
+ if [[ -f "${REPLY}/bin/activate" ]]; then
+ venvs+=("${REPLY}")
+ fi
+ done < <(find . -mindepth 1 -maxdepth 1 -type d -print0)
+ if (( ${#venvs[@]} == 0 )); then
+ venv=venv
+ python -m venv "${venv}"
+ else
+ venv="${venvs[0]}"
+ fi
+ if [[ -f env.sh ]]; then
+ if ! grep -qER 'env\.sh' venv/bin/activate; then
+ d='${VIRTUAL_ENV}'
+ d="[[ -n \"${d}\" ]] && . \"\${d}/../env.sh\" --deactivate"
+ sed -i -r $'/unset VIRTUAL_ENV/i\\ '"${d}" venv/bin/activate
+ fi
+ . venv/bin/activate
+ pip install -qU pip -r requirements.txt
+ . env.sh
+ else
+ . venv/bin/activate
+ pip install -qU pip -r requirements.txt
+ fi
+}
+
+quiet_coverage() {
+ local prev_lp=0 lp
+ quiet=0
+ while (( $# > 0 )); do
+ case "${1}" in
+ --quiet|-q)
+ quiet=1
+ ;;
+ esac
+ shift
+ done
+ if (( quiet == 0 )); then
+ cat
+ return
+ fi
+ while read -d $'\n' -r; do
+ grep -qE '^\-+$' <(printf '%s' "${REPLY}")
+ lp="${?}"
+ if [[ "${REPLY}" == *" 100%" && "${REPLY}" != TOTAL* ]] ||
+ (( "${prev_lp}" == 0 && "${lp}" == 0 )); then
+ continue
+ fi
+ echo "${REPLY}"
+ prev_lp="${lp}"
+ done
+}
+
+run_tests() {
+ local browser= quiet_args=
+ while (( ${#} > 0 )); do
+ case "${1}" in
+ --browser)
+ if [[ -n "${browser}" ]]; then
+ echo "Error: browser already set." >&2
+ exit 1
+ fi
+ browser="${2}"
+ shift
+ ;;
+ --quiet|-q)
+ quiet_args+=("${1}")
+ ;;
+ *)
+ echo "Error: invalid argument: ${1}" >&2
+ exit 1
+ ;;
+ esac
+ shift
+ done
+ coverage run -m unittest || exit 1
+ coverage report | quiet_coverage "${quiet_args[@]}"
+ coverage html
+ if [[ -n "${browser}" ]]; then
+ "${browser}" htmlcov/index.html 2>/dev/null &
+ fi
+}
+
+ret=0
+case "${1}" in
+ env)
+ if [[ "${0}" -ef "${BASH_SOURCE[0]}" ]]; then
+ echo "Error: For this action, the script must be sourced!" >&2
+ exit 1
+ fi
+ setup_venv
+ ;;
+ shell)
+ if [[ -z "${VIRTUAL_ENV}" ]]; then
+ setup_venv
+ fi
+ python "${@:2}"
+ ;;
+ test)
+ if [[ -z "${VIRTUAL_ENV}" ]]; then
+ setup_venv
+ fi
+ run_tests "${@:2}"
+ ;;
+ serve)
+ gunicorn --reload shorturl.api:api
+ ;;
+ -h|--help)
+ usage "${BASH_SOURCE[0]}"
+ ;;
+ *)
+ usage "${BASH_SOURCE[0]}"
+ echo "Error: action missing." >&2
+ ret=1
+ ;;
+esac
+
+if [[ "${0}" -ef "${BASH_SOURCE[0]}" ]]; then
+ exit "${ret}"
+else
+ return "${ret}"
+fi
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/requirements.txt
diff --git a/shorturl/api.py b/shorturl/api.py
new file mode 100755
index 0000000..4d4875e
--- /dev/null
+++ b/shorturl/api.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+
+from falcon import API, HTTPMovedPermanently, HTTPNotFound
+import os
+import sqlite3
+import sys
+
+
+class URLResolver:
+ conn = sqlite3.connect(os.path.join(os.getcwd(), "urls.sqlite"))
+
+ def __call__(self, req, resp):
+ c = self.conn.cursor()
+ c.execute("SELECT * FROM urls WHERE hash=?", (req.path.strip("/"),))
+ entry = c.fetchone()
+ if entry is not None:
+ raise HTTPMovedPermanently(entry[2])
+ raise HTTPNotFound(
+ title="Resource not found",
+ description="Resource not found: {}".format(req.path)
+ )
+
+
+api = API()
+api.add_sink(URLResolver(), '/')
diff --git a/tests.py b/tests.py
new file mode 100644
index 0000000..05bc151
--- /dev/null
+++ b/tests.py
@@ -0,0 +1,25 @@
+from falcon.testing import TestCase
+import json
+from unittest.mock import patch
+from shorturl.api import api
+
+
+class ShortUrlTest(TestCase):
+ def setUp(self):
+ super().setUp()
+ self.app = api
+
+ def test_fail(self):
+ print("test_fail")
+ result = json.loads(self.simulate_get('/test').content.decode())
+ self.assertEqual(result["title"], "Resource not found")
+
+ @patch('shorturl.api.URLResolver.conn')
+ def test_success(self, mock_conn):
+ expected_url = "https://example.com/destination"
+ mock_conn.cursor().fetchone.side_effect = lambda: (
+ None, None, expected_url,
+ )
+ response = self.simulate_get('/test')
+ self.assertEqual(response.status_code, 301)
+ self.assertEqual(response.headers['Location'], expected_url)