diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42e87b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,164 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/Python,Pycharm +# Edit at https://www.toptal.com/developers/gitignore?templates=Python,Pycharm + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea + +# CMake +cmake-build-*/ + +# File-based project format +*.iws + +# IntelliJ +out/ + +# JIRA plugin +atlassian-ide-plugin.xml + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# End of https://www.toptal.com/developers/gitignore/api/Python,Pycharm diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e25ebf0 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +init: + pip install -r requirements.txt + +test: + nosetests tests \ No newline at end of file diff --git a/README.md b/README.md index d5ce48d..c7d1323 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # hr-resources -Esempio in Python di un gestore delle risorse umane +Esempio in Python di una REST API per la gestione delle risorse umane diff --git a/app.py b/app.py new file mode 100644 index 0000000..7b84d0d --- /dev/null +++ b/app.py @@ -0,0 +1,81 @@ +import itertools +from datetime import date +from typing import List, Dict + +from dateutil import relativedelta as period +from flask import Flask +from flask_json import FlaskJSON + +from db.access import fetch_european_departments, \ + fetch_department_employees, \ + fetch_american_departments, \ + fetch_employees, \ + fetch_departments, \ + fetch_canadian_departments +from models.department import Department + +app = Flask(__name__) +FlaskJSON(app) + +app.config['JSON_DATE_FORMAT'] = '%d/%m/%Y' +app.config['JSON_USE_ENCODE_METHODS'] = True + + +@app.get("/") +def index(): + return "

Homepage HR Resources

" + + +@app.errorhandler(404) +@app.errorhandler(500) +def handle_error(error): + return { + "status": error.code, + "description": error.description, + "name": error.name + } + + +@app.get("/employee") +def all_employees(): + return {"employees": [e for e in fetch_employees()]} + + +def __employees_group_by_seniority(departments: List[Department]) -> Dict[str, dict]: + result = {} + for department in departments: + # Per ogni dipartimento ricerco i suoi impiegati + department_employees = fetch_department_employees(department.department_id) + + # Raggruppo gli impiegati per anni di anzianità + employee_group_by_years = itertools.groupby(department_employees, + lambda e: period.relativedelta(date.today(), e.hire_date).years) + + result[department.name] = {year: [e for e in employees] + for (year, employees) in employee_group_by_years} + + return result + + +@app.get("/employee/all-by-seniority") +def employees_by_seniority(): + departments = fetch_departments() + return __employees_group_by_seniority(departments) + + +@app.get("/employee/american-by-seniority") +def american_employees_by_seniority(): + american_departments = fetch_american_departments() + return __employees_group_by_seniority(american_departments) + + +@app.get("/employee/canadian-by-seniority") +def canadian_employees_by_seniority(): + canadian_departments = fetch_canadian_departments() + return __employees_group_by_seniority(canadian_departments) + + +@app.get("/employee/european-by-seniority") +def european_employees_by_seniority(): + european_departments = fetch_european_departments() + return __employees_group_by_seniority(european_departments) diff --git a/db/__init__.py b/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/db/access.py b/db/access.py new file mode 100644 index 0000000..568beed --- /dev/null +++ b/db/access.py @@ -0,0 +1,265 @@ +from typing import List + +from psycopg2 import connect, extras as dbopts + +from models.department import Department +from models.employee import Employee +from models.location import Location + +db_url: str = 'postgresql://postgres:@localhost:5432/postgres' + + +def __create_department(row): + return Department( + department_id=row["department_id"], + name=row["department_name"], + location=Location( + location_id=row["location_id"], + postal_code=row["postal_code"], + street_address=row["street_address"], + state_province=row["state_province"], + country_id=row["country_id"], + city=row["city"] + )) + + +def fetch_departments() -> List[Department]: + """ + Ricerca tutti i dipartimenti attivi + :return: elenco dei dipartimenti + """ + with connect(db_url) as connection: + with connection.cursor(cursor_factory=dbopts.DictCursor) as cursor: + cursor.execute( + """ + select + d.department_id, + d.department_name, + d.location_id, + l.postal_code, + l.street_address, + l.city, + l.state_province, + l.country_id + from + hr.departments d + left join hr.locations l on + d.location_id = l.location_id + """ + ) + + departments = [__create_department(row) for row in cursor.fetchall()] + + return departments + + +def fetch_american_departments() -> List[Department]: + """ + Ricerca tutti i dipartimenti presenti negli USA. + :return: elenco dei dipartimenti USA + """ + with connect(db_url) as connection: + with connection.cursor(cursor_factory=dbopts.DictCursor) as cursor: + cursor.execute( + """ + select + d.department_id, + d.department_name, + d.location_id, + l.postal_code, + l.street_address, + l.city, + l.state_province, + l.country_id + from + hr.departments d + left join hr.locations l on + d.location_id = l.location_id + where + l.country_id = 'US' + """ + ) + + departments = [__create_department(row) for row in cursor.fetchall()] + + return departments + + +def fetch_canadian_departments() -> List[Department]: + """ + Ricerca tutti i dipartimenti presenti in Canada. + :return: elenco dei dipartimenti canadesi + """ + with connect(db_url) as connection: + with connection.cursor(cursor_factory=dbopts.DictCursor) as cursor: + cursor.execute( + """ + select + d.department_id, + d.department_name, + d.location_id, + l.postal_code, + l.street_address, + l.city, + l.state_province, + l.country_id + from + hr.departments d + left join hr.locations l on + d.location_id = l.location_id + where + l.country_id = 'CA' + """ + ) + + departments = [__create_department(row) for row in cursor.fetchall()] + + return departments + + +def fetch_european_departments() -> List[Department]: + """ + Ricerca tutti i dipartimenti dei paesi europei dove sono presenti + dei dipartimenti. + :return: elenco dei dipartimenti europei + """ + with connect(db_url) as connection: + with connection.cursor(cursor_factory=dbopts.DictCursor) as cursor: + cursor.execute( + """ + select + d.department_id, + d.department_name, + d.location_id, + l.postal_code, + l.street_address, + l.city, + l.state_province, + l.country_id + from + hr.departments d + left join hr.locations l on + d.location_id = l.location_id + where + l.country_id in ('UK', 'DE') + """ + ) + + # Prendo dal DB tutti i dipartimenti presenti nel Regno Unito e Germania + departments = [__create_department(row) for row in cursor.fetchall()] + + return departments + + +def fetch_department_employees(department_id: int) -> List[Employee]: + """ + Ricerca tutti i dipendenti di un dipartimento + :param department_id: dipartimento da ricercare + :return: elenco dei dipendenti + """ + employees = [] + with connect(db_url) as connection: + with connection.cursor(cursor_factory=dbopts.DictCursor) as cursor: + cursor.execute( + f""" + select + e.employee_id, + e.first_name, + e.last_name, + e.email, + e.phone_number, + e.hire_date, + e.job_id, + e.salary, + e.manager_id, + e.department_id, + d.department_name, + l.location_id, + l.city, + l.postal_code, + l.state_province, + l.street_address, + l.country_id + from + hr.employees e + left join hr.departments d on + e.department_id = d.department_id + left join hr.locations l on + d.location_id = l.location_id + where + e.department_id = %s + """, + (department_id,) + ) + + # Prendo dal DB tutti i dipendenti di un dipartimento + for row in cursor.fetchall(): + employees.append(Employee( + employee_id=row["employee_id"], + first_name=row["first_name"], + last_name=row["last_name"], + email=row["email"], + phone_number=row["phone_number"], + hire_date=row["hire_date"], + job_id=row["job_id"], + salary=row["salary"], + manager_id=row["manager_id"], + department=__create_department(row) + )) + + return employees + + +def fetch_employees() -> List[Employee]: + """ + Ricerca tutti i dipendenti dell'azienda + :return: elenco dei dipendenti + """ + employees = [] + with connect(db_url) as connection: + with connection.cursor(cursor_factory=dbopts.DictCursor) as cursor: + cursor.execute( + f""" + select + e.employee_id, + e.first_name, + e.last_name, + e.email, + e.phone_number, + e.hire_date, + e.job_id, + e.salary, + e.manager_id, + e.department_id, + d.department_name, + l.location_id, + l.city, + l.postal_code, + l.state_province, + l.street_address, + l.country_id + from + hr.employees e + left join hr.departments d on + e.department_id = d.department_id + left join hr.locations l on + d.location_id = l.location_id + """ + ) + + # Prendo dal DB tutti i dipendenti di un dipartimento + for row in cursor.fetchall(): + employees.append(Employee( + employee_id=row["employee_id"], + first_name=row["first_name"], + last_name=row["last_name"], + email=row["email"], + phone_number=row["phone_number"], + hire_date=row["hire_date"], + job_id=row["job_id"], + salary=row["salary"], + manager_id=row["manager_id"], + department=__create_department(row) + )) + + return employees diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/department.py b/models/department.py new file mode 100644 index 0000000..65ea1e9 --- /dev/null +++ b/models/department.py @@ -0,0 +1,30 @@ +from models.location import Location + + +class Department: + def __init__(self, department_id: int, name: str, location: Location): + self.__department_id = department_id + self.__name = name + self.__location = location + + @property + def department_id(self) -> int: + return self.__department_id + + @property + def name(self) -> str: + return self.__name + + @property + def location_id(self) -> Location: + return self.__location + + def __json__(self): + return { + 'department_id': self.__department_id, + 'name': self.__name, + 'location': self.__location if self.__location is not None else None + } + + def __str__(self): + return f"Department({self.__department_id}, {self.__name}, {self.__location})" diff --git a/models/employee.py b/models/employee.py new file mode 100644 index 0000000..706008f --- /dev/null +++ b/models/employee.py @@ -0,0 +1,80 @@ +from datetime import date + +from models.department import Department + + +class Person: + def __init__(self, first_name, last_name): + self.__first_name = first_name + self.__last_name = last_name + + @property + def first_name(self) -> str: + return self.__first_name + + @property + def last_name(self) -> str: + return self.__last_name + + +class Employee(Person): + def __init__(self, employee_id: int, first_name: str, last_name: str, email: str, phone_number: str, + hire_date: date, department: Department, job_id: int = 0, salary: float = 0, manager_id: int = 0): + super().__init__(first_name, last_name) + self.__employee_id = employee_id + self.__email = email + self.__phone_number = phone_number + self.__hire_date = hire_date + self.__department = department + self.__job_id = job_id + self.__salary = salary + self.__manager_id = manager_id + + @property + def employee_id(self) -> int: + return self.__employee_id + + @property + def email(self) -> str: + return self.__email + + @property + def phone_number(self) -> str: + return self.__phone_number + + @property + def hire_date(self) -> date: + return self.__hire_date + + @property + def department(self) -> Department: + return self.__department + + @property + def job_id(self) -> int: + return self.__job_id + + @property + def salary(self) -> float: + return self.__salary + + @property + def manager_id(self) -> int: + return self.__manager_id + + def __json__(self): + return { + 'employee_id': self.__employee_id, + 'first_name': self.first_name, + 'last_name': self.last_name, + 'email': self.__email, + 'hire_date': self.__hire_date, + 'department': self.__department if self.__department is not None else None, + 'job_id': self.__job_id, + 'salary': f"{self.__salary:,.2f}", + 'manager_id': self.__manager_id + } + + def __str__(self): + return f"Employee({self.__employee_id}, {self.first_name}, {self.last_name}, " \ + f"{self.__email}, {self.__phone_number}, {self.__hire_date})" diff --git a/models/location.py b/models/location.py new file mode 100644 index 0000000..125dc63 --- /dev/null +++ b/models/location.py @@ -0,0 +1,47 @@ +class Location: + def __init__(self, location_id: int, street_address: str, postal_code: str, + city: str, state_province: str, country_id: int): + self.__location_id = location_id + self.__street_address = street_address + self.__postal_code = postal_code + self.__city = city + self.__state_province = state_province + self.__country_id = country_id + + @property + def location_id(self) -> int: + return self.__location_id + + @property + def street_address(self) -> str: + return self.__street_address + + @property + def postal_code(self) -> str: + return self.__postal_code + + @property + def city(self) -> str: + return self.__city + + @property + def state_province(self) -> str: + return self.__state_province + + @property + def country_id(self) -> int: + return self.__country_id + + def __json__(self): + return { + 'location_id': self.__location_id, + 'street_address': self.__street_address, + 'postal_code': self.__postal_code, + 'city': self.__city, + 'state_province': self.__state_province, + 'country_id': self.__country_id + } + + def __str__(self): + return f"Location({self.__location_id}, {self.__postal_code}, " \ + f"{self.__street_address}, {self.__city}, {self.__state_province}) " diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e0bc9b0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +records~=0.5.3 +psycopg2-binary~=2.9.1 +setuptools~=57.0.0 +python-dateutil~=2.8.1 +Flask~=2.0.1 +Flask-JSON~=0.3.4 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9142566 --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup + +setup( + name='hr-resources', + version='0.1.0', + packages=[''], + url='', + license='', + author='Fabio Scotto di Santolo', + author_email='fabio.scottodisantolo@gmail.com', + description='Servizio per la visualizzazione delle risorse umane' +)