From 9964c363269e925978f3b37ef56e205bae6b920d Mon Sep 17 00:00:00 2001 From: DF5HSE Date: Sun, 19 Sep 2021 18:15:38 +0300 Subject: [PATCH 1/4] Add GET and POST endpoints --- .gitignore | 3 +++ main.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 .gitignore create mode 100644 main.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb80269 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea +/__pycache__ +/venv \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..11a52e4 --- /dev/null +++ b/main.py @@ -0,0 +1,38 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, Field + +wsd_app = FastAPI() + +user_list = [ + { + 'mail': 'abc@yandex.ru', + 'password': 'abc_pwd' + }, + { + 'mail': 'qwerty@gmail.com', + 'password': 'qwerty' + } +] + + +@wsd_app.get("/statistics/") +async def get_statistics(indicator: str): + if indicator == "num_of_users": + return {'number of users': len(user_list)} + raise HTTPException(status_code=404, detail="Don't know this indicator") + + +class User(BaseModel): + mail: str = Field(..., regex=r".+@.+\..+") + password1: str + password2: str + + +@wsd_app.post("/registration/", status_code=201) +async def create_account(user: User): + if user.mail in [d['mail'] for d in user_list]: + raise HTTPException(status_code=403, detail="This mail has been already registered") + if user.password1 != user.password2: + raise HTTPException(status_code=403, detail="Passwords don't match") + user_list.append({'mail': user.mail, 'password': user.password1}) + return "Account is created" From 19d5c5d9b806add16fb139d5902ededbbede7d55 Mon Sep 17 00:00:00 2001 From: DF5HSE Date: Mon, 27 Sep 2021 02:25:15 +0300 Subject: [PATCH 2/4] Add task 2 solution --- .gitignore | 5 ++- README.md | 16 +++++++- databases/users.py | 39 ++++++++++++++++++ main.py | 37 +++++++++-------- models/users.py | 21 ++++++++++ tests/integration_tests.py | 58 ++++++++++++++++++++++++++ tests/unit_tests.py | 84 ++++++++++++++++++++++++++++++++++++++ utils.py | 17 ++++++++ 8 files changed, 256 insertions(+), 21 deletions(-) create mode 100644 databases/users.py create mode 100644 models/users.py create mode 100644 tests/integration_tests.py create mode 100644 tests/unit_tests.py create mode 100644 utils.py diff --git a/.gitignore b/.gitignore index bb80269..8d1da93 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.idea -/__pycache__ -/venv \ No newline at end of file +*/__pycache__ +/venv +/.pytest_cash \ No newline at end of file diff --git a/README.md b/README.md index 68a01b0..8a3cf38 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ -# WebServiceDevelopment \ No newline at end of file +# WebServiceDevelopment + +Это репозиторий, в котором будут храниться решение ДЗ по курсу +разработки веб-сервисов на питоне. Домашними заданиями является работа +над проектом. Пока что в качестве проекта сделать "датаграмм" (типо +"телеграмм", но "датаграмм", потому что на курсе по Java нам предлагали +различать UDP и TCP так, что UDP -- это датаграмма, а TCP -- телеграмма). + +## Тестирование +Перед тестированием установите: +`pip install email-validator` + +Запуск юнит тестов: `python -m pytest tests/unit_tests.py` + +Запуск интеграционных тестов: `python -m pytest tests/integration_tests.py` \ No newline at end of file diff --git a/databases/users.py b/databases/users.py new file mode 100644 index 0000000..96da212 --- /dev/null +++ b/databases/users.py @@ -0,0 +1,39 @@ +from typing import Dict, Optional + + +class UserMeta: + def __init__(self, email: str, name: str): + self.email = email + self.name = name + + +__id_pwd: Dict[int, str] = {} +__id_user_meta: Dict[int, UserMeta] = {} + + +def get_user_by_auth(email: str, password: str) -> Optional[UserMeta]: + for (id, user_meta) in __id_user_meta.items(): + if user_meta.email == email: + if __id_pwd[id] != password: + return None + return user_meta + return None + + +def is_email_occupied(email: str) -> bool: + for (_, user_meta) in __id_user_meta.items(): + if user_meta.email == email: + return True + return False + + +def add_user(email: str, name: str, password: str): + if is_email_occupied(email): + raise ValueError("User with this email exists") + id = len(__id_pwd) + __id_pwd[id] = password + __id_user_meta[id] = UserMeta(email, name) + + +def get_num_of_users(): + return len(__id_user_meta) \ No newline at end of file diff --git a/main.py b/main.py index 11a52e4..fe6c1e2 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,8 @@ from fastapi import FastAPI, HTTPException -from pydantic import BaseModel, Field +from models.users import UserCreateModel, UserAuthModel +from databases.users import get_user_by_auth, is_email_occupied, add_user, get_num_of_users -wsd_app = FastAPI() +datagram_app = FastAPI() user_list = [ { @@ -15,24 +16,24 @@ ] -@wsd_app.get("/statistics/") +@datagram_app.get("/statistics/") async def get_statistics(indicator: str): - if indicator == "num_of_users": - return {'number of users': len(user_list)} - raise HTTPException(status_code=404, detail="Don't know this indicator") + if indicator != "num_of_users": + raise HTTPException(status_code=404, detail="Don't know this indicator") + get_num_of_users() -class User(BaseModel): - mail: str = Field(..., regex=r".+@.+\..+") - password1: str - password2: str +@datagram_app.post("/users/registration", status_code=201) +async def register_user(user: UserCreateModel): + if is_email_occupied(user.email): + raise HTTPException(status_code=403, detail="This mail has been already registered") + add_user(user.email, user.name, user.password.get_secret_value()) + return "Registered successfully" -@wsd_app.post("/registration/", status_code=201) -async def create_account(user: User): - if user.mail in [d['mail'] for d in user_list]: - raise HTTPException(status_code=403, detail="This mail has been already registered") - if user.password1 != user.password2: - raise HTTPException(status_code=403, detail="Passwords don't match") - user_list.append({'mail': user.mail, 'password': user.password1}) - return "Account is created" +@datagram_app.post("/users/auth") +async def authorize(user_auth: UserAuthModel): + user = get_user_by_auth(user_auth.email, user_auth.password.get_secret_value()) + if user is None: + raise HTTPException(status_code=403, detail="Wrong mail or password") + return user diff --git a/models/users.py b/models/users.py new file mode 100644 index 0000000..0d13046 --- /dev/null +++ b/models/users.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel, EmailStr, SecretStr, validator +from utils import validate_new_password + + +class UserAuthModel(BaseModel): + email: EmailStr + password: SecretStr + + @validator('password') + def password_acceptable(cls, v): + validate_new_password(v) + return v + + +class UserCreateModel(UserAuthModel): + name: str + + @validator('name') + def name_alphanumeric(cls, v): + assert v.isalnum(), 'must be alphanumeric' + return v diff --git a/tests/integration_tests.py b/tests/integration_tests.py new file mode 100644 index 0000000..f130405 --- /dev/null +++ b/tests/integration_tests.py @@ -0,0 +1,58 @@ +import pytest +from fastapi.testclient import TestClient + +from main import datagram_app + +client = TestClient(datagram_app) + + +@pytest.fixture() +def setup_and_teardown_db(): + from databases.users import __id_pwd, __id_user_meta + save_pwd = __id_pwd.copy() + save_meta = __id_user_meta.copy() + __id_pwd.clear() + __id_user_meta.clear() + + __id_pwd = save_pwd + __id_user_meta = save_meta + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ registration test ~~~~~~~~~~~~~~~~~~ +def test_registration_success(setup_and_teardown_db): + rs = client.post("/users/registration", json={"email": "a@yandex.ru", "name": "a", "password": "Aa1!"}) + assert rs.status_code == 201 + + +def test_registration_wrong_pwd_format(setup_and_teardown_db): + rs = client.post("/users/registration", json={"email": "a@yandex.ru", "name": "a", "password": "a1!"}) + assert rs.status_code == 422 + + +def test_registration_mail_occupied(setup_and_teardown_db): + rs = client.post("/users/registration", json={"email": "a@yandex.ru", "name": "a", "password": "Aa1!"}) + assert rs.status_code == 201 + rs = client.post("/users/registration", json={"email": "a@yandex.ru", "name": "a", "password": "Aa1!"}) + assert rs.status_code == 403 + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ auth test ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +def test_auth_success(setup_and_teardown_db): + rs = client.post("/users/registration", json={"email": "a@yandex.ru", "name": "a", "password": "Aa1!"}) + assert rs.status_code == 201 + rs = client.post("/users/auth", json={"email": "a@yandex.ru", "password": "Aa1!"}) + assert rs.status_code == 200 + + +def test_auth_user_not_exist(setup_and_teardown_db): + rs = client.post("/users/registration", json={"email": "a@yandex.ru", "name": "a", "password": "Aa1!"}) + assert rs.status_code == 201 + rs = client.post("/users/auth", json={"email": "b@yandex.ru", "password": "Aa1!"}) + assert rs.status_code == 403 + + +def test_auth_wrong_password(setup_and_teardown_db): + rs = client.post("/users/registration", json={"email": "a@yandex.ru", "name": "a", "password": "Aa1!"}) + assert rs.status_code == 201 + rs = client.post("/users/auth", json={"email": "a@yandex.ru", "password": "Ba1!"}) + assert rs.status_code == 403 diff --git a/tests/unit_tests.py b/tests/unit_tests.py new file mode 100644 index 0000000..b4c5382 --- /dev/null +++ b/tests/unit_tests.py @@ -0,0 +1,84 @@ +import pytest +from utils import validate_new_password +from databases.users import add_user, get_user_by_auth +from pydantic import SecretStr + + +# ~~~~~~~~~~~~~~~~~ validate password tests ~~~~~~~~~~~~~~~~~~~~ +def test_correct_password(): + validate_new_password(SecretStr("Ab1!")) + + +def test_miss_lowercase(): + with pytest.raises(ValueError) as ve: + validate_new_password(SecretStr("A1!")) + assert ve.value.args[1] == r"[a-z]" + + +def test_miss_uppercase(): + with pytest.raises(ValueError) as ve: + validate_new_password(SecretStr("b1!")) + assert ve.value.args[1] == r"[A-Z]" + + +def test_miss_digit(): + with pytest.raises(ValueError) as ve: + validate_new_password(SecretStr("Ab!")) + assert ve.value.args[1] == r"[0-9]" + + +def test_miss_special_char(): + with pytest.raises(ValueError) as ve: + validate_new_password(SecretStr("Ab1")) + assert ve.value.args[1] == r"[!@#_.]" + + +def test_wrong_char(): + with pytest.raises(ValueError) as ve: + validate_new_password(SecretStr("Ab1!$")) + assert ve.value.args[1] == "wrong char" + + +@pytest.fixture() +def setup_and_teardown_db(): + from databases.users import __id_pwd, __id_user_meta + save_pwd = __id_pwd.copy() + save_meta = __id_user_meta.copy() + __id_pwd.clear() + __id_user_meta.clear() + + __id_pwd = save_pwd + __id_user_meta = save_meta + + +# ~~~~~~~~~~~~~~~~~ add new user tests ~~~~~~~~~~~~~~~~~~~~ +def test_add_user_correct_params(setup_and_teardown_db): + add_user(email="a@yandex.ru", name="a", password="Aa1!") + add_user(email="b@yandex.ru", name="b", password="Bb2@") + + +def test_add_user_with_existing_name(setup_and_teardown_db): + add_user(email="a@yandex.ru", name="a", password="Aa1!") + add_user(email="b@yandex.ru", name="a", password="Aa1!") + + +def test_add_user_occupied_email(setup_and_teardown_db): + add_user(email="a@yandex.ru", name="a", password="Aa1!") + with pytest.raises(ValueError): + add_user(email="a@yandex.ru", name="b", password="Bb2@") + + +# # ~~~~~~~~~~~~~~~~~ get user meta tests ~~~~~~~~~~~~~~~~~~~~ +def test_get_user_meta_correct_params(setup_and_teardown_db): + add_user(email="a@yandex.ru", name="a", password="Aa1!") + assert get_user_by_auth(email="a@yandex.ru", password="Aa1!") is not None + + +def test_get_user_meta_occupied_email(setup_and_teardown_db): + add_user(email="a@yandex.ru", name="a", password="Aa1!") + assert get_user_by_auth(email="aa@yandex.ru", password="Aa1!") is None + + +def test_get_user_meta_wrong_password(setup_and_teardown_db): + add_user(email="a@yandex.ru", name="a", password="Aa1!") + assert get_user_by_auth(email="a@yandex.ru", password="Aa1!!") is None diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..0d07bcd --- /dev/null +++ b/utils.py @@ -0,0 +1,17 @@ +from pydantic import SecretStr +import re + + +def validate_new_password(pwd: SecretStr): + value = pwd.get_secret_value() + if re.search(r"[a-z]", value) is None: + raise ValueError("password must contain lowercase letter", r"[a-z]") + if re.search(r"[A-Z]", value) is None: + raise ValueError("password must contain uppercase letter", r"[A-Z]") + if re.search(r"[0-9]", value) is None: + raise ValueError("password must contain digit", r"[0-9]") + if re.search(r"[!@#_.]", value) is None: + raise ValueError("password must contain special character '!', '@', '#', '_' or '.'", r"[!@#_.]") + m = re.search(r"[^a-zA-Z0-9!@#_.]", value) + if m is not None: + raise ValueError("password contains forbidden character " + m.group(0), "wrong char") From f3f2ae2c938c9afd5458226e6f45073d324523db Mon Sep 17 00:00:00 2001 From: DF5HSE Date: Tue, 5 Oct 2021 19:26:35 +0300 Subject: [PATCH 3/4] Fix mistakes in README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8a3cf38..e02db3a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # WebServiceDevelopment -Это репозиторий, в котором будут храниться решение ДЗ по курсу +Это репозиторий, в котором будут храниться решения ДЗ по курсу разработки веб-сервисов на питоне. Домашними заданиями является работа -над проектом. Пока что в качестве проекта сделать "датаграмм" (типо -"телеграмм", но "датаграмм", потому что на курсе по Java нам предлагали +над проектом. Пока что в качестве проекта сделать "датаграм" (типо +"телеграм", но "датаграм", потому что на курсе по Java нам предлагали различать UDP и TCP так, что UDP -- это датаграмма, а TCP -- телеграмма). ## Тестирование From b9a030693eaaf317d21cf55445b3929a9597d5b2 Mon Sep 17 00:00:00 2001 From: DF5HSE Date: Tue, 5 Oct 2021 22:48:03 +0300 Subject: [PATCH 4/4] Add solution and some changes in previous task files --- README.md | 32 +++++++++++++++++++ databases/users.py | 37 ++++++++++++++-------- main.py | 23 +++++++------- {models => pydantic_models}/users.py | 0 strawberry_types.py | 46 ++++++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 24 deletions(-) rename {models => pydantic_models}/users.py (100%) create mode 100644 strawberry_types.py diff --git a/README.md b/README.md index e02db3a..95cebf0 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,38 @@ "телеграм", но "датаграм", потому что на курсе по Java нам предлагали различать UDP и TCP так, что UDP -- это датаграмма, а TCP -- телеграмма). +## Установка необходимых пакетов +`pip install strawberry-graphql[debug-server]` -- установка библиотеки +[strawberry](https://strawberry.rocks/). +Ещё нужно установить `fastapi` и `pydantic`, позже добавлю инструкции. + +## Запуск +`uvicorn main:datagram_app --reload` -- для запуска. + +http://127.0.0.1:8000/docs -- ручки + +http://127.0.0.1:8000/graphql -- работа с GraphQL + +## GraphQL +Пример запроса: + +``` +{ + users (year:2001) { + name + additionalInfo { + status + birthDate { + year + day + } + } + } +} +``` + +Все схемы и типы указаны в файле `strawbery_types.py` + ## Тестирование Перед тестированием установите: `pip install email-validator` diff --git a/databases/users.py b/databases/users.py index 96da212..bfa17ab 100644 --- a/databases/users.py +++ b/databases/users.py @@ -1,30 +1,42 @@ from typing import Dict, Optional +from datetime import date class UserMeta: - def __init__(self, email: str, name: str): + class AdditionalInfo: + status: Optional[str] + birth_date: Optional[date] + + def __init__(self, status: Optional[str] = None, birth_date: Optional[date] = None): + self.status = status + self.birth_date = birth_date + + email: str + name: str + additional_info: AdditionalInfo + + def __init__(self, email: str, name: str, additional_info: AdditionalInfo = AdditionalInfo()): self.email = email self.name = name + self.additional_info = additional_info __id_pwd: Dict[int, str] = {} __id_user_meta: Dict[int, UserMeta] = {} +__email_id: Dict[str, int] = {} def get_user_by_auth(email: str, password: str) -> Optional[UserMeta]: - for (id, user_meta) in __id_user_meta.items(): - if user_meta.email == email: - if __id_pwd[id] != password: - return None - return user_meta - return None + if not is_email_occupied(email): + return None + id = __email_id[email] + if __id_pwd[id] != password: + return None + return __id_user_meta[id] def is_email_occupied(email: str) -> bool: - for (_, user_meta) in __id_user_meta.items(): - if user_meta.email == email: - return True - return False + return email in __email_id def add_user(email: str, name: str, password: str): @@ -33,7 +45,8 @@ def add_user(email: str, name: str, password: str): id = len(__id_pwd) __id_pwd[id] = password __id_user_meta[id] = UserMeta(email, name) + __email_id[email] = id def get_num_of_users(): - return len(__id_user_meta) \ No newline at end of file + return len(__id_user_meta) diff --git a/main.py b/main.py index fe6c1e2..c8f58ed 100644 --- a/main.py +++ b/main.py @@ -1,19 +1,12 @@ from fastapi import FastAPI, HTTPException -from models.users import UserCreateModel, UserAuthModel +from pydantic_models.users import UserCreateModel, UserAuthModel from databases.users import get_user_by_auth, is_email_occupied, add_user, get_num_of_users +from strawberry_types import Query +from strawberry.asgi import GraphQL +import strawberry -datagram_app = FastAPI() -user_list = [ - { - 'mail': 'abc@yandex.ru', - 'password': 'abc_pwd' - }, - { - 'mail': 'qwerty@gmail.com', - 'password': 'qwerty' - } -] +datagram_app = FastAPI() @datagram_app.get("/statistics/") @@ -37,3 +30,9 @@ async def authorize(user_auth: UserAuthModel): if user is None: raise HTTPException(status_code=403, detail="Wrong mail or password") return user + + +schema = strawberry.Schema(query=Query) +graphql_app = GraphQL(schema) +datagram_app.add_route("/graphql", graphql_app) +datagram_app.add_websocket_route("/graphql", graphql_app) diff --git a/models/users.py b/pydantic_models/users.py similarity index 100% rename from models/users.py rename to pydantic_models/users.py diff --git a/strawberry_types.py b/strawberry_types.py new file mode 100644 index 0000000..6ba6803 --- /dev/null +++ b/strawberry_types.py @@ -0,0 +1,46 @@ +from typing import List +import strawberry + + +@strawberry.type +class Date: + year: int + month: int + day: int + + +@strawberry.type +class AdditionalInfo: + status: str + birth_date: Date + + +@strawberry.type +class UserMeta: + email: str + name: str + additional_info: AdditionalInfo + + +@strawberry.type +class Query: + @strawberry.field + def users(self, year: int) -> List[UserMeta]: + return list(filter(lambda u: u.additional_info.birth_date.year == year, [ + UserMeta( + "hello@world.ru", + "Mike", + AdditionalInfo( + "I am a hero", + Date(2001, 1, 1) + ) + ), + UserMeta( + "hi@world.ru", + "Pol", + AdditionalInfo( + "DevOps", + Date(1001, 1, 1) + ) + ), + ]))