Skip to content

Commit

Permalink
Merge pull request #26 from exactpro/release-8.1.0
Browse files Browse the repository at this point in the history
Release 8.1.0
  • Loading branch information
Svyat935 authored Jul 10, 2021
2 parents ebe0b3a + 7ba279d commit 740acff
Show file tree
Hide file tree
Showing 287 changed files with 10,771 additions and 7,210 deletions.
4 changes: 3 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ POSTGRES_USER=postgres_user
POSTGRES_PASSWORD=postgres_password
POSTGRES_DB=users
POSTGRES_HOST=postgresql
POSTGRES_PORT=5432
POSTGRES_PORT=5432

SENTRY_DSN=
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ repos:
rev: stable
hooks:
- id: black
language_version: python3.7
language_version: python3.8

default_stages: [commit, push]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Nostradamus also calculates various statistical data including distributions and
* a list of the most frequently used terms;
* a list of the most significant words, etc.

This knowledge further allows to achieve various IT-related goals, e.g.:
This knowledge further allows achieving various IT-related goals, e.g.:
* 📝 More accurate planning and goal setting for Project Managers;
* 📈 Improving the defect report quality for QA Engineers and Junior Analysts;
* 🔎 Discovering the dependencies hidden in development, for system architects and developers.
Expand Down
16 changes: 16 additions & 0 deletions auth/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.8-slim

WORKDIR /app

USER root

RUN apt-get -y update && apt-get install -y libpq-dev gcc \
&& pip install --upgrade pip

RUN pip3 install poetry==1.1.4

COPY poetry.lock pyproject.toml /app/

RUN poetry install --no-dev

COPY . /app/
File renamed without changes.
30 changes: 30 additions & 0 deletions auth/authentication/register.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from passlib.context import CryptContext

from models.User import User
from models.UserSettings import UserSettings
from models.UserFilter import UserFilter
from models.UserQAMetricsFilter import UserQAMetricsFilter
from models.UserPredictionsTable import UserPredictionsTable

from serializers import UserSerializer

from settings.settings import init_filters, init_predictions_table


async def create_user(user: UserSerializer) -> None:
"""Creates a new User with default settings.
:param user: New user.
"""
user = user.dict()
user["password"] = CryptContext(schemes=["sha256_crypt"]).hash(
user["password"]
)
user["email"] = user["email"].lower().strip()
user["name"] = user["name"].strip()
user = await User(**user).create()
user_settings = await UserSettings(user_id=user.id).create()

await init_filters(UserFilter, user_settings.id)
await init_filters(UserQAMetricsFilter, user_settings.id)
await init_predictions_table(UserPredictionsTable, user_settings.id)
59 changes: 59 additions & 0 deletions auth/authentication/sign_in.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import json
from typing import Dict, Union


from fastapi import HTTPException
from passlib.context import CryptContext

from authentication.token import create_token
from models.User import User

from serializers import UserCredentialsSerializer
from database import create_session


DEFAULT_ERROR_CODE = 500


def auth_user(
user_data: UserCredentialsSerializer,
) -> Dict[str, Union[str, int]]:
"""Authenticates User.
:param user_data: User credentials.
:return: Authenticated User with JWT.
"""
with create_session() as db:
user = (
db.query(User)
.filter(User.email == user_data.credentials.lower().strip())
.first()
)
if not user:
user = (
db.query(User)
.filter(
User.name == user_data.credentials.strip(),
)
.first()
)
if not user:
raise HTTPException(
status_code=DEFAULT_ERROR_CODE,
detail="Incorrect username or password.",
)
if not CryptContext(schemes=["sha256_crypt"]).verify(
user_data.password, user.password
):
raise HTTPException(
status_code=DEFAULT_ERROR_CODE,
detail="Incorrect username or password.",
)

response = {
"id": str(user.id),
"name": user.name,
"email": user.email,
"token": create_token(user),
}
return response
52 changes: 52 additions & 0 deletions auth/authentication/token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import os

import jwt
from datetime import datetime, timedelta
from fastapi import HTTPException

from database import create_session
from models.User import User

ACCESS_TOKEN_EXPIRE_WEEKS = 15

SECRET_KEY_DEFAULT = "v$1bx=+6#ibt4a$4i&5i8stwjqzm+3=tjsde9iku1a0w(u6bfy"
SECRET_KEY = os.environ.get("SECRET_KEY", default=SECRET_KEY_DEFAULT)


def create_token(user: User) -> str:
"""Generates JSON Web Token.
:param user: User to be authenticated.
:return: JSON Web Token.
"""
raw_token = {
"exp": datetime.utcnow() + timedelta(weeks=ACCESS_TOKEN_EXPIRE_WEEKS),
"id": str(user.id),
}
token = jwt.encode(raw_token, key=SECRET_KEY).decode("UTF-8")
return token


def decode_jwt(token: str) -> str:
"""Checks JSON Web Token and return user id.
:param token: JSON Web Token.
:return: User id.
"""
# To avoid a circular dependency
from authentication.sign_in import DEFAULT_ERROR_CODE

try:
decoded_token = jwt.decode(jwt=token, key=SECRET_KEY)
except jwt.DecodeError:
raise HTTPException(status_code=DEFAULT_ERROR_CODE, detail="Token is invalid")
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=DEFAULT_ERROR_CODE, detail="Token expired")
with create_session() as db:
user = db.query(User).filter(User.id == decoded_token.get("id")).first()

if not user:
raise HTTPException(
status_code=DEFAULT_ERROR_CODE, detail="User not found",
)
return str(user.id)
32 changes: 32 additions & 0 deletions auth/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import os
from contextlib import contextmanager

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session


DB_NAME = os.environ.get("POSTGRES_DB", "users")
DB_USER = os.environ.get("POSTGRES_USER", "postgres_user")
DB_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "postgres_password")
DB_HOST = os.environ.get("POSTGRES_HOST", "localhost")
DB_PORT = os.environ.get("POSTGRES_PORT", "5432")
DB_URL = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"

engine = create_engine(DB_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()


@contextmanager
def create_session() -> Session:
"""Create database session.
:return: Database session.
"""
session = SessionLocal()
try:
yield session
finally:
session.close()
69 changes: 69 additions & 0 deletions auth/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError, HTTPException
from fastapi.responses import JSONResponse
from starlette.requests import Request
from starlette.responses import Response

from authentication.token import decode_jwt
from authentication.sign_in import auth_user
from authentication.register import create_user

from database import engine, Base

from serializers import (
UserSerializer,
UserCredentialsSerializer,
AuthResponseSerializer,
VerifyTokenResponse,
)


TABLES_TO_CREATE = (
Base.metadata.tables["users"],
Base.metadata.tables["user_settings"],
Base.metadata.tables["user_filter"],
Base.metadata.tables["user_qa_metrics_filter"],
Base.metadata.tables["user_predictions_table"],
)

Base.metadata.create_all(bind=engine, tables=TABLES_TO_CREATE)

app = FastAPI()


@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
return JSONResponse(
content={"exception": {"detail": exc.detail}}, status_code=exc.status_code,
)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
exception_detail = [
{"name": error.get("loc")[1], "errors": [error.get("msg")]}
for error in exc.errors()
]

return JSONResponse(
status_code=400, content=jsonable_encoder({"exception": exception_detail}),
)


@app.post("/sign_in/", response_model=AuthResponseSerializer)
def sign_in(user: UserCredentialsSerializer):
user = auth_user(user)
return JSONResponse(user)


@app.post("/register/")
async def register(user: UserSerializer):
await create_user(user)
return Response(status_code=200)


@app.get("/verify_token/", response_model=VerifyTokenResponse)
def verify_token(token: str):
response = {"id": decode_jwt(token)}
return JSONResponse(response)
37 changes: 37 additions & 0 deletions auth/models/User.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from sqlalchemy import Column, String, Integer
from sqlalchemy.orm import relationship

from database import Base, create_session


class User(Base):
__tablename__ = "users"

id = Column(
Integer, primary_key=True, index=True, unique=True, autoincrement=True
)
email = Column(String, unique=True)
name = Column(
String,
unique=True,
)
password = Column(String)

user_settings = relationship(
"UserSettings",
uselist=False,
back_populates="user",
cascade="all, delete, delete-orphan",
)

def __repr__(self):
return (
f"<User(id='{self.id}', email='{self.email}', name='{self.name}')>"
)

async def create(self):
with create_session() as db:
db.add(self)
db.commit()
db.refresh(self)
return self
32 changes: 32 additions & 0 deletions auth/models/UserFilter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from sqlalchemy import Column, ForeignKey, String, Integer
from sqlalchemy.orm import relationship

from database import Base, create_session


class UserFilter(Base):
__tablename__ = "user_filter"

id = Column(
Integer, primary_key=True, index=True, unique=True, autoincrement=True
)
settings_id = Column(
Integer, ForeignKey("user_settings.id", ondelete="CASCADE")
)
name = Column(String)
type = Column(String)

user_settings = relationship("UserSettings", back_populates="user_filter")

def __repr__(self):
return (
f"<UserFilter(id='{self.id}', name='{self.name}', "
f"type='{self.type}', settings_id='{self.settings_id}')>"
)

async def create(self):
with create_session() as db:
db.add(self)
db.commit()
db.refresh(self)
return self
36 changes: 36 additions & 0 deletions auth/models/UserPredictionsTable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean
from sqlalchemy.orm import relationship

from database import Base, create_session


class UserPredictionsTable(Base):
__tablename__ = "user_predictions_table"

id = Column(
Integer, primary_key=True, index=True, unique=True, autoincrement=True
)
settings_id = Column(
Integer, ForeignKey("user_settings.id", ondelete="CASCADE")
)
name = Column(String)
is_default = Column(Boolean)
position = Column(Integer)

user_settings = relationship(
"UserSettings", back_populates="user_predictions_table"
)

def __repr__(self):
return (
f"<UserPredictionsTable(id='{self.id}', name='{self.name}', "
f"is_default='{self.is_default}', position='{self.position}', "
f"settings_id='{self.settings_id}')>"
)

async def create(self):
with create_session() as db:
db.add(self)
db.commit()
db.refresh(self)
return self
Loading

0 comments on commit 740acff

Please sign in to comment.