Skip to content

Commit

Permalink
ft: add ability to exclude special characters #1
Browse files Browse the repository at this point in the history
  • Loading branch information
toolen committed Apr 4, 2021
1 parent 8028313 commit dc73569
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 48 deletions.
10 changes: 9 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ You can specify a length of generated password by adding the "length" parameter
https://passgen.zakharov.cc/api/v1/passwords?length=6


Exclude punctuation marks
-------------------------

You can generate password without punctuation marks by adding the "exclude_punctuation" parameter to your request. Valid values for the parameter: true, on, ok, y, yes, 1::

https://passgen.zakharov.cc/api/v1/passwords?exclude_punctuation=true


Settings
==========

Expand Down Expand Up @@ -88,4 +96,4 @@ Docker

Use docker container::

docker run -d -p 8080:8080 --restart always toolen/passgen:2.0.1
docker run -d -p 8080:8080 --restart=always --cap-drop=ALL toolen/passgen:2.1.0
6 changes: 6 additions & 0 deletions passgen/passwords/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@
DIGITS = string.digits
PUNCTUATION = string.punctuation
ALPHABET = ASCII_LETTERS + DIGITS + PUNCTUATION
ALPHABET_WO_PUNCTUATION = ASCII_LETTERS + DIGITS
REQUIRED_SEQUENCES = (
ASCII_LOWERCASE,
ASCII_UPPERCASE,
DIGITS,
PUNCTUATION,
)
REQUIRED_SEQUENCES_WO_PUNCTUATION = (
ASCII_LOWERCASE,
ASCII_UPPERCASE,
DIGITS,
)
22 changes: 18 additions & 4 deletions passgen/passwords/services.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
"""This file contains service methods to generate passwords."""
import secrets

from .constants import ALPHABET, MAX_LENGTH, MIN_LENGTH, REQUIRED_SEQUENCES
from .constants import (
ALPHABET,
ALPHABET_WO_PUNCTUATION,
MAX_LENGTH,
MIN_LENGTH,
REQUIRED_SEQUENCES,
REQUIRED_SEQUENCES_WO_PUNCTUATION,
)


def validate_length(length: int) -> None:
Expand All @@ -25,22 +32,29 @@ def validate_length(length: int) -> None:
raise AssertionError(f"Greater than the maximum length {MAX_LENGTH}")


def get_password(length: int) -> str:
def get_password(length: int, exclude_punctuation: bool = False) -> str:
"""
Return password.
:param int length: password length
:param bool exclude_punctuation: generate password without special chars
:return: password
:rtype: str
"""
validate_length(length)

alphabet = ALPHABET_WO_PUNCTUATION if exclude_punctuation else ALPHABET
sequences = (
REQUIRED_SEQUENCES_WO_PUNCTUATION if exclude_punctuation else REQUIRED_SEQUENCES
)

password = []
for _ in range(0, length):
password.append(secrets.choice(ALPHABET))
password.append(secrets.choice(alphabet))

idx_list = list([x for x in range(0, length)])
for sequence in REQUIRED_SEQUENCES:

for sequence in sequences:
idx = secrets.choice(idx_list)
idx_list.remove(idx)
password[idx] = secrets.choice(sequence)
Expand Down
18 changes: 15 additions & 3 deletions passgen/passwords/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
"""This file contains application views."""
from typing import Final

import multidict
from aiohttp import web
from aiohttp.web_response import Response

from passgen.utils import BOOL_TRUE_STRINGS

from .constants import DEFAULT_LENGTH
from .services import get_password

Expand All @@ -15,9 +20,16 @@ async def passwords(request: web.Request) -> Response:
:return: Response with generated password
"""
try:
length_str = request.rel_url.query.get("length", DEFAULT_LENGTH)
length = int(length_str)
password = get_password(length)
query_params: Final[multidict.MultiDict[str]] = request.rel_url.query

raw_length: str = query_params.get("length", str(DEFAULT_LENGTH))
length: int = int(raw_length)

raw_exclude_punctuation: str = query_params.get("exclude_punctuation", "")
exclude_punctuation: bool = raw_exclude_punctuation in BOOL_TRUE_STRINGS

password = get_password(length, exclude_punctuation)

return web.json_response({"password": password})
except ValueError:
return web.json_response(
Expand Down
19 changes: 10 additions & 9 deletions passgen/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
import os
from typing import Union

BOOL_TRUE_STRINGS = (
"true",
"on",
"ok",
"y",
"yes",
"1",
)


def get_bool_env(key: str, default: bool = False) -> bool:
"""
Expand All @@ -12,17 +21,9 @@ def get_bool_env(key: str, default: bool = False) -> bool:
:return: boolean value from environment variable
:rtype: bool
"""
bool_true_strings = (
"true",
"on",
"ok",
"y",
"yes",
"1",
)
value: Union[str, None] = os.getenv(key)
if value is not None:
return value.lower() in bool_true_strings
return value.lower() in BOOL_TRUE_STRINGS
else:
return bool(default)

Expand Down
49 changes: 24 additions & 25 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "passgen"
version = "2.0.1"
version = "2.1.0"
description = "A simple service for generating passwords with guaranteed presence of uppercase and lowercase letters, numbers and special characters."
authors = ["Dmitrii Zakharov <dmitrii@zakharov.cc>"]
license = "MIT"
Expand All @@ -9,7 +9,7 @@ license = "MIT"
python = "^3.8"
aiohttp = "3.7.4.post0"
aiohttp_cors = "0.7.0"
gunicorn = "20.0.4"
gunicorn = "20.1.0"
certifi = "2020.12.5"

[tool.poetry.dev-dependencies]
Expand All @@ -20,11 +20,11 @@ coveralls = "3.0.1"
flake8 = "3.9.0"
pytest = "6.2.2"
black = "20.8b1"
isort = "5.7.0"
isort = "5.8.0"
bandit = "1.7.0"
safety = "1.10.3"
mypy = "0.812"
pydocstyle = "5.1.1"
pydocstyle = "6.0.0"

[tool.black]
skip-string-normalization = 1
Expand Down
13 changes: 12 additions & 1 deletion tests/test_passwords/test_services.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import pytest

from passgen.passwords.constants import MAX_LENGTH, MIN_LENGTH
from passgen.passwords.constants import (
DEFAULT_LENGTH,
MAX_LENGTH,
MIN_LENGTH,
PUNCTUATION,
)
from passgen.passwords.services import get_password


Expand Down Expand Up @@ -41,3 +46,9 @@ def test_get_password_w_various_valid_length():
password = get_password(length)

assert len(password) == length


def test_get_password_wo_punctuation():
password = get_password(DEFAULT_LENGTH, True)
for char in PUNCTUATION:
assert char not in password
29 changes: 28 additions & 1 deletion tests/test_passwords/test_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import pytest

from passgen.passwords.constants import DEFAULT_LENGTH, MAX_LENGTH, MIN_LENGTH
from passgen.passwords.constants import (
DEFAULT_LENGTH,
MAX_LENGTH,
MIN_LENGTH,
PUNCTUATION,
)
from passgen.utils import BOOL_TRUE_STRINGS


async def test_get_password_wo_length_param(client):
Expand Down Expand Up @@ -32,3 +38,24 @@ async def test_get_password_w_various_length(client, length, expected_status_cod
result = await client.get(url)

assert result.status == expected_status_code


@pytest.mark.parametrize(
"param_value",
BOOL_TRUE_STRINGS,
)
async def test_get_password_w_exclude_punctuation_param(client, param_value):
url = (
client.app.router["passwords"]
.url_for()
.with_query({"exclude_punctuation": param_value})
)
result = await client.get(url)

assert result.status == 200

data = await result.json()
password = data.get("password")

for char in PUNCTUATION:
assert char not in password

0 comments on commit dc73569

Please sign in to comment.