├── app ├── core │ ├── __init__.py │ ├── domain │ │ ├── __init__.py │ │ └── user.py │ ├── ports │ │ ├── __init__.py │ │ └── user.py │ └── usecase │ │ ├── __init__.py │ │ └── user.py ├── modules │ ├── __init__.py │ └── users │ │ ├── services │ │ ├── __init__.py │ │ └── github_api.py │ │ ├── controllers │ │ ├── __init__.py │ │ ├── list_user_by_email.py │ │ ├── list_user_by_username.py │ │ ├── list_all_users.py │ │ ├── list_user_by_name.py │ │ ├── get_user_profile.py │ │ ├── save_user.py │ │ ├── register_with_github.py │ │ └── update_user.py │ │ └── __init__.py ├── testing │ ├── __init__.py │ └── core │ │ ├── __init__.py │ │ ├── domain │ │ ├── __init__.py │ │ └── user_test.py │ │ └── usecase │ │ ├── __init__.py │ │ └── user_usercase_test.py ├── database │ ├── __init__.py │ ├── config │ │ ├── __init__.py │ │ ├── db_base.py │ │ ├── init_database.py │ │ └── db_connection.py │ ├── tables │ │ ├── __init__.py │ │ └── user.py │ └── repositories │ │ ├── __init__.py │ │ └── user_repository.py └── __init__.py ├── .env.example ├── config.py ├── requirements.txt ├── Dockerfile ├── main.py ├── .gitignore └── readme.md /app/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/testing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/core/domain/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/core/ports/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/core/usecase/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/testing/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/database/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/database/tables/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/database/repositories/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/modules/users/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/testing/core/domain/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/testing/core/usecase/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/modules/users/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_API_TOKEN= 2 | GITHU_URL=https://api.github.com/graphql 3 | APP_PORT=8081 -------------------------------------------------------------------------------- /app/database/config/db_base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative import declarative_base 2 | 3 | Base = declarative_base() 4 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Statement for enabling the development environment 4 | DEBUG = True 5 | 6 | # Define the application directory 7 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 8 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from app.modules.users import register_user_routes 3 | 4 | 5 | def register_routes(app: Flask): 6 | app.add_url_rule( 7 | "/", view_func=lambda: {'message': 'application is working perfectly'}) 8 | register_user_routes(app) 9 | -------------------------------------------------------------------------------- /app/database/config/init_database.py: -------------------------------------------------------------------------------- 1 | from .db_connection import DBConnection 2 | from .db_base import Base 3 | 4 | 5 | def init_database(): 6 | # import all tables 7 | import app.database.tables 8 | with DBConnection() as connection: 9 | Base.metadata.create_all(bind=connection.get_engine()) 10 | connection.session.close() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | autopep8==1.5.7 2 | certifi==2021.5.30 3 | chardet==4.0.0 4 | click==8.0.1 5 | Flask==2.0.1 6 | greenlet==1.1.0 7 | idna==2.10 8 | itsdangerous==2.0.1 9 | Jinja2==3.0.1 10 | MarkupSafe==2.0.1 11 | pycodestyle==2.7.0 12 | python-dotenv==0.18.0 13 | requests==2.25.1 14 | SQLAlchemy==1.4.20 15 | toml==0.10.2 16 | urllib3==1.26.6 17 | Werkzeug==2.0.1 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim-buster 2 | 3 | RUN apt-get update -y && \ 4 | apt-get install -y python-pip python-dev 5 | 6 | # We copy just the requirements.txt first to leverage Docker cache 7 | COPY ./requirements.txt /app/requirements.txt 8 | 9 | WORKDIR /app 10 | 11 | RUN pip install -r requirements.txt 12 | 13 | COPY . /app 14 | 15 | ENTRYPOINT [ "python" ] 16 | 17 | CMD [ "main.py" ] -------------------------------------------------------------------------------- /app/modules/users/controllers/list_user_by_email.py: -------------------------------------------------------------------------------- 1 | from flask.json import jsonify 2 | 3 | from app.database.repositories.user_repository import UserRepository 4 | from app.core.usecase.user import LoadUserByEmailUsecase 5 | 6 | 7 | def list_with_email(user_email: str): 8 | usecase = LoadUserByEmailUsecase(load_user_port=UserRepository()) 9 | try: 10 | result, status_code = usecase.execute(user_email=user_email) 11 | return jsonify(result), status_code 12 | except: 13 | return jsonify({'message': 'Internal server error'}), 500 14 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | from app.database.config.init_database import init_database 4 | from flask import Flask 5 | 6 | from app import register_routes 7 | 8 | app = Flask(__name__) 9 | app.config["DEBUG"] = True 10 | 11 | load_dotenv() 12 | 13 | 14 | @app.errorhandler(404) 15 | def page_not_found(e): 16 | return {'message': 'Route not found'}, 404 17 | 18 | 19 | register_routes(app) 20 | init_database() 21 | 22 | if __name__ == '__main__': 23 | app_port = os.getenv('APP_PORT',8081) 24 | app.run(host='0.0.0.0', port=app_port) 25 | -------------------------------------------------------------------------------- /app/modules/users/controllers/list_user_by_username.py: -------------------------------------------------------------------------------- 1 | from flask.json import jsonify 2 | 3 | from app.database.repositories.user_repository import UserRepository 4 | from app.core.usecase.user import LoadUserByUsernameUsecase 5 | 6 | 7 | def list_user_by_username(user_username: str): 8 | 9 | usecase = LoadUserByUsernameUsecase(load_user_port=UserRepository()) 10 | try: 11 | result, status_code = usecase.execute(user_username=user_username,) 12 | return jsonify(result), status_code 13 | except: 14 | return jsonify({'message': 'Internal server error'}), 500 15 | -------------------------------------------------------------------------------- /app/modules/users/controllers/list_all_users.py: -------------------------------------------------------------------------------- 1 | from flask.json import jsonify 2 | from flask import request 3 | 4 | from app.database.repositories.user_repository import UserRepository 5 | from app.core.usecase.user import LoadUsersUsecase 6 | 7 | 8 | def list_all_users(): 9 | limit = request.args.get('limit', None) 10 | offset = request.args.get('offset', None) 11 | usecase = LoadUsersUsecase(load_user_port=UserRepository()) 12 | 13 | try: 14 | result, status_code = usecase.execute(limit=limit, offset=offset) 15 | return jsonify(result), status_code 16 | except: 17 | return jsonify({'message': 'Internal server error'}), 500 18 | -------------------------------------------------------------------------------- /app/modules/users/controllers/list_user_by_name.py: -------------------------------------------------------------------------------- 1 | from flask.json import jsonify 2 | from flask import request 3 | 4 | from app.database.repositories.user_repository import UserRepository 5 | from app.core.usecase.user import LoadUserByLikeNameUsecase 6 | 7 | 8 | def list_user_by_name(like_name: str): 9 | limit = request.args.get('limit', None) 10 | offset = request.args.get('offset', None) 11 | 12 | usecase = LoadUserByLikeNameUsecase( 13 | load_user_port=UserRepository() 14 | ) 15 | 16 | try: 17 | result, status_code = usecase.execute( 18 | like_name, limit=limit, offset=offset) 19 | return jsonify(result), status_code 20 | except: 21 | return jsonify({'message': 'Internal server error'}), 500 22 | -------------------------------------------------------------------------------- /app/modules/users/controllers/get_user_profile.py: -------------------------------------------------------------------------------- 1 | from flask.json import jsonify 2 | 3 | from app.modules.users.services.github_api import LoadGithubInformations 4 | from app.database.repositories.user_repository import UserRepository 5 | from app.core.usecase.user import LoadUserProfileUsecase 6 | 7 | 8 | def get_profile_user(user_id: int): 9 | 10 | usecase = LoadUserProfileUsecase(load_user_port=UserRepository(), 11 | get_user_metrics_port=LoadGithubInformations() 12 | ) 13 | 14 | try: 15 | result, status_code = usecase.execute(user_id=user_id) 16 | return jsonify(result), status_code 17 | except: 18 | return jsonify({'message': 'Internal server error'}), 500 19 | -------------------------------------------------------------------------------- /app/database/config/db_connection.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.engine.base import Engine 2 | from sqlalchemy.orm import sessionmaker 3 | from sqlalchemy.orm.scoping import scoped_session 4 | from sqlalchemy import create_engine 5 | 6 | 7 | class DBConnection: 8 | _connection_url: str 9 | 10 | def get_engine(self) -> Engine: 11 | return create_engine(self._connection_url) 12 | 13 | def __init__(self) -> None: 14 | self._connection_url = 'sqlite:///./teste.db' 15 | 16 | def __enter__(self): 17 | session = scoped_session( 18 | sessionmaker(autocommit=False, bind=self.get_engine()) 19 | ) 20 | self.session = session 21 | return self 22 | 23 | def __exit__(self, *args, **kwargs) -> bool: 24 | if self.session is not None: 25 | self.session.close() 26 | return False 27 | -------------------------------------------------------------------------------- /app/modules/users/controllers/save_user.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | from flask import request, jsonify 3 | 4 | from app.database.repositories.user_repository import UserRepository 5 | from app.core.usecase.user import SaveUserDTO, SaveUserUsecase 6 | 7 | 8 | def save_new_user() -> Tuple[dict, int]: 9 | 10 | data = request.get_json() 11 | 12 | user_data = SaveUserDTO( 13 | email=data['email'], name=data['name'], username=data['username'] 14 | ) 15 | 16 | try: 17 | user_data.validate() 18 | except Exception as e: 19 | return jsonify({'message': e}), 404 20 | 21 | user_repository = UserRepository() 22 | 23 | usecase = SaveUserUsecase( 24 | save_user_interface_port=user_repository, load_user_port=user_repository) 25 | 26 | try: 27 | result, status_code = usecase.execute(userToSave=user_data) 28 | return jsonify(result), status_code 29 | except: 30 | return jsonify({'message': 'Internal server error'}), 500 31 | -------------------------------------------------------------------------------- /app/modules/users/controllers/register_with_github.py: -------------------------------------------------------------------------------- 1 | from flask import request, jsonify 2 | from typing import Tuple 3 | 4 | from app.core.usecase.user import SaveUserWithGithubUsernameUsecase 5 | from app.database.repositories.user_repository import UserRepository 6 | from app.modules.users.services.github_api import LoadGithubInformations 7 | 8 | 9 | def register_with_github() -> Tuple[dict, int]: 10 | 11 | data = request.get_json() 12 | 13 | if not 'username' in data: 14 | return {'message': 'Missing required field: username'}, 400 15 | 16 | try: 17 | user_repository = UserRepository() 18 | github_provider = LoadGithubInformations() 19 | usecase = SaveUserWithGithubUsernameUsecase( 20 | load_user_port=user_repository, 21 | save_user_interface_port=user_repository, 22 | get_github_informations_port=github_provider, 23 | get_users_from_github_by_username_port=github_provider 24 | ) 25 | 26 | response, status_code = usecase.execute(data['username']) 27 | 28 | data = jsonify(response) 29 | 30 | return data, status_code 31 | except: 32 | return jsonify({'message': 'Internal server error'}), 500 33 | -------------------------------------------------------------------------------- /app/database/tables/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, Enum 2 | 3 | from app.core.domain.user import User, GenderEnum 4 | from app.database.config.db_base import Base 5 | 6 | 7 | class UserModel(Base, User): 8 | __tablename__ = "users" 9 | 10 | id = Column('ID', Integer, primary_key=True, 11 | autoincrement=True, nullable=True) 12 | username = Column(String(30), unique=True) 13 | name = Column(String(45), nullable=False) 14 | last_name = Column(String(45), nullable=True) 15 | email = Column(String, unique=True) 16 | profile_image_url = Column(String(100), nullable=True) 17 | bio = Column(String(30), nullable=True) 18 | gender = Column(Enum(GenderEnum), default=GenderEnum.NOTSPECIFIED) 19 | 20 | def __repr__(self) -> str: 21 | return '' % (self.id, self.name) 22 | 23 | def to_core_model(self) -> User: 24 | return User( 25 | id=self.id, 26 | username=self.username, 27 | name=self.name, 28 | last_name=self.last_name, 29 | email=self.email, 30 | profile_image_url=self.profile_image_url, 31 | bio=self.bio, 32 | gender=self.gender, 33 | ) 34 | -------------------------------------------------------------------------------- /app/modules/users/controllers/update_user.py: -------------------------------------------------------------------------------- 1 | from flask.json import jsonify, request 2 | 3 | from app.core.domain.user import GenderEnum, User 4 | from app.database.repositories.user_repository import UserRepository 5 | from app.core.usecase.user import UpdateUserUsecase 6 | 7 | 8 | def update_user(user_id: int): 9 | user_repository = UserRepository() 10 | 11 | usecase = UpdateUserUsecase( 12 | load_user_port=user_repository, 13 | update_user_port=user_repository 14 | ) 15 | request_data: dict = request.get_json() 16 | 17 | user_data = User( 18 | id=user_id, 19 | bio=request_data.get('bio', ''), 20 | name=request_data.get('name', ''), 21 | email=request_data.get('email', ''), 22 | username=request_data.get('username', ''), 23 | gender=request_data.get('gender', GenderEnum.NOTSPECIFIED), 24 | last_name=request_data.get('last_name'), 25 | profile_image_url=request_data.get('profile_image_url', '') 26 | ) 27 | 28 | try: 29 | result, status_code = usecase.execute( 30 | user_id=user_id, 31 | user=user_data 32 | ) 33 | return jsonify(result), status_code 34 | except: 35 | return jsonify({'message': 'Internal server error'}), 500 36 | -------------------------------------------------------------------------------- /app/modules/users/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from .controllers.save_user import save_new_user 4 | from .controllers.update_user import update_user 5 | from .controllers.get_user_profile import get_profile_user 6 | from .controllers.list_user_by_email import list_with_email 7 | from .controllers.list_user_by_username import list_user_by_username 8 | from .controllers.list_user_by_name import list_user_by_name 9 | from .controllers.list_all_users import list_all_users 10 | from .controllers.register_with_github import register_with_github 11 | 12 | 13 | def register_user_routes(app: Flask): 14 | # inser user 15 | app.add_url_rule('/users', view_func=save_new_user, methods=['POST']) 16 | # update user route 17 | app.add_url_rule('/users/', view_func=update_user, methods=['PUT']) 18 | # inser user by github 19 | app.add_url_rule( 20 | '/users/register/github', view_func=register_with_github, methods=['POST']) 21 | 22 | # list user by user email 23 | app.add_url_rule('/users/email/', 24 | view_func=list_with_email, methods=['GET']) 25 | 26 | # list user by username 27 | app.add_url_rule('/users/username/', 28 | view_func=list_user_by_username, methods=['GET']) 29 | 30 | # list all users 31 | app.add_url_rule('/users', view_func=list_all_users, methods=['GET']) 32 | 33 | # load user profile 34 | app.add_url_rule('/users//profile', 35 | view_func=get_profile_user, methods=['GET']) 36 | 37 | # search user by name 38 | app.add_url_rule('/users/search/', 39 | view_func=list_user_by_name, methods=['GET']) 40 | -------------------------------------------------------------------------------- /app/core/ports/user.py: -------------------------------------------------------------------------------- 1 | from abc import abstractclassmethod 2 | 3 | from app.core.domain.user import GithubUser, GithubUserInformations, User, SaveUserDTO 4 | from typing import List, Optional, Union 5 | 6 | 7 | class LoadUserPort: 8 | @abstractclassmethod 9 | def load_user_by_username(self, username: str) -> Optional[User]: 10 | raise Exception('Not implemented method') 11 | 12 | @abstractclassmethod 13 | def load_user_by_email(self, email: str) -> Optional[User]: 14 | raise Exception('Not implemented method') 15 | 16 | @abstractclassmethod 17 | def load_user_by_id(self, id: int) -> Optional[User]: 18 | raise Exception('Not implemented method') 19 | 20 | @abstractclassmethod 21 | def load_users(self, limit=None, offset=None) -> List[User]: 22 | raise Exception('Not implemented method') 23 | 24 | @abstractclassmethod 25 | def load_user_by_like_name(self, like_name: str, limit: int = None, offset: int = None) -> List[User]: 26 | raise Exception('Not implemented method') 27 | 28 | 29 | class UpdateUserPort: 30 | @abstractclassmethod 31 | def update_user(self, user_id: int, user: User) -> User: 32 | raise Exception('Not implemented method update_user on UpdateUserPort') 33 | 34 | 35 | class SaveUserPort: 36 | @abstractclassmethod 37 | def save(self, user: Union[SaveUserDTO, User]) -> User: 38 | raise Exception('Not implemented method save on SaveNewUserPort') 39 | 40 | 41 | class GetUserInformationsFromGithubInterfacePort: 42 | @abstractclassmethod 43 | def load_github_informations(self, username: str) -> Optional[GithubUser]: 44 | raise Exception('Not implemented method : load_github_informations') 45 | 46 | 47 | class GetUserMetricsGithubInterfacePort: 48 | @abstractclassmethod 49 | def load_user_metrics(self, user_login: str) -> Optional[GithubUserInformations]: 50 | raise Exception('Not implemented method : load_user_metrics') 51 | 52 | 53 | class GetUsersInformationsFromGithubWithUsername: 54 | @abstractclassmethod 55 | def load_users_by_username(self, username: str, limit: int = 50) -> List[GithubUser]: 56 | raise Exception('Not implemented method : load_users_by_username') 57 | -------------------------------------------------------------------------------- /app/core/domain/user.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | 5 | class GenderEnum(str, Enum): 6 | MALE: str = "Male" 7 | FEMALE: str = "Female" 8 | NOTSPECIFIED: str = "Not specified" 9 | 10 | 11 | @dataclass 12 | class User: 13 | def __init__(self, 14 | username: str, 15 | name: str, 16 | email: str, 17 | profile_image_url: str = '', 18 | last_name: str = '', 19 | bio: str = '', 20 | gender: str = GenderEnum.NOTSPECIFIED, 21 | id: int = None, 22 | ) -> None: 23 | self.id = id 24 | self.username = username 25 | self.name = name 26 | self.email = email 27 | self.profile_image_url = profile_image_url 28 | self.last_name = last_name 29 | self.bio = bio 30 | self.gender = gender 31 | 32 | def __repr__(self) -> str: 33 | return '' % (self.name) 34 | 35 | 36 | @dataclass 37 | class SaveUserDTO: 38 | def __init__(self, username: str, name: str, email: str) -> None: 39 | self.name = name 40 | self.username = username 41 | self.email = email 42 | 43 | def validate(self) -> None: 44 | required_fields = ['username', 'name', 'email'] 45 | 46 | for field in required_fields: 47 | if not hasattr(self, field): 48 | raise 'Missing required field: %s' % (field) 49 | 50 | 51 | @dataclass 52 | class GithubUser: 53 | def __init__(self, 54 | name: str, 55 | profileImageUrl: str, 56 | email: str, 57 | bio: str, 58 | login: str, 59 | gender: str, 60 | ) -> None: 61 | self.name = name 62 | self.profileImageUrl = profileImageUrl 63 | self.email = email 64 | self.bio = bio 65 | self.login = login 66 | self.gender = gender 67 | 68 | def __repr__(self) -> str: 69 | return '' % (self.name) 70 | 71 | 72 | @dataclass 73 | class GithubUserInformations: 74 | def __init__(self, 75 | total_followers: int, 76 | total_public_repositories: int, 77 | total_following: int, 78 | profile_url: int 79 | ) -> None: 80 | self.total_followers = total_followers 81 | self.total_public_repositories = total_public_repositories 82 | self.total_following = total_following 83 | self.profile_url = profile_url 84 | -------------------------------------------------------------------------------- /app/testing/core/domain/user_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from app.core.domain.user import User, GithubUser, GithubUserInformations, SaveUserDTO 4 | 5 | 6 | class TestUserModel(TestCase): 7 | def test_user_is_intances_with_success(self): 8 | user = User( 9 | id=1, 10 | bio="fake bio", 11 | email="any@email.com", 12 | last_name="Silver", 13 | name="John", 14 | username="johnsv" 15 | ) 16 | self.assertEqual(user.id, 1) 17 | self.assertEqual(user.bio, "fake bio") 18 | self.assertEqual(user.email, "any@email.com") 19 | self.assertEqual(user.last_name, "Silver") 20 | self.assertEqual(user.name, "John") 21 | self.assertEqual(user.username, "johnsv") 22 | 23 | 24 | class TesteGithubUser(TestCase): 25 | def teste_github_istance_correct(self) -> None: 26 | github_user = GithubUser( 27 | name="fake name", 28 | bio="fake bio", 29 | email="any123@email.com", 30 | gender="", 31 | login="marlete", 32 | profileImageUrl="http://fakepath.com" 33 | ) 34 | 35 | self.assertEqual(github_user.name, "fake name") 36 | self.assertEqual(github_user.bio, "fake bio") 37 | self.assertEqual(github_user.email, "any123@email.com") 38 | self.assertEqual(github_user.gender, "") 39 | self.assertEqual(github_user.login, "marlete") 40 | self.assertEqual(github_user.profileImageUrl, "http://fakepath.com") 41 | 42 | 43 | class TesteGithuUserMetrics(TestCase): 44 | def teste_github_metrics_instace_correct(self) -> None: 45 | github_metrics = GithubUserInformations( 46 | profile_url="http://github.com/fake", 47 | total_followers=10, 48 | total_following=20, 49 | total_public_repositories=50 50 | ) 51 | self.assertEqual(github_metrics.profile_url, "http://github.com/fake") 52 | self.assertEqual(github_metrics.total_followers, 10) 53 | self.assertEqual(github_metrics.total_following, 20) 54 | self.assertEqual(github_metrics.total_public_repositories, 50) 55 | 56 | 57 | class TesteUserDTO(TestCase): 58 | def test_instance_is_correct(self) -> None: 59 | user_dto = SaveUserDTO( 60 | email="fake@email.com", 61 | name="fakename", 62 | username="anylogin" 63 | ) 64 | self.assertEqual(user_dto.email, "fake@email.com") 65 | self.assertEqual(user_dto.name, "fakename") 66 | self.assertEqual(user_dto.username, "anylogin") 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | .vscode 141 | *.db 142 | -------------------------------------------------------------------------------- /app/database/repositories/user_repository.py: -------------------------------------------------------------------------------- 1 | from app.database.config.db_connection import DBConnection 2 | from typing import List, Optional, Union 3 | 4 | from app.database.tables.user import UserModel 5 | from app.core.domain.user import SaveUserDTO, User 6 | from app.core.ports.user import SaveUserPort, LoadUserPort, UpdateUserPort 7 | 8 | 9 | class UserRepository(LoadUserPort, SaveUserPort, UpdateUserPort): 10 | def save(self, user: Union[SaveUserDTO, User]) -> User: 11 | user_params: User = None 12 | 13 | if isinstance(user, SaveUserDTO): 14 | user_params = User( 15 | name=user.name, 16 | username=user.username, 17 | email=user.email 18 | ) 19 | else: 20 | user_params = user 21 | 22 | with DBConnection() as connection: 23 | try: 24 | user_to_save = UserModel( 25 | username=user_params.username, 26 | name=user_params.name, 27 | last_name=user_params.last_name, 28 | email=user_params.email, 29 | profile_image_url=user_params.profile_image_url, 30 | bio=user_params.bio, 31 | gender=user_params.gender, 32 | ) 33 | 34 | connection.session.add(user_to_save) 35 | connection.session.commit() 36 | connection.session.flush() 37 | 38 | return user_to_save.to_core_model() 39 | 40 | except: 41 | connection.session.rollback() 42 | raise Exception('Error on save user') 43 | finally: 44 | connection.session.close() 45 | 46 | def load_user_by_email(self, email: str) -> Optional[User]: 47 | with DBConnection() as connection: 48 | try: 49 | user = connection.session.query( 50 | UserModel).filter_by(email=email).first() 51 | if user is not None: 52 | return user.to_core_model() 53 | return None 54 | except: 55 | return None 56 | finally: 57 | connection.session.close() 58 | 59 | def load_user_by_id(self, id: int) -> Optional[User]: 60 | with DBConnection() as connection: 61 | try: 62 | user = connection.session.query( 63 | UserModel).filter_by(id=id).first() 64 | if user is not None: 65 | return user.to_core_model() 66 | return None 67 | except: 68 | return None 69 | finally: 70 | connection.session.close() 71 | 72 | def load_user_by_username(self, username: str) -> Optional[User]: 73 | with DBConnection() as connection: 74 | try: 75 | user = connection.session.query( 76 | UserModel).filter_by(username=username).first() 77 | if user is not None: 78 | return user.to_core_model() 79 | return None 80 | except: 81 | return None 82 | finally: 83 | connection.session.close() 84 | 85 | def load_users(self, limit, offset) -> List[User]: 86 | with DBConnection() as connection: 87 | try: 88 | return connection.session.query(UserModel).limit(limit=limit).offset(offset=offset).all() 89 | except: 90 | return [] 91 | finally: 92 | connection.session.close() 93 | 94 | def load_user_by_like_name(self, like_name: str, limit: int = None, offset: int = None) -> List[User]: 95 | with DBConnection() as connection: 96 | try: 97 | return connection.session.query(UserModel).filter(UserModel.name.ilike( 98 | "%s%%" % (like_name) 99 | )).limit(limit=limit).offset(offset=offset).all() 100 | except: 101 | return [] 102 | finally: 103 | connection.session.close() 104 | 105 | def update_user(self, user_id: int, user: User) -> User: 106 | with DBConnection() as connection: 107 | try: 108 | user_in_database = connection.session.query( 109 | UserModel).filter_by(id=user_id).first() 110 | 111 | user_in_database.gender = user.gender 112 | user_in_database.bio = user.bio 113 | user_in_database.profile_image_url = user.profile_image_url 114 | user_in_database.name = user.name 115 | 116 | connection.session.commit() 117 | return user_in_database.to_core_model() 118 | except: 119 | return None 120 | finally: 121 | connection.session.close() 122 | -------------------------------------------------------------------------------- /app/modules/users/services/github_api.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | import requests 3 | import os 4 | 5 | 6 | from app.core.ports.user import GetUserInformationsFromGithubInterfacePort, GetUserMetricsGithubInterfacePort, GetUsersInformationsFromGithubWithUsername 7 | from app.core.domain.user import GenderEnum, GithubUser, GithubUserInformations 8 | 9 | 10 | class LoadGithubInformations(GetUserInformationsFromGithubInterfacePort, 11 | GetUserMetricsGithubInterfacePort, 12 | GetUsersInformationsFromGithubWithUsername 13 | ): 14 | def __init__(self) -> None: 15 | self._base_url = os.environ.get('GITHU_URL', '') 16 | 17 | def _get_headers(self) -> dict: 18 | token = os.environ.get('GITHUB_API_TOKEN', '') 19 | return { 20 | 'Content-Type': 'application/json', 21 | 'Authorization': 'bearer %s' % (token) 22 | } 23 | 24 | def _build_graphql_body(self, graphql_query: str, variables: dict) -> dict: 25 | return {'query': graphql_query, 'variables': variables} 26 | 27 | def load_github_informations(self, username: str) -> Optional[GithubUser]: 28 | headers = self._get_headers() 29 | 30 | param_query: str = ( 31 | """ 32 | query getUser($login: String!){ 33 | user(login:$login){ 34 | name 35 | avatarUrl 36 | email 37 | login 38 | bio 39 | } 40 | } 41 | """ 42 | ) 43 | 44 | body = self._build_graphql_body( 45 | graphql_query=param_query, variables={'login': username}) 46 | 47 | response = requests.post(self._base_url, json=body, headers=headers) 48 | 49 | response_data = response.json() 50 | 51 | user: Optional[GithubUser] = None 52 | 53 | if response.status_code == 200 and 'data' in response_data and 'user' in response_data['data'] and response_data['data']['user'] is not None: 54 | json_user: dict = response.json()['data']['user'] 55 | user = GithubUser( 56 | name=json_user['name'], profileImageUrl=json_user['avatarUrl'], bio=json_user[ 57 | 'bio'], email=json_user['email'], login=json_user['login'], gender=json_user.get('gender', GenderEnum.NOTSPECIFIED) 58 | ) 59 | 60 | return user 61 | 62 | def load_user_metrics(self, username: str) -> Optional[GithubUserInformations]: 63 | query: str = ( 64 | """ 65 | query getDetailProfile($login:String!){ 66 | user(login:$login){ 67 | url 68 | repositories { totalCount } 69 | followers { totalCount } 70 | following { totalCount } 71 | } 72 | } 73 | """ 74 | ) 75 | body = self._build_graphql_body( 76 | graphql_query=query, variables={'login': username}) 77 | headers = self._get_headers() 78 | 79 | response = requests.post(self._base_url, 80 | json=body, headers=headers) 81 | response_data = response.json() 82 | 83 | user: GithubUserInformations = None 84 | if response.status_code == 200 and 'data' in response_data and 'user' in response_data['data']: 85 | json_user: dict = response.json()['data']['user'] 86 | total_followers = json_user['followers']['totalCount'] 87 | total_following = json_user['following']['totalCount'] 88 | total_public_repositories = json_user['repositories']['totalCount'] 89 | profile_url = json_user['url'] 90 | 91 | user = GithubUserInformations( 92 | profile_url=profile_url, 93 | total_followers=total_followers, 94 | total_following=total_following, 95 | total_public_repositories=total_public_repositories, 96 | ) 97 | 98 | return user 99 | 100 | def load_users_by_username(self, username: str, limit: int = 50) -> List[GithubUser]: 101 | query = ( 102 | """ 103 | query getUsersByLogin($login : String!, $limit: Int!){ 104 | search(type:USER,first:$limit,query:$login){ 105 | nodes { 106 | ... on User{ 107 | name 108 | avatarUrl 109 | email 110 | login 111 | bio 112 | } 113 | } 114 | } 115 | } 116 | """ 117 | ) 118 | body = self._build_graphql_body( 119 | query, {'login': username, 'limit': limit}) 120 | headers = self._get_headers() 121 | response = requests.post(self._base_url, 122 | json=body, headers=headers) 123 | response_json = response.json() 124 | users: List[GithubUser] = [] 125 | if response.status_code == 200 and 'data' in response_json and 'search' in response_json['data'] and 'nodes' in response_json['data']['search']: 126 | item: dict 127 | for item in response_json['data']['search']['nodes']: 128 | users.append( 129 | GithubUser( 130 | name=item.get('name', ''), 131 | email=item.get('email', ''), 132 | login=item.get('login', ''), 133 | bio=item.get('bio', ''), 134 | profileImageUrl=item.get('avatarUrl', ''), 135 | gender=item.get('gender', GenderEnum.NOTSPECIFIED) 136 | ) 137 | ) 138 | 139 | return users 140 | -------------------------------------------------------------------------------- /app/core/usecase/user.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | from app.core.ports.user import GetUserInformationsFromGithubInterfacePort, GetUserMetricsGithubInterfacePort, GetUsersInformationsFromGithubWithUsername, SaveUserPort, LoadUserPort, UpdateUserPort 4 | from app.core.domain.user import SaveUserDTO, User 5 | 6 | 7 | class SaveUserUsecase: 8 | save_user_interface_port: SaveUserPort 9 | load_user_port: LoadUserPort 10 | 11 | def __init__(self, 12 | save_user_interface_port: SaveUserPort, 13 | load_user_port: LoadUserPort 14 | ) -> None: 15 | self.load_user_port = load_user_port 16 | self.save_user_interface_port = save_user_interface_port 17 | 18 | def execute(self, userToSave: SaveUserDTO) -> Tuple[dict, int]: 19 | user = self.load_user_port.load_user_by_email(email=userToSave.email) 20 | if user is not None: 21 | return {'message': 'User with same email saved'}, 400 22 | 23 | user = self.load_user_port.load_user_by_username( 24 | username=userToSave.username) 25 | 26 | if user is not None: 27 | return {'message': 'User with same name saved'}, 400 28 | 29 | response = self.save_user_interface_port.save(userToSave) 30 | 31 | return {'user': response}, 201 32 | 33 | 34 | class SaveUserWithGithubUsernameUsecase: 35 | get_github_informations_port: GetUserInformationsFromGithubInterfacePort 36 | save_user_interface_port: SaveUserPort 37 | load_user_port: LoadUserPort 38 | get_users_from_github_by_username_port: GetUsersInformationsFromGithubWithUsername 39 | 40 | def __init__(self, 41 | save_user_interface_port: SaveUserPort, 42 | get_github_informations_port: GetUserInformationsFromGithubInterfacePort, 43 | load_user_port: LoadUserPort, 44 | get_users_from_github_by_username_port: GetUsersInformationsFromGithubWithUsername 45 | ) -> None: 46 | self.load_user_port = load_user_port 47 | self.save_user_interface_port = save_user_interface_port 48 | self.get_github_informations_port = get_github_informations_port 49 | self.get_users_from_github_by_username_port = get_users_from_github_by_username_port 50 | 51 | def execute(self, github_username: str) -> Tuple[dict, int]: 52 | 53 | user = self.load_user_port.load_user_by_username( 54 | username=github_username) 55 | 56 | if user is not None: 57 | return {'user': user}, 201 58 | 59 | github_informations = self.get_github_informations_port.load_github_informations( 60 | username=github_username 61 | ) 62 | 63 | if github_informations is None: 64 | availible_users = self.get_users_from_github_by_username_port.load_users_by_username( 65 | username=github_username, limit=50) 66 | return {'message': 'User not found on github', 'availableUsers': availible_users}, 404 67 | 68 | user = User( 69 | username=github_informations.login, 70 | name=github_informations.name, 71 | last_name='', 72 | bio=github_informations.bio, 73 | email=github_informations.email, 74 | gender=github_informations.gender, 75 | profile_image_url=github_informations.profileImageUrl) 76 | 77 | response = self.save_user_interface_port.save(user) 78 | 79 | return {'user': response}, 201 80 | 81 | 82 | class LoadUserProfileUsecase: 83 | load_user_port: LoadUserPort 84 | get_user_metrics_port: GetUserMetricsGithubInterfacePort 85 | 86 | def __init__(self, load_user_port: LoadUserPort, 87 | get_user_metrics_port: GetUserMetricsGithubInterfacePort 88 | ) -> None: 89 | self.load_user_port = load_user_port 90 | self.get_user_metrics_port = get_user_metrics_port 91 | 92 | def execute(self, user_id: int) -> Tuple[dict, int]: 93 | user: User = self.load_user_port.load_user_by_id(id=user_id) 94 | 95 | if user is None: 96 | return {'message': 'User not found'}, 404 97 | 98 | result = vars(user) 99 | 100 | if hasattr(user, 'username'): 101 | user_metrics = self.get_user_metrics_port.load_user_metrics( 102 | user_login=user.username) 103 | if user_metrics is not None: 104 | result.update(vars(user_metrics)) 105 | 106 | return {'user': result}, 200 107 | 108 | 109 | class LoadUserByEmailUsecase: 110 | load_user_port: LoadUserPort 111 | 112 | def __init__(self, load_user_port: LoadUserPort) -> None: 113 | self.load_user_port = load_user_port 114 | 115 | def execute(self, user_email: str) -> Tuple[dict, int]: 116 | user = self.load_user_port.load_user_by_email(email=user_email) 117 | 118 | if user is None: 119 | return {'message': 'User not found'}, 404 120 | 121 | return {'user': user}, 200 122 | 123 | 124 | class LoadUserByUsernameUsecase: 125 | load_user_port: LoadUserPort 126 | 127 | def __init__(self, load_user_port: LoadUserPort) -> None: 128 | self.load_user_port = load_user_port 129 | 130 | def execute(self, user_username: str) -> Tuple[dict, int]: 131 | user = self.load_user_port.load_user_by_username( 132 | username=user_username) 133 | 134 | if user is None: 135 | return {'message': 'User not found'}, 404 136 | 137 | return {'user': user}, 200 138 | 139 | 140 | class LoadUsersUsecase: 141 | load_user_port: LoadUserPort 142 | 143 | def __init__(self, load_user_port: LoadUserPort) -> None: 144 | self.load_user_port = load_user_port 145 | 146 | def execute(self, limit=0, offset=0) -> Tuple[dict, int]: 147 | users = self.load_user_port.load_users(limit=limit, offset=offset) 148 | return {'users': users}, 200 149 | 150 | 151 | class LoadUserByLikeNameUsecase: 152 | load_user_port: LoadUserPort 153 | 154 | def __init__(self, load_user_port: LoadUserPort) -> None: 155 | self.load_user_port = load_user_port 156 | 157 | def execute(self, like_name: str, limit: int = None, offset: int = None) -> Tuple[dict, int]: 158 | users = self.load_user_port.load_user_by_like_name( 159 | like_name=like_name, limit=limit, offset=offset) 160 | return {'users': users}, 200 161 | 162 | 163 | class UpdateUserUsecase: 164 | load_user_port: LoadUserPort 165 | update_user_port: UpdateUserPort 166 | 167 | def __init__(self, load_user_port: LoadUserPort, update_user_port: UpdateUserPort) -> None: 168 | self.update_user_port = update_user_port 169 | self.load_user_port = load_user_port 170 | 171 | def execute(self, user_id: int, user: User) -> Tuple[dict, int]: 172 | if not user_id: 173 | return {'message': 'Id is required'}, 400 174 | 175 | user_saved = self.load_user_port.load_user_by_id(user_id) 176 | 177 | if user_saved is None: 178 | return {'message': 'User not found'}, 400 179 | 180 | result = self.update_user_port.update_user( 181 | user=user, 182 | user_id=user_id 183 | ) 184 | 185 | return {'user': result}, 200 186 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Bem vindo ao meu teste 2 | 3 | Olá, meu nome é Hítallo William e esse é o meu projeto. Meu email pessoal é hitallo91@gmail.com 4 | 5 | ## Documentação da API 6 | 7 | Para acessar a documentação clique [aqui](https://app.swaggerhub.com/apis/hitallow/crud-users/1.0) 8 | 9 | A documentação foi feita utilizando o swagger 10 | 11 | ## Tecnologias utilizadas 12 | Para o desenvolvimento dessa aplicação foram usadas as seguintes tecnlogias: 13 | - Python 14 | - SQlite 15 | - SQLAlchemy 16 | - Flask 17 | 18 | ## Arquitetura 19 | Para o desenvolvimento foi utilizado uma arquitetura baseada no Hexagonal(ports and adapter). 20 | Utilizando a inversão de dependências para desaclopar o código principal da aplicação. 21 | No `core` da aplicação há apenas código python puro, tornando a aplicação maleável e independente de tecnologia ou framework. 22 | Os modulos são separados da seguinte maneira 23 | - domain: Entidades do core da aplicação 24 | - ports: Portas(ou adapters) cujos quais são responsáveis por conectar o código ao mundo externo, nessa pasta há apenas contratos de implementação. 25 | - usecase: Fluxo principal da aplicação, local onde há de fato a lógica de negócio da aplicação. Faz uso das portas para executar a lógica. 26 | 27 | # Testes 28 | 29 | Foi utilizado os testes providos pelo própio python, fazendo uso do pacote `unittest`. 30 | #### Para rodar os teste, execute em seu terminal 31 | ``` 32 | $ python -m unittest discover -s app/testing/ -p '*_test.py' 33 | ``` 34 | #### Cobertura de código 35 | Cobertura de código gerado com [Coverage.py](https://coverage.readthedocs.io/en/coverage-5.5/). 36 | ``` 37 | Name Stmts Miss Cover 38 | ---------------------------------------------------------------------------- 39 | app/__init__.py 5 2 60% 40 | app/core/__init__.py 0 0 100% 41 | app/core/domain/__init__.py 0 0 100% 42 | app/core/domain/user.py 54 6 89% 43 | app/core/ports/__init__.py 0 0 100% 44 | app/core/ports/user.py 39 10 74% 45 | app/core/usecase/__init__.py 0 0 100% 46 | app/core/usecase/user.py 101 4 96% 47 | app/database/__init__.py 0 0 100% 48 | app/database/config/__init__.py 0 0 100% 49 | app/database/config/db_base.py 2 0 100% 50 | app/database/config/db_connection.py 18 8 56% 51 | app/database/config/init_database.py 7 7 0% 52 | app/database/repositories/__init__.py 0 0 100% 53 | app/database/repositories/user_repository.py 79 66 16% 54 | app/database/tables/__init__.py 0 0 100% 55 | app/database/tables/user.py 17 2 88% 56 | app/modules/__init__.py 0 0 100% 57 | app/modules/users/__init__.py 18 8 56% 58 | app/modules/users/controllers/__init__.py 0 0 100% 59 | app/modules/users/controllers/get_user_profile.py 11 6 45% 60 | app/modules/users/controllers/list_all_users.py 13 8 38% 61 | app/modules/users/controllers/list_user_by_email.py 10 6 40% 62 | app/modules/users/controllers/list_user_by_name.py 13 8 38% 63 | app/modules/users/controllers/list_user_by_username.py 10 6 40% 64 | app/modules/users/controllers/register_with_github.py 18 12 33% 65 | app/modules/users/controllers/save_user.py 18 13 28% 66 | app/modules/users/controllers/update_user.py 14 9 36% 67 | app/modules/users/services/__init__.py 0 0 100% 68 | app/modules/users/services/github_api.py 50 38 24% 69 | app/testing/__init__.py 0 0 100% 70 | app/testing/core/__init__.py 0 0 100% 71 | app/testing/core/domain/__init__.py 0 0 100% 72 | app/testing/core/domain/user_test.py 33 0 100% 73 | app/testing/core/usecase/__init__.py 0 0 100% 74 | app/testing/core/usecase/user_usercase_test.py 184 0 100% 75 | ---------------------------------------------------------------------------- 76 | TOTAL 714 219 69% 77 | ``` 78 | É possível verificar a veracidade instalando o coverage com o comando: 79 | ``` 80 | pip install coverage 81 | ``` 82 | E executando os comandos, então será mostrado em seu terminal o mesmo output mostrado acima 83 | ``` 84 | $ coverage run --source app -m unittest discover -s app/testing/ -p '*_test.py' && coverage report 85 | ``` 86 | 87 | ## Executar localmente 88 | Caso tenha curiosade e queira executar o código local, você pode sequir um dos dois passos. 89 | Mas primeiro crie um arquivo `.env` com base no `.env.example` e preencha as informações, ao mudar a porta onde a aplicação será servida é preciso ter atenção na hora da execução. 90 | 91 | Bem depois de configurado você pode utilizar uma `venv` ou o docker provido por mim. 92 | 93 | ### Utilizando venv 94 | 95 | É preciso ter instalado o pip na sua máquina. Para instalar a virtualenv você pode usar este comando, caso você utilize uma versão diferente é preciso especificar. 96 | ``` 97 | $ pip install virtualenv 98 | ``` 99 | Agora você pode criar sua virtualenv com o comando a seguir. 100 | ``` 101 | $ virtualenv 102 | ``` 103 | Após criar sua virtualenv ative ela executando. 104 | ``` 105 | $ source /bin/activate 106 | ``` 107 | Utilize o arquivo `requirements.txt` para utilizar as memas dependencias que eu utilizei. Para isto rode: 108 | ``` 109 | $ pip install requirements.txt 110 | ``` 111 | Rode a aplicação executando: 112 | ``` 113 | $ python main.py 114 | ``` 115 | 116 | Após executar este comando basta acessar seu localhost na porta que você especificou no arquivo `.env`, mas caso não tenha alterado ou tenha deixado vazio a porta padrão é a porta 8081. 117 | 118 | 119 | Para desativar a virtualenv utilize: 120 | ``` 121 | $ deactivate 122 | ``` 123 | 124 | 125 | ### Utilizando docker 126 | 127 | Tenha o docker instalado na sua máquina. 128 | Builde a imagem com o seguinte comando no meso nível do arquivo Dockerfile: 129 | ``` 130 | $ docker build -t test-hitallo-backend-py:latest . 131 | ``` 132 | Para então executar o container, utilize: (altere a porta 8081 para a porta que você desejar) 133 | ``` 134 | $ docker run -d --name test-hitallo-backend-py -p 8081:8081 test-hitallo-backend-py 135 | ``` 136 | 137 | Levando em conta que nós deixamos a porta do `.env` como 8081 o código deverá funcionar como mágica ✨. 138 | 139 | Caso você tenha escolhido uma porta diferente é preciso alterar o mapeamento para onde foi especifidado. 140 | Neste caso siga este comando personalizado: 141 | ``` 142 | $ docker run -d --name test-hitallo-backend-py -p : test-hitallo-backend-py 143 | ``` 144 | 145 | 146 | ### Porque utilizar SQlite? 147 | 148 | Bem levando em conta a facilidade de lidar com o banco de dados sqlite e não precisar se configurações avançadas me pareceu mais convidativo, não é uma limitação. 149 | -------------------------------------------------------------------------------- /app/testing/core/usecase/user_usercase_test.py: -------------------------------------------------------------------------------- 1 | from app.core.domain.user import GithubUser, GithubUserInformations, SaveUserDTO, User 2 | from app.core.ports.user import GetUserInformationsFromGithubInterfacePort, GetUserMetricsGithubInterfacePort, GetUsersInformationsFromGithubWithUsername, LoadUserPort, SaveUserPort, UpdateUserPort 3 | from unittest import TestCase 4 | from unittest.mock import Mock 5 | 6 | from app.core.usecase.user import LoadUserByEmailUsecase, LoadUserByUsernameUsecase, LoadUserProfileUsecase, LoadUsersUsecase, SaveUserUsecase, SaveUserWithGithubUsernameUsecase, UpdateUserUsecase 7 | 8 | 9 | class InserNewUserTest(TestCase): 10 | def test_should_return_already_saved_email(self) -> None: 11 | mock_save_user_interface_port = SaveUserPort() 12 | mock_load_user_port = LoadUserPort() 13 | 14 | mock_load_user_port.load_user_by_email = Mock(return_value=User( 15 | id=1, 16 | bio="fake bio", 17 | email="any@email.com", 18 | last_name="Silver", 19 | name="John", 20 | username="johnsv" 21 | )) 22 | 23 | usecase = SaveUserUsecase( 24 | load_user_port=mock_load_user_port, 25 | save_user_interface_port=mock_save_user_interface_port 26 | ) 27 | 28 | result, status_code = usecase.execute( 29 | userToSave=SaveUserDTO( 30 | username='fakeusername', 31 | email="fakeemail@email.com", 32 | name="fakename" 33 | ) 34 | ) 35 | mock_load_user_port.load_user_by_email.assert_called_with( 36 | email="fakeemail@email.com" 37 | ) 38 | self.assertEqual(result['message'], 'User with same email saved') 39 | self.assertEqual(status_code, 400) 40 | 41 | def test_should_return_already_saved_user_username(self) -> None: 42 | mock_save_user_interface_port = SaveUserPort() 43 | mock_load_user_port = LoadUserPort() 44 | 45 | mock_load_user_port.load_user_by_email = Mock(return_value=None) 46 | 47 | mock_load_user_port.load_user_by_username = Mock(return_value=User( 48 | id=1, 49 | bio="fake bio", 50 | email="any@email.com", 51 | last_name="Silver", 52 | name="John", 53 | username="johnsv" 54 | )) 55 | 56 | usecase = SaveUserUsecase( 57 | load_user_port=mock_load_user_port, 58 | save_user_interface_port=mock_save_user_interface_port 59 | ) 60 | 61 | result, status_code = usecase.execute( 62 | userToSave=SaveUserDTO( 63 | username='fakeusername', 64 | email="fakeemail@email.com", 65 | name="fakename" 66 | ) 67 | ) 68 | mock_load_user_port.load_user_by_email.assert_called_with( 69 | email="fakeemail@email.com" 70 | ) 71 | mock_load_user_port.load_user_by_username.assert_called_with( 72 | username="fakeusername" 73 | ) 74 | self.assertEqual(result['message'], 'User with same name saved') 75 | self.assertEqual(status_code, 400) 76 | 77 | def test_should_save_user(self) -> None: 78 | mock_save_user_interface_port = SaveUserPort() 79 | mock_load_user_port = LoadUserPort() 80 | 81 | mock_load_user_port.load_user_by_email = Mock(return_value=None) 82 | 83 | mock_load_user_port.load_user_by_username = Mock(return_value=None) 84 | 85 | mock_save_user_interface_port.save = Mock(return_value=User( 86 | id=1, 87 | bio="fake bio", 88 | email="any@email.com", 89 | last_name="Silver", 90 | name="John", 91 | username="johnsv" 92 | )) 93 | 94 | usecase = SaveUserUsecase( 95 | load_user_port=mock_load_user_port, 96 | save_user_interface_port=mock_save_user_interface_port 97 | ) 98 | 99 | result, status_code = usecase.execute( 100 | userToSave=SaveUserDTO( 101 | username='fakeusername', 102 | email="fakeemail@email.com", 103 | name="fakename" 104 | ) 105 | ) 106 | saved_user: User = result['user'] 107 | 108 | mock_save_user_interface_port.save.assert_called() 109 | mock_load_user_port.load_user_by_username.assert_called_with( 110 | username="fakeusername" 111 | ) 112 | mock_load_user_port.load_user_by_email.assert_called_with( 113 | email="fakeemail@email.com", 114 | ) 115 | self.assertEqual(saved_user.id, 1) 116 | self.assertEqual(status_code, 201) 117 | 118 | 119 | class SaveUserWithGithubUsecaseTest(TestCase): 120 | def test_should_get_already_saved_user(self) -> None: 121 | mock_get_user_informations_port = GetUserInformationsFromGithubInterfacePort() 122 | mock_save_user_port = SaveUserPort() 123 | mock_load_user_port = LoadUserPort() 124 | mock_get_users_from_github_by_username_port = GetUsersInformationsFromGithubWithUsername() 125 | 126 | mock_load_user_port.load_user_by_username = Mock(return_value=User( 127 | id=1, 128 | bio="fake bio", 129 | email="any@email.com", 130 | last_name="Silver", 131 | name="John", 132 | username="johnsv" 133 | )) 134 | 135 | usecase = SaveUserWithGithubUsernameUsecase( 136 | get_github_informations_port=mock_get_user_informations_port, 137 | save_user_interface_port=mock_save_user_port, 138 | load_user_port=mock_load_user_port, 139 | get_users_from_github_by_username_port=mock_get_users_from_github_by_username_port, 140 | ) 141 | 142 | result, status_code = usecase.execute(github_username="fakeusername") 143 | user_already_saved: User = result['user'] 144 | 145 | mock_load_user_port.load_user_by_username.assert_called_with( 146 | username="fakeusername" 147 | ) 148 | self.assertEqual(status_code, 201) 149 | self.assertEqual(user_already_saved.id, 1) 150 | 151 | def test_should_not_found_github_user(self) -> None: 152 | mock_get_user_informations_port = GetUserInformationsFromGithubInterfacePort() 153 | mock_save_user_port = SaveUserPort() 154 | mock_load_user_port = LoadUserPort() 155 | mock_get_users_from_github_by_username_port = GetUsersInformationsFromGithubWithUsername() 156 | 157 | mock_load_user_port.load_user_by_username = Mock(return_value=None) 158 | mock_get_user_informations_port.load_github_informations = Mock( 159 | return_value=None) 160 | mock_get_users_from_github_by_username_port.load_users_by_username = Mock( 161 | return_value=[]) 162 | 163 | usecase = SaveUserWithGithubUsernameUsecase( 164 | get_github_informations_port=mock_get_user_informations_port, 165 | save_user_interface_port=mock_save_user_port, 166 | load_user_port=mock_load_user_port, 167 | get_users_from_github_by_username_port=mock_get_users_from_github_by_username_port, 168 | ) 169 | 170 | result, status_code = usecase.execute(github_username="fakeusername") 171 | 172 | mock_load_user_port.load_user_by_username.assert_called_with( 173 | username="fakeusername" 174 | ) 175 | mock_get_user_informations_port.load_github_informations.assert_called_with( 176 | username="fakeusername" 177 | ) 178 | mock_get_users_from_github_by_username_port.load_users_by_username.assert_called_with( 179 | username="fakeusername", limit=50 180 | ) 181 | 182 | self.assertEqual(result['message'], 'User not found on github') 183 | self.assertListEqual(result['availableUsers'], []) 184 | self.assertEqual(status_code, 404) 185 | 186 | def test_should_save_user_from_github(self) -> None: 187 | mock_get_user_informations_port = GetUserInformationsFromGithubInterfacePort() 188 | mock_save_user_port = SaveUserPort() 189 | mock_load_user_port = LoadUserPort() 190 | mock_get_users_from_github_by_username_port = GetUsersInformationsFromGithubWithUsername() 191 | 192 | mock_load_user_port.load_user_by_username = Mock(return_value=None) 193 | mock_get_user_informations_port.load_github_informations = Mock( 194 | return_value=GithubUser( 195 | name="fake name", 196 | bio="fake bio", 197 | email="any123@email.com", 198 | gender="", 199 | login="marlete", 200 | profileImageUrl="http://fakepath.com" 201 | )) 202 | mock_get_users_from_github_by_username_port.load_users_by_username = Mock( 203 | return_value=[]) 204 | mock_save_user_port.save = Mock(return_value=User( 205 | id=21, 206 | bio="fake bio", 207 | email="any@email.com", 208 | last_name="Silver", 209 | name="John", 210 | username="johnsv" 211 | )) 212 | 213 | usecase = SaveUserWithGithubUsernameUsecase( 214 | get_github_informations_port=mock_get_user_informations_port, 215 | save_user_interface_port=mock_save_user_port, 216 | load_user_port=mock_load_user_port, 217 | get_users_from_github_by_username_port=mock_get_users_from_github_by_username_port, 218 | ) 219 | 220 | result, status_code = usecase.execute(github_username="fakeusername") 221 | 222 | saved_user: User = result['user'] 223 | 224 | mock_load_user_port.load_user_by_username.assert_called_with( 225 | username="fakeusername" 226 | ) 227 | mock_get_user_informations_port.load_github_informations.assert_called_with( 228 | username="fakeusername" 229 | ) 230 | mock_get_users_from_github_by_username_port.load_users_by_username.assert_not_called() 231 | self.assertEqual(saved_user.id, 21) 232 | self.assertEqual(saved_user.name, 'John') 233 | self.assertEqual(status_code, 201) 234 | 235 | 236 | class LoadUserProfileUsecaseTest(TestCase): 237 | def test_should_not_found_user_by_id(self): 238 | mock_load_user_port = LoadUserPort() 239 | mock_get_user_metrics_port = GetUserMetricsGithubInterfacePort() 240 | 241 | mock_load_user_port.load_user_by_id = Mock(return_value=None) 242 | 243 | usecase = LoadUserProfileUsecase( 244 | get_user_metrics_port=mock_get_user_metrics_port, 245 | load_user_port=mock_load_user_port 246 | ) 247 | 248 | result, status_code = usecase.execute(user_id=12) 249 | 250 | mock_load_user_port.load_user_by_id.assert_called_with( 251 | id=12 252 | ) 253 | self.assertEqual(status_code, 404) 254 | self.assertEqual(result['message'], 'User not found') 255 | 256 | def test_should_return_user_without_metrics(self): 257 | mock_load_user_port = LoadUserPort() 258 | mock_get_user_metrics_port = GetUserMetricsGithubInterfacePort() 259 | 260 | mock_load_user_port.load_user_by_id = Mock(return_value=User( 261 | id=77, 262 | bio="any bio here", 263 | email="rigoni@email.com", 264 | last_name="Emidick", 265 | name="Rigonni", 266 | username="rigonni77" 267 | )) 268 | 269 | mock_get_user_metrics_port.load_user_metrics = Mock(return_value=GithubUserInformations( 270 | total_followers=10, 271 | profile_url="http://github.com/fakepage", 272 | total_following=20, 273 | total_public_repositories=21 274 | )) 275 | 276 | usecase = LoadUserProfileUsecase( 277 | get_user_metrics_port=mock_get_user_metrics_port, 278 | load_user_port=mock_load_user_port 279 | ) 280 | 281 | result, status_code = usecase.execute(user_id=1) 282 | 283 | user: dict = result['user'] 284 | 285 | mock_load_user_port.load_user_by_id.assert_called_with( 286 | id=1 287 | ) 288 | mock_get_user_metrics_port.load_user_metrics.assert_called_with( 289 | user_login="rigonni77" 290 | ) 291 | self.assertEqual(status_code, 200) 292 | self.assertEqual(user['id'], 77) 293 | self.assertEqual(user['total_followers'], 10) 294 | self.assertEqual(user['total_following'], 20) 295 | self.assertEqual(user['total_public_repositories'], 21) 296 | 297 | 298 | class LoadUserByEmailUsecaseTest(TestCase): 299 | def test_should_not_found_user(self): 300 | mock_load_user_port = LoadUserPort() 301 | mock_load_user_port.load_user_by_email = Mock(return_value=None) 302 | usecase = LoadUserByEmailUsecase( 303 | load_user_port=mock_load_user_port 304 | ) 305 | 306 | result, status_code = usecase.execute(user_email="email@email.com") 307 | mock_load_user_port.load_user_by_email.assert_called_with( 308 | email="email@email.com" 309 | ) 310 | self.assertEqual(status_code, 404) 311 | self.assertEqual(result['message'], 'User not found') 312 | 313 | def test_should_return_user(self): 314 | mock_load_user_port = LoadUserPort() 315 | mock_load_user_port.load_user_by_email = Mock(return_value=User( 316 | id=10, 317 | bio="i'm a holf", 318 | email="benitez@email.com", 319 | last_name="Benitez", 320 | name="Silva", 321 | username="benitez10" 322 | )) 323 | 324 | usecase = LoadUserByEmailUsecase( 325 | load_user_port=mock_load_user_port 326 | ) 327 | 328 | result, status_code = usecase.execute(user_email="email@email.com") 329 | user: User = result['user'] 330 | 331 | mock_load_user_port.load_user_by_email.assert_called_with( 332 | email="email@email.com" 333 | ) 334 | self.assertEqual(status_code, 200) 335 | self.assertEqual(user.id, 10) 336 | self.assertEqual(user.username, "benitez10") 337 | 338 | 339 | class LoadUserByUsernameUsecaseTest(TestCase): 340 | def test_should_not_found_user(self) -> None: 341 | mock_load_user_port = LoadUserPort() 342 | mock_load_user_port.load_user_by_username = Mock(return_value=None) 343 | usecase = LoadUserByUsernameUsecase( 344 | load_user_port=mock_load_user_port 345 | ) 346 | 347 | result, status_code = usecase.execute(user_username="jocaleri") 348 | 349 | mock_load_user_port.load_user_by_username.assert_called_with( 350 | username="jocaleri" 351 | ) 352 | self.assertEqual(status_code, 404) 353 | self.assertEqual(result['message'], 'User not found') 354 | 355 | def test_should_return_user(self): 356 | mock_load_user_port = LoadUserPort() 357 | mock_load_user_port.load_user_by_username = Mock(return_value=User( 358 | id=9, 359 | bio="killer", 360 | email="jocaleri@email.com", 361 | last_name="Calleri", 362 | name="", 363 | username="jocaleri9" 364 | )) 365 | usecase = LoadUserByUsernameUsecase( 366 | load_user_port=mock_load_user_port 367 | ) 368 | 369 | result, status_code = usecase.execute(user_username="jocaleri") 370 | user: User = result['user'] 371 | 372 | mock_load_user_port.load_user_by_username.assert_called_with( 373 | username="jocaleri" 374 | ) 375 | self.assertEqual(status_code, 200) 376 | self.assertEqual(user.id, 9) 377 | self.assertEqual(user.username, 'jocaleri9') 378 | 379 | 380 | class LoadUsersUsecaseTestCase(TestCase): 381 | def test_should_return_users(self): 382 | mock_load_user_port = LoadUserPort() 383 | mock_load_user_port.load_users = Mock(return_value=[]) 384 | usecase = LoadUsersUsecase( 385 | load_user_port=mock_load_user_port 386 | ) 387 | 388 | _, status_code = usecase.execute(limit=1, offset=2) 389 | 390 | mock_load_user_port.load_users.assert_called_with( 391 | limit=1, offset=2 392 | ) 393 | self.assertEqual(status_code, 200) 394 | 395 | 396 | class UpdateUserUsecaseTest(TestCase): 397 | def test_should_not_found_user(self): 398 | mock_load_user_port = LoadUserPort() 399 | mock_update_user_port = UpdateUserPort() 400 | mock_load_user_port.load_user_by_id = Mock(return_value=None) 401 | mock_update_user_port.update_user = Mock(return_value=None) 402 | 403 | usecase = UpdateUserUsecase( 404 | load_user_port=mock_load_user_port, 405 | update_user_port=mock_update_user_port 406 | ) 407 | 408 | response, status_code = usecase.execute(50, User( 409 | id=50, 410 | bio="killer", 411 | email="johnwick@email.com", 412 | last_name="johnwixi", 413 | name="john", 414 | username="johnwick" 415 | )) 416 | mock_load_user_port.load_user_by_id.assert_called_with(50) 417 | mock_update_user_port.update_user.assert_not_called() 418 | self.assertEqual(status_code, 400) 419 | self.assertEqual(response['message'], 'User not found') 420 | 421 | def test_should_update_user(self): 422 | user_saved = User( 423 | id=54, 424 | bio="killer", 425 | email="johnwick@email.com", 426 | last_name="johnwixi2", 427 | name="johnw", 428 | username="johnwick2" 429 | ) 430 | 431 | mock_load_user_port = LoadUserPort() 432 | mock_update_user_port = UpdateUserPort() 433 | 434 | mock_load_user_port.load_user_by_id = Mock(return_value=user_saved) 435 | mock_update_user_port.update_user = Mock(return_value=user_saved) 436 | 437 | usecase = UpdateUserUsecase( 438 | load_user_port=mock_load_user_port, 439 | update_user_port=mock_update_user_port 440 | ) 441 | 442 | response, status_code = usecase.execute(54, User( 443 | id=54, 444 | bio="killer", 445 | email="johnwick@email.com", 446 | last_name="johnwixi", 447 | name="john", 448 | username="johnwick" 449 | )) 450 | user: User = response['user'] 451 | 452 | mock_load_user_port.load_user_by_id.assert_called_with(54) 453 | mock_update_user_port.update_user.assert_called_once() 454 | 455 | self.assertEqual(status_code, 200) 456 | self.assertEqual(user.name, 'johnw') 457 | self.assertEqual(user.username, 'johnwick2') 458 | --------------------------------------------------------------------------------