diff --git a/.envrc.dist b/.envrc.dist index 0bbf608..97e2ca3 100644 --- a/.envrc.dist +++ b/.envrc.dist @@ -1,12 +1,3 @@ -export FLASK_APP=shortener.main -export FLASK_ENV=development -export BASE_DOMAIN=g4b.ovh -export PROVIDER=ovh -export OVH_ENDPOINT=ovh-eu -export OVH_APPLICATION_KEY= -export OVH_APPLICATION_SECRET= -export OVH_CONSUMER_KEY= -export PORT=5000 -export REDIS_HOST=localhost -export REDIS_PORT=6379 -export REDIS_DB=0 +export DEBUG=false +export SECRET_KEY=changeme +export ALLOWED_HOSTS=localhost,127.0.0.1 diff --git a/Dockerfile b/Dockerfile index 37e5b92..b4a8d74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,21 +35,11 @@ ENV PYTHONPATH $PYTHONPATH:/app WORKDIR /app COPY pyproject.toml ./ -COPY shortener ./shortener/ +COPY contrib/run ./run +COPY src ./src/ COPY --from=git /version /app/.version -ENV FLASK_APP shortener.main -ENV FLASK_ENV production -ENV BASE_DOMAIN g4b.ovh -ENV PROVIDER ovh -ENV OVH_ENDPOINT ovh-eu -ENV OVH_APPLICATION_KEY "" -ENV OVH_APPLICATION_SECRET "" -ENV OVH_CONSUMER_KEY "" -ENV REDIS_HOST redis -ENV REDIS_PORT 6379 -ENV REDIS_DB 0 +HEALTHCHECK --start-period=30s CMD python -c "import requests; requests.get('http://localhost:8000', timeout=2)" -HEALTHCHECK --start-period=30s CMD python -c "import requests; requests.get('http://localhost:5000', timeout=2)" - -CMD ["gunicorn", "--bind", "0.0.0.0:5000", "shortener.wsgi:app"] +WORKDIR /app/src +CMD ["/app/run"] diff --git a/contrib/run b/contrib/run new file mode 100755 index 0000000..8f40238 --- /dev/null +++ b/contrib/run @@ -0,0 +1,6 @@ +#!/bin/bash +set -eu + +./manage.py migrate +./manage.py collectstatic --clear --noinput +gunicorn --bind 0.0.0.0:8000 shortener.wsgi:application diff --git a/docker-compose.yaml b/docker-compose.yaml index a3b10e5..ed812a2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,9 +5,12 @@ services: image: rg.fr-par.scw.cloud/crocmagnon/shortener build: . env_file: shortener.env + environment: + DATABASE_URL: sqlite:////db/db.sqlite3 ports: - - 5000:5000 - depends_on: - - redis - redis: - image: redis:6 + - 8000:8000 + volumes: + - shortener_data:/db + +volumes: + shortener_data: diff --git a/poetry.lock b/poetry.lock index 68b4a13..b32cc58 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,11 +6,22 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "asgiref" +version = "3.3.4" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] + [[package]] name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -18,7 +29,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "attrs" version = "21.2.0" description = "Classes Without Boilerplate" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -28,9 +39,17 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] +[[package]] +name = "brotli" +version = "1.0.9" +description = "Python bindings for the Brotli compression library" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "certifi" -version = "2020.12.5" +version = "2021.5.30" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -52,17 +71,6 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -[[package]] -name = "click" -version = "8.0.1" -description = "Composable command line interface toolkit" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "colorama" version = "0.4.4" @@ -80,19 +88,29 @@ optional = false python-versions = "*" [[package]] -name = "dnspython" -version = "2.1.0" -description = "DNS toolkit" +name = "django" +version = "3.2.4" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false python-versions = ">=3.6" +[package.dependencies] +asgiref = ">=3.3.2,<4" +pytz = "*" +sqlparse = ">=0.2.2" + [package.extras] -dnssec = ["cryptography (>=2.6)"] -doh = ["requests", "requests-toolbelt"] -idna = ["idna (>=2.1)"] -curio = ["curio (>=1.2)", "sniffio (>=1.1)"] -trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-environ" +version = "0.4.5" +description = "Django-environ allows you to utilize 12factor inspired environment variables to configure your Django application." +category = "main" +optional = false +python-versions = "*" [[package]] name = "filelock" @@ -102,24 +120,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "flask" -version = "2.0.1" -description = "A simple framework for building complex web applications." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -click = ">=7.1.2" -itsdangerous = ">=2.0" -Jinja2 = ">=3.0" -Werkzeug = ">=2.0" - -[package.extras] -async = ["asgiref (>=3.2)"] -dotenv = ["python-dotenv"] - [[package]] name = "gunicorn" version = "20.1.0" @@ -136,7 +136,7 @@ tornado = ["tornado (>=0.2)"] [[package]] name = "identify" -version = "2.2.6" +version = "2.2.9" description = "File identification library for Python" category = "dev" optional = false @@ -154,42 +154,23 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] -name = "itsdangerous" -version = "2.0.1" -description = "Safely pass data to untrusted environments and back." +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" category = "main" optional = false -python-versions = ">=3.6" +python-versions = "*" [[package]] -name = "jinja2" -version = "3.0.1" -description = "A very fast and expressive template engine." +name = "model-bakery" +version = "1.3.1" +description = "Smart object creation facility for Django." category = "main" optional = false -python-versions = ">=3.6" +python-versions = "*" [package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "markupsafe" -version = "2.0.1" -description = "Safely add untrusted strings to HTML/XML markup." -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "more-itertools" -version = "8.8.0" -description = "More routines for operating on iterables, beyond itertools" -category = "dev" -optional = false -python-versions = ">=3.5" +django = ">=2.2" [[package]] name = "nodeenv" @@ -199,23 +180,11 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "ovh" -version = "0.5.0" -description = "\"Official OVH.com API wrapper\"" -category = "main" -optional = false -python-versions = "*" - -[package.extras] -dev = ["coverage (==3.7.1)", "mock (==1.0.1)", "nose (==1.3.3)", "yanc (==0.2.4)", "Sphinx (==1.2.2)", "coveralls (==0.4.4)", "setuptools (>=30.3.0)", "wheel"] -test = ["coverage (==3.7.1)", "mock (==1.0.1)", "nose (==1.3.3)", "yanc (==0.2.4)"] - [[package]] name = "packaging" version = "20.9" description = "Core utilities for Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -226,7 +195,7 @@ pyparsing = ">=2.0.2" name = "pluggy" version = "0.13.1" description = "plugin and hook calling mechanisms for python" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -253,7 +222,7 @@ virtualenv = ">=20.0.8" name = "py" version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -261,31 +230,53 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "pyparsing" version = "2.4.7" description = "Python parsing module" -category = "dev" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" -version = "5.4.3" +version = "6.2.4" description = "pytest: simple powerful testing with Python" -category = "dev" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-django" +version = "4.4.0" +description = "A Django plugin for pytest." +category = "main" optional = false python-versions = ">=3.5" [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=17.4.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -more-itertools = ">=4.0.0" -packaging = "*" -pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -wcwidth = "*" +pytest = ">=5.4.0" [package.extras] -checkqa-mypy = ["mypy (==v0.761)"] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +docs = ["sphinx", "sphinx-rtd-theme"] +testing = ["django", "django-configurations (>=2.0)"] + +[[package]] +name = "pytz" +version = "2021.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" [[package]] name = "pyyaml" @@ -295,17 +286,6 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -[[package]] -name = "redis" -version = "3.5.3" -description = "Python client for Redis key-value store" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.extras] -hiredis = ["hiredis (>=0.1.3)"] - [[package]] name = "requests" version = "2.25.1" @@ -332,11 +312,19 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "sqlparse" +version = "0.4.1" +description = "A non-validating SQL parser." +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" @@ -372,34 +360,33 @@ docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sp testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] [[package]] -name = "wcwidth" -version = "0.2.5" -description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "werkzeug" -version = "2.0.1" -description = "The comprehensive WSGI web application library." +name = "whitenoise" +version = "5.2.0" +description = "Radically simplified static file serving for WSGI applications" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.5, <4" + +[package.dependencies] +Brotli = {version = "*", optional = true, markers = "extra == \"brotli\""} [package.extras] -watchdog = ["watchdog"] +brotli = ["brotli"] [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "524e8d0aa500e21953ad6bb630504c3455d1dadaf2b310b22b52a233496221a7" +content-hash = "0e2349ff77825e1aefa5d3f709364571f9a745ebc7bfba5414b17432067660eb" [metadata.files] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] +asgiref = [ + {file = "asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee"}, + {file = "asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"}, +] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, @@ -408,9 +395,43 @@ attrs = [ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] +brotli = [ + {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"}, + {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"}, + {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6"}, + {file = "Brotli-1.0.9-cp27-cp27m-win32.whl", hash = "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa"}, + {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452"}, + {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7"}, + {file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"}, + {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"}, + {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"}, + {file = "Brotli-1.0.9-cp35-cp35m-win32.whl", hash = "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1"}, + {file = "Brotli-1.0.9-cp35-cp35m-win_amd64.whl", hash = "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea"}, + {file = "Brotli-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f"}, + {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"}, + {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"}, + {file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"}, + {file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"}, + {file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"}, + {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"}, + {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"}, + {file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"}, + {file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"}, + {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8"}, + {file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"}, + {file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"}, + {file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"}, + {file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"}, + {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7"}, + {file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"}, + {file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"}, + {file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"}, + {file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"}, + {file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"}, +] certifi = [ - {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, - {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, + {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, + {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, ] cfgv = [ {file = "cfgv-3.3.0-py2.py3-none-any.whl", hash = "sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1"}, @@ -420,10 +441,6 @@ chardet = [ {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] -click = [ - {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, - {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, -] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, @@ -432,86 +449,42 @@ distlib = [ {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"}, {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"}, ] -dnspython = [ - {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, - {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, +django = [ + {file = "Django-3.2.4-py3-none-any.whl", hash = "sha256:ea735cbbbb3b2fba6d4da4784a0043d84c67c92f1fdf15ad6db69900e792c10f"}, + {file = "Django-3.2.4.tar.gz", hash = "sha256:66c9d8db8cc6fe938a28b7887c1596e42d522e27618562517cc8929eb7e7f296"}, +] +django-environ = [ + {file = "django-environ-0.4.5.tar.gz", hash = "sha256:6c9d87660142608f63ec7d5ce5564c49b603ea8ff25da595fd6098f6dc82afde"}, + {file = "django_environ-0.4.5-py2.py3-none-any.whl", hash = "sha256:c57b3c11ec1f319d9474e3e5a79134f40174b17c7cc024bbb2fad84646b120c4"}, ] filelock = [ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, ] -flask = [ - {file = "Flask-2.0.1-py3-none-any.whl", hash = "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9"}, - {file = "Flask-2.0.1.tar.gz", hash = "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55"}, -] gunicorn = [ {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, ] identify = [ - {file = "identify-2.2.6-py2.py3-none-any.whl", hash = "sha256:1560bb645b93d5c05c3535c72a4f4884133006423d02c692ac6862a45eb0d521"}, - {file = "identify-2.2.6.tar.gz", hash = "sha256:01ebbc7af37043806216c7550539210cde4f82451983eb8735a02b3b9d013e40"}, + {file = "identify-2.2.9-py2.py3-none-any.whl", hash = "sha256:96c57d493184daecc7299acdeef0ad7771c18a59931ea927942df393688fe849"}, + {file = "identify-2.2.9.tar.gz", hash = "sha256:3a8493cf49cfe4b28d50865e38f942c11be07a7b0aab8e715073e17f145caacc"}, ] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] -itsdangerous = [ - {file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"}, - {file = "itsdangerous-2.0.1.tar.gz", hash = "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"}, +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] -jinja2 = [ - {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, - {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, -] -markupsafe = [ - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, -] -more-itertools = [ - {file = "more-itertools-8.8.0.tar.gz", hash = "sha256:83f0308e05477c68f56ea3a888172c78ed5d5b3c282addb67508e7ba6c8f813a"}, - {file = "more_itertools-8.8.0-py3-none-any.whl", hash = "sha256:2cf89ec599962f2ddc4d568a05defc40e0a587fbc10d5989713638864c36be4d"}, +model-bakery = [ + {file = "model_bakery-1.3.1-py2.py3-none-any.whl", hash = "sha256:3bd63ac4135a6918f1b7bb3da31cd9c44283d99c37dc6315c13d061026d6ab2d"}, + {file = "model_bakery-1.3.1.tar.gz", hash = "sha256:713b2d32319e033b221714b5c3a3e1c88e0c40c84481b21955c44979801e5e03"}, ] nodeenv = [ {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, ] -ovh = [ - {file = "ovh-0.5.0-py2.py3-none-any.whl", hash = "sha256:03c7a5e7a62e7bc09b899f7692c694360be7db93ebe44428d6605fccda244692"}, - {file = "ovh-0.5.0.tar.gz", hash = "sha256:f74d190c4bff0953d76124cb8ed319a8a999138720e42957f0db481ef4746ae8"}, -] packaging = [ {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, @@ -533,8 +506,16 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, - {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, +] +pytest-django = [ + {file = "pytest-django-4.4.0.tar.gz", hash = "sha256:b5171e3798bf7e3fc5ea7072fe87324db67a4dd9f1192b037fed4cc3c1b7f455"}, + {file = "pytest_django-4.4.0-py3-none-any.whl", hash = "sha256:65783e78382456528bd9d79a35843adde9e6a47347b20464eb2c885cb0f1f606"}, +] +pytz = [ + {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, + {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, @@ -559,10 +540,6 @@ pyyaml = [ {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] -redis = [ - {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, - {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, -] requests = [ {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, @@ -571,6 +548,10 @@ six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +sqlparse = [ + {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, + {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -583,11 +564,7 @@ virtualenv = [ {file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"}, {file = "virtualenv-20.4.7.tar.gz", hash = "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467"}, ] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, -] -werkzeug = [ - {file = "Werkzeug-2.0.1-py3-none-any.whl", hash = "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8"}, - {file = "Werkzeug-2.0.1.tar.gz", hash = "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42"}, +whitenoise = [ + {file = "whitenoise-5.2.0-py2.py3-none-any.whl", hash = "sha256:05d00198c777028d72d8b0bbd234db605ef6d60e9410125124002518a48e515d"}, + {file = "whitenoise-5.2.0.tar.gz", hash = "sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7"}, ] diff --git a/pyproject.toml b/pyproject.toml index 4bb7f3f..75a3bf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,15 +6,16 @@ authors = ["Gabriel Augendre "] [tool.poetry.dependencies] python = "^3.9" -dnspython = "^2.1.0" -Flask = "^2.0.0" -ovh = "^0.5.0" -redis = "^3.5.3" requests = "^2.25.1" gunicorn = "^20.1.0" +Django = "^3.2.4" +model-bakery = "^1.3.1" +pytest-django = "^4.4.0" +django-environ = "^0.4.5" +whitenoise = {extras = ["brotli"], version = "^5.2.0"} [tool.poetry.dev-dependencies] -pytest = "^5.2" +pytest = "^6.2" pre-commit = "^2.13.0" [build-system] @@ -26,3 +27,20 @@ target-version = ['py38'] [tool.isort] profile = "black" + +[tool.pytest.ini_options] +addopts = "--color=yes" +minversion = "6.0" +DJANGO_SETTINGS_MODULE = "shortener.settings" +junit_family = "xunit1" +norecursedirs = [ + ".git", + ".pytest_cache", +] +testpaths = [ + "src", +] +python_files = [ + "test_*.py", + "tests.py", +] diff --git a/shortener/__init__.py b/shortener/__init__.py deleted file mode 100644 index 3dc1f76..0000000 --- a/shortener/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.1.0" diff --git a/shortener/api_connectors/base_api_connector.py b/shortener/api_connectors/base_api_connector.py deleted file mode 100644 index eaaf647..0000000 --- a/shortener/api_connectors/base_api_connector.py +++ /dev/null @@ -1,6 +0,0 @@ -from shortener.data_structures import UserInput - - -class BaseApiConnector: - def add_record(self, user_input: UserInput) -> None: - raise NotImplementedError("Subclasses must implement add_record") diff --git a/shortener/api_connectors/ovh/ovh_api.py b/shortener/api_connectors/ovh/ovh_api.py deleted file mode 100644 index f66108c..0000000 --- a/shortener/api_connectors/ovh/ovh_api.py +++ /dev/null @@ -1,38 +0,0 @@ -import os - -import ovh -from ovh import HTTPError, InvalidResponse - -from shortener.api_connectors.base_api_connector import BaseApiConnector -from shortener.data_structures import UserInput -from shortener.exceptions import ApiException - - -class OvhApi(BaseApiConnector): - def __init__(self): - endpoint = os.environ["OVH_ENDPOINT"] - application_key = os.environ["OVH_APPLICATION_KEY"] - application_secret = os.environ["OVH_APPLICATION_SECRET"] - consumer_key = os.environ["OVH_CONSUMER_KEY"] - self._base_domain = os.environ["BASE_DOMAIN"] - self._client = ovh.Client( - endpoint=endpoint, - application_key=application_key, - application_secret=application_secret, - consumer_key=consumer_key, - ) - - def add_record(self, user_input: UserInput) -> None: - try: - self._add_record(user_input) - except (HTTPError, InvalidResponse) as e: - raise ApiException(message=str(e)) - - def _add_record(self, user_input: UserInput) -> None: - self._client.post( - f"/domain/zone/{self._base_domain}/record", - fieldType="TXT", - subDomain=user_input.short_code, - target=user_input.url, - ) - self._client.post(f"/domain/zone/{self._base_domain}/refresh") diff --git a/shortener/cache.py b/shortener/cache.py deleted file mode 100644 index b9ec040..0000000 --- a/shortener/cache.py +++ /dev/null @@ -1,8 +0,0 @@ -import os - -import redis - -REDIS_HOST = os.getenv("REDIS_HOST", "localhost") -REDIS_PORT = int(os.getenv("REDIS_PORT", 6379)) -REDIS_DB = int(os.getenv("REDIS_DB", 0)) -cache = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB) diff --git a/shortener/data_structures.py b/shortener/data_structures.py deleted file mode 100644 index 38738cf..0000000 --- a/shortener/data_structures.py +++ /dev/null @@ -1,13 +0,0 @@ -from dataclasses import dataclass - -from flask import request - - -@dataclass -class UserInput: - short_code: str - url: str - - @property - def short_url(self): - return f"{request.host_url}{self.short_code}" diff --git a/shortener/exceptions.py b/shortener/exceptions.py deleted file mode 100644 index f795091..0000000 --- a/shortener/exceptions.py +++ /dev/null @@ -1,13 +0,0 @@ -class ApiException(Exception): - def __init__(self, message="Unknown server error", code=500): - self.message = message - self.code = code - - -class BadRequestException(ApiException): - def __init__(self, message="Bad request", code=400): - super().__init__(message, code) - - -class ShortCodeNotFoundException(Exception): - pass diff --git a/shortener/handlers.py b/shortener/handlers.py deleted file mode 100644 index 70fa2ab..0000000 --- a/shortener/handlers.py +++ /dev/null @@ -1,79 +0,0 @@ -import os -import re -from typing import Dict -from urllib.parse import urlparse - -from flask import request - -from shortener.api_connectors.base_api_connector import BaseApiConnector -from shortener.api_connectors.ovh.ovh_api import OvhApi -from shortener.cache import cache -from shortener.data_structures import UserInput -from shortener.exceptions import BadRequestException, ShortCodeNotFoundException -from shortener.resolver import resolver - -SUPPORTED_PROVIDERS: Dict[str, type] = {"ovh": OvhApi} -PROVIDER_NAME = os.environ["PROVIDER"].lower() -PROVIDER_CLASS = SUPPORTED_PROVIDERS[PROVIDER_NAME] -COOLDOWN = "cooldown" - - -def handle_shorten(): - user_input: UserInput = _validate_user_input() - cooldown = cache.get(user_input.short_code) - if cooldown: - raise BadRequestException( - message=f"Please wait before submitting again for {user_input.short_code}." - ) - provider: BaseApiConnector = _get_provider() - provider.add_record(user_input) - cache.set(user_input.short_code, COOLDOWN, ex=90) - return {"status": "ok", "url": user_input.short_url} - - -def _validate_user_input(): - user_input = request.json - if not user_input: - user_input = request.form - url = user_input.get("url") - if not url: - raise BadRequestException(message="`url` is required") - parsed_url = urlparse(url) - if not all([parsed_url.scheme, parsed_url.netloc]): - raise BadRequestException( - message="`url` doesn't seem well formatted. " - "Please include at least protocol and domain name. " - "E.g. 'https://google.com' instead of 'google.com'." - ) - - short_code = user_input.get("short_code") - if not short_code: - raise BadRequestException(message="`short_code` is required") - short_code = str(short_code) - if len(short_code) < 4: - raise BadRequestException( - message="`short_code` must be longer than 4 characters." - ) - reg = re.compile(r"^[a-zA-Z0-9_-]+$") - if not reg.match(short_code): - raise BadRequestException( - message="`short_code` must only contain non accentuated letters, " - "digits, dashes (-) and underscores (_)." - ) - if _short_code_exists(short_code): - raise BadRequestException(message=f"`{short_code}` is already taken") - - user_input = UserInput(short_code=short_code, url=url) - return user_input - - -def _get_provider() -> BaseApiConnector: - return PROVIDER_CLASS() - - -def _short_code_exists(short_code): - try: - resolver.resolve_shortcode(short_code) - return True - except ShortCodeNotFoundException: - return False diff --git a/shortener/main.py b/shortener/main.py deleted file mode 100644 index 93fcfb0..0000000 --- a/shortener/main.py +++ /dev/null @@ -1,43 +0,0 @@ -import os - -from flask import Flask, redirect, render_template, request - -from shortener.exceptions import ApiException, ShortCodeNotFoundException -from shortener.handlers import handle_shorten -from shortener.resolver import resolver - -app = Flask(__name__) - - -@app.route("/") -def home(): - return render_template("welcome.html") - - -@app.route("/shorten", methods=["POST"]) -def shorten_url(): - try: - result = handle_shorten() - code = 200 - except ApiException as e: - result = {"message": e.message, "status": "error"} - code = e.code - is_api = bool(request.json) - if is_api: - return result, code - - return render_template("result.html", **result), code - - -@app.route("/") -def expand_url(shortcode: str): - try: - redirect_url = resolver.resolve_shortcode(shortcode) - except ShortCodeNotFoundException: - return render_template("not_found.html") - return redirect(redirect_url, code=302) - - -if __name__ == "__main__": - port = int(os.environ.get("PORT", 5000)) - app.run(host="0.0.0.0", port=port) diff --git a/shortener/resolver.py b/shortener/resolver.py deleted file mode 100644 index f8711f7..0000000 --- a/shortener/resolver.py +++ /dev/null @@ -1,25 +0,0 @@ -import os - -from dns.name import from_text -from dns.resolver import Resolver - -from shortener.exceptions import ShortCodeNotFoundException - -BASE_DOMAIN = os.getenv("BASE_DOMAIN") - - -class ShortenerResolver(Resolver): - def __init__(self): - super().__init__() - self.search = [from_text(BASE_DOMAIN + ".")] - - def resolve_shortcode(self, shortcode): - try: - answer = self.resolve(shortcode, "TXT", search=True, lifetime=10) - redirect_url = list(answer)[0].strings[0].decode("utf-8") - return redirect_url - except: - raise ShortCodeNotFoundException() - - -resolver = ShortenerResolver() diff --git a/shortener/templates/base.html b/shortener/templates/base.html deleted file mode 100644 index bd2ba12..0000000 --- a/shortener/templates/base.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - URL shortener - - -{% block content %} -{% endblock %} - - diff --git a/shortener/templates/not_found.html b/shortener/templates/not_found.html deleted file mode 100644 index f4a5f55..0000000 --- a/shortener/templates/not_found.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

URL not found

-

If you recently created the redirect, please wait a couple of minutes for DNS to propagate.

-

If you've been sent this URL, please contact the sender. It's likely there's an issue with the URL you received.

-{% endblock %} diff --git a/shortener/templates/result.html b/shortener/templates/result.html deleted file mode 100644 index b97fe78..0000000 --- a/shortener/templates/result.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

Result

-

Status: {{ status }}

- {% if message %} -

{{ message }}

- {% endif %} - {% if url %} -

- Your short URL is: {{ url }}.
- Please wait a couple of minutes for DNS to propagate before testing and - sharing this URL. -

- {% endif %} -

- - Go back - -

-{% endblock %} diff --git a/shortener/templates/welcome.html b/shortener/templates/welcome.html deleted file mode 100644 index 69102f3..0000000 --- a/shortener/templates/welcome.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

Shorten urls

-

Welcome to the URL shortener.

-
-

- - -

-

- - -

-

- - -

-
-{% endblock %} diff --git a/shortener/wsgi.py b/shortener/wsgi.py deleted file mode 100644 index 9bfa081..0000000 --- a/shortener/wsgi.py +++ /dev/null @@ -1,4 +0,0 @@ -from shortener.main import app - -if __name__ == "__main__": - app.run() diff --git a/src/manage.py b/src/manage.py new file mode 100755 index 0000000..0b15080 --- /dev/null +++ b/src/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shortener.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/shortener/api_connectors/__init__.py b/src/redirect/__init__.py similarity index 100% rename from shortener/api_connectors/__init__.py rename to src/redirect/__init__.py diff --git a/src/redirect/admin.py b/src/redirect/admin.py new file mode 100644 index 0000000..bb7d9da --- /dev/null +++ b/src/redirect/admin.py @@ -0,0 +1,14 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin + +from redirect.models import Redirect + +from .models import RedirectUser + + +@admin.register(Redirect) +class RedirectAdmin(admin.ModelAdmin): + list_display = ["short_code", "target_url"] + + +admin.site.register(RedirectUser, UserAdmin) diff --git a/src/redirect/apps.py b/src/redirect/apps.py new file mode 100644 index 0000000..ca6afa1 --- /dev/null +++ b/src/redirect/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RedirectConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "redirect" diff --git a/src/redirect/migrations/0001_initial.py b/src/redirect/migrations/0001_initial.py new file mode 100644 index 0000000..a71b6bf --- /dev/null +++ b/src/redirect/migrations/0001_initial.py @@ -0,0 +1,148 @@ +# Generated by Django 3.2.4 on 2021-06-07 14:01 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="Redirect", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("short_code", models.CharField(max_length=250, unique=True)), + ("target_url", models.URLField()), + ], + ), + migrations.CreateModel( + name="RedirectUser", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/shortener/api_connectors/ovh/__init__.py b/src/redirect/migrations/__init__.py similarity index 100% rename from shortener/api_connectors/ovh/__init__.py rename to src/redirect/migrations/__init__.py diff --git a/src/redirect/models.py b/src/redirect/models.py new file mode 100644 index 0000000..f18d7f3 --- /dev/null +++ b/src/redirect/models.py @@ -0,0 +1,14 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models + + +class Redirect(models.Model): + short_code = models.CharField(max_length=250, blank=False, null=False, unique=True) + target_url = models.URLField() + + def __str__(self): + return f"{self.short_code} => {self.target_url}" + + +class RedirectUser(AbstractUser): + pass diff --git a/tests/__init__.py b/src/redirect/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to src/redirect/tests/__init__.py diff --git a/src/redirect/tests/test_models.py b/src/redirect/tests/test_models.py new file mode 100644 index 0000000..6e52af9 --- /dev/null +++ b/src/redirect/tests/test_models.py @@ -0,0 +1,16 @@ +from django.db import IntegrityError +from django.test import TestCase +from model_bakery import baker + + +class RedirectModelTestCase(TestCase): + def test_has_short_code(self): + baker.make("Redirect", short_code="potain3") + + def test_short_code_is_unique(self): + baker.make("Redirect", short_code="potain3") + with self.assertRaises(IntegrityError): + baker.make("Redirect", short_code="potain3") + + def test_has_target_url(self): + baker.make("Redirect", target_url="https://static.augendre.info/potain3") diff --git a/src/redirect/tests/test_views.py b/src/redirect/tests/test_views.py new file mode 100644 index 0000000..11ff9b4 --- /dev/null +++ b/src/redirect/tests/test_views.py @@ -0,0 +1,32 @@ +from django.test import TestCase +from django.urls import reverse +from model_bakery import baker + +from redirect.models import Redirect + + +class RedirectViewTestCase(TestCase): + def test_redirect_to_target_url(self): + short_code = "potain3" + target_url = "https://static.augendre.info/potain3/" + Redirect.objects.create(short_code=short_code, target_url=target_url) + url = reverse("redirect", kwargs={"short_code": short_code}) + res = self.client.get(url) + assert res.status_code == 302 + assert res.url == target_url + + def test_non_existent_short_code(self): + short_code = "potain3" + target_url = "https://static.augendre.info/potain3/" + Redirect.objects.create(short_code=short_code, target_url=target_url) + url = reverse("redirect", kwargs={"short_code": short_code + "4"}) + res = self.client.get(url) + assert res.status_code == 404 + + +class HomeViewTestCase(TestCase): + def test_home_redirects_to_admin(self): + url = reverse("home") + res = self.client.get(url) + assert res.status_code == 302 + assert res.url == reverse("admin:index") diff --git a/src/redirect/urls.py b/src/redirect/urls.py new file mode 100644 index 0000000..40635cf --- /dev/null +++ b/src/redirect/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from redirect.views import home, redirect_view + +urlpatterns = [ + path("", home, name="home"), + path("", redirect_view, name="redirect"), +] diff --git a/src/redirect/views.py b/src/redirect/views.py new file mode 100644 index 0000000..bac88e3 --- /dev/null +++ b/src/redirect/views.py @@ -0,0 +1,13 @@ +from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect as django_redirect + +from redirect.models import Redirect + + +def redirect_view(request, short_code): + redirect = get_object_or_404(Redirect, short_code=short_code) + return django_redirect(redirect.target_url) + + +def home(request): + return django_redirect("admin:index") diff --git a/src/shortener/__init__.py b/src/shortener/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/shortener/asgi.py b/src/shortener/asgi.py new file mode 100644 index 0000000..b3d586b --- /dev/null +++ b/src/shortener/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for shortener project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shortener.settings") + +application = get_asgi_application() diff --git a/src/shortener/settings.py b/src/shortener/settings.py new file mode 100644 index 0000000..5ebad84 --- /dev/null +++ b/src/shortener/settings.py @@ -0,0 +1,140 @@ +""" +Django settings for shortener project. + +Generated by 'django-admin startproject' using Django 3.2.4. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" +import os +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +import environ + +BASE_DIR = Path(__file__).resolve().parent.parent + +env = environ.Env( + DEBUG=(bool, False), + SECRET_KEY=( + str, + "django-insecure-rk#sto!9js$j6=l+z=@g*(h8if!r(%#u(f(=k6n79d^md0b^=&", + ), + ALLOWED_HOSTS=(list, []), +) +env_file = os.getenv("ENV_FILE") +if env_file: + environ.Env.read_env(env_file) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = env("SECRET_KEY") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = env("DEBUG") + +ALLOWED_HOSTS = env("ALLOWED_HOSTS") + + +# Application definition + +INSTALLED_APPS = [ + "whitenoise.runserver_nostatic", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "redirect", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "shortener.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "shortener.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = {"default": env.db(default=f"sqlite:///{BASE_DIR / 'db.sqlite3'}")} + + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "Europe/Paris" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = "/static/" +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" +STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +AUTH_USER_MODEL = "redirect.RedirectUser" diff --git a/src/shortener/urls.py b/src/shortener/urls.py new file mode 100644 index 0000000..c2dae6c --- /dev/null +++ b/src/shortener/urls.py @@ -0,0 +1,22 @@ +"""shortener URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path("admin/", admin.site.urls), + path("", include("redirect.urls")), +] diff --git a/src/shortener/wsgi.py b/src/shortener/wsgi.py new file mode 100644 index 0000000..44e9ec3 --- /dev/null +++ b/src/shortener/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for shortener project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shortener.settings") + +application = get_wsgi_application() diff --git a/tests/test_shortener.py b/tests/test_shortener.py deleted file mode 100644 index 75b611a..0000000 --- a/tests/test_shortener.py +++ /dev/null @@ -1,5 +0,0 @@ -from shortener import __version__ - - -def test_version(): - assert __version__ == "0.1.0"