Skip to content

Commit

Permalink
Rewrite on aiohttp #3
Browse files Browse the repository at this point in the history
  • Loading branch information
toolen committed Jan 7, 2021
1 parent 4df588c commit 6d49534
Show file tree
Hide file tree
Showing 14 changed files with 1,141 additions and 88 deletions.
12 changes: 7 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
language: python
python:
- "3.7"
- "3.8"
services:
- docker
before_install:
- pip install poetry
install:
- pip install --require-hashes -r requirements/requirements.txt
- pip install --require-hashes -r requirements/dev-requirements.txt
- poetry install
script:
- flake8 --ignore E501 passgen tests
- py.test --cov=passgen tests/
- coveralls
- docker build -t toolen/passgen:latest .
- export VERSION=$(poetry version -s)
- docker build -t toolen/passgen:$VERSION .
- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
- docker push toolen/passgen:latest
- docker push toolen/passgen:$VERSION
61 changes: 50 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,57 @@
FROM python:3.7.6-alpine3.11
FROM python:3.8.7-alpine3.12

LABEL maintainer="dmitrii@zakharov.cc"

ENV PASSGEN_CORS_ENABLED="True"
ENV GUNICORN_CMD_ARGS="-b 0.0.0.0:80"
ENV \
# python:
PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PYTHONDONTWRITEBYTECODE=1 \
# pip:
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
# poetry:
POETRY_VERSION=1.1.4 \
POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=false \
POETRY_CACHE_DIR='/var/cache/pypoetry' \
PATH="$PATH:/root/.poetry/bin" \
# passgen
PASSGEN_CORS_ENABLED="True" \
GUNICORN_CMD_ARGS="-b 0.0.0.0:8080"

WORKDIR .
RUN \
set -ex \
&& apk add --no-cache \
# Installing `tini` utility:
# https://github.com/krallin/tini
tini==0.19.0-r0 \
&& apk add --no-cache --virtual .build-deps \
gcc==9.3.0-r2 \
musl-dev==1.1.24-r10 \
libffi-dev==3.3-r2 \
libressl-dev==3.1.2-r0 \
# Installing `poetry` package manager
&& pip install --no-cache-dir "poetry==$POETRY_VERSION" \
# Setting up proper permissions:
&& addgroup -S passgen \
&& adduser -S -h /srv/passgen -G passgen passgen

COPY ./passgen /passgen
COPY requirements/requirements.txt requirements.txt
COPY --chown=passgen:passgen ./poetry.lock ./pyproject.toml /srv/passgen/

RUN pip install --require-hashes --no-cache-dir -r requirements.txt && \
addgroup -S gunicorn && \
adduser -S -G gunicorn gunicorn && \
chown -R gunicorn:gunicorn passgen
WORKDIR /srv/passgen

CMD ["gunicorn", "-u", "gunicorn", "-g", "gunicorn", "passgen.app:application"]
# Project initialization:
RUN poetry install --no-dev --no-interaction --no-ansi \
# Cleaning
&& rm -rf "$POETRY_CACHE_DIR" \
&& apk del .build-deps

COPY --chown=passgen:passgen ./passgen /srv/passgen/passgen/

# Running as non-root user:
USER passgen

CMD ["/sbin/tini", "--", "gunicorn", "--worker-class", "aiohttp.worker.GunicornWebWorker", "--chdir", "/srv/passgen", "passgen.app:create_app"]
10 changes: 1 addition & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,5 @@ GUNICORN_CMD_ARGS="--bind=127.0.0.1:8080 --workers=3"
### Docker

```
docker run -d -p 80:80 --restart always toolen/passgen:latest
```

## Development
```shell
virtualenv --python python3.7 --prompt "(passgen) " venv
. venv/bin/activate
pip install --require-hashes -r requirements/requirements.txt
pip install --require-hashes -r requirements/dev-requirements.txt
docker run -d -p 80:8080 --restart always toolen/passgen:2.0.0
```
2 changes: 1 addition & 1 deletion docs/source/deploy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Docker

Use docker container::

docker run -d -p 80:80 --restart always toolen/passgen:latest
docker run -d -p 80:8080 --restart always toolen/passgen:2.0.0
21 changes: 15 additions & 6 deletions passgen/app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import falcon
from aiohttp import web
from aiohttp.abc import Application

from .cors import cors
from .passwords.resources import PasswordsResource
from passgen.cors import init_cors
from passgen.routes import init_routes, init_routes_with_cors
from passgen.settings import CORS_ENABLED

application = falcon.API(middleware=[cors.middleware])

passwords = PasswordsResource()
application.add_route('/api/v1/passwords', passwords)
async def create_app() -> web.Application:
app: Application = web.Application()

if CORS_ENABLED:
cors = init_cors(app)
init_routes_with_cors(app, cors)
else:
init_routes(app)

return app
25 changes: 17 additions & 8 deletions passgen/cors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from falcon_cors import CORS
import aiohttp_cors
from aiohttp import web
from aiohttp_cors import CorsConfig, ResourceOptions

default_headers = (
'accept',
Expand All @@ -13,14 +15,21 @@
)

default_methods = (
'DELETE',
# 'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
# 'PATCH',
# 'POST',
# 'PUT',
)

cors = CORS(allow_all_origins=True,
allow_headers_list=default_headers,
allow_methods_list=default_methods)

def init_cors(app: web.Application) -> CorsConfig:
cors = aiohttp_cors.setup(app, defaults={
'*': ResourceOptions(
allow_headers=default_headers,
allow_methods=default_methods
)
})

return cors
26 changes: 0 additions & 26 deletions passgen/passwords/resources.py

This file was deleted.

19 changes: 15 additions & 4 deletions passgen/passwords/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,22 @@
from .constants import MIN_LENGTH, MAX_LENGTH, ALPHABET, REQUIRED_SEQUENCES


def validate_length(length):
if length is None:
raise AssertionError('Must be not None')

if type(length) is not int:
raise AssertionError('Must be an integer')

if length < MIN_LENGTH:
raise AssertionError(f'Less than the minimum length {MIN_LENGTH}.')

if length > MAX_LENGTH:
raise AssertionError(f'Greater than the maximum length {MAX_LENGTH}')


def get_password(length):
assert length is not None, 'Must be not None'
assert type(length) is int, 'Must be an integer'
assert length >= MIN_LENGTH, 'Less than the minimum length {0}.'.format(MIN_LENGTH)
assert length <= MAX_LENGTH, 'Greater than the maximum length {0}'.format(MAX_LENGTH)
validate_length(length)

password = []
for _ in range(0, length):
Expand Down
22 changes: 22 additions & 0 deletions passgen/passwords/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from aiohttp import web

from .constants import DEFAULT_LENGTH
from .services import get_password


async def passwords(request: web.Request):
try:
length_str = request.rel_url.query.get('length', DEFAULT_LENGTH)
length = int(length_str)
password = get_password(length)
return web.json_response({'password': password})
except ValueError:
return web.json_response({
'title': 'Invalid parameter',
'description': 'The "length" parameter is invalid.'
}, status=400)
except AssertionError as exc:
return web.json_response({
'title': 'Invalid parameter',
'description': f'The "length" parameter is invalid. {str(exc)}.'
}, status=400)
14 changes: 14 additions & 0 deletions passgen/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from aiohttp import web
from aiohttp_cors import CorsConfig

from passgen.passwords.views import passwords


def init_routes_with_cors(app: web.Application, cors: CorsConfig) -> None:
resource = cors.add(app.router.add_resource('/api/v1/passwords', name='passwords'))
cors.add(resource.add_route('GET', passwords))


def init_routes(app: web.Application) -> None:
add_route = app.router.add_route
add_route('GET', '/api/v1/passwords', passwords, name='passwords')
Loading

0 comments on commit 6d49534

Please sign in to comment.