diff --git a/homework/cli.py b/homework/cli.py new file mode 100644 index 0000000..813279a --- /dev/null +++ b/homework/cli.py @@ -0,0 +1,43 @@ +import click +from patient import Patient, PatientCollection, DEFAULT_PARAMS, Patient_DB + + +@click.group() +def cli(): + pass + + +@click.command() +@click.argument('first_name', type=str) +@click.argument('last_name', type=str) +@click.option('--birth-date', '-b', default=None, type=str) +@click.option('--phone', '-p', default=None, type=str) +@click.option('--document-type', '-t', nargs=2, default=str) +@click.option('--document-number', '-n', nargs=3, default=str) +def create(first_name, last_name, birth_date, phone, document_type, document_number): + if None in (first_name, last_name, birth_date, phone, document_type, document_number): + raise Exception + Patient.create(first_name, last_name, birth_date, phone, ' '.join(document_type), ' '.join(document_number)).save() + click.echo('Patient added') + + +@click.command() +@click.argument('limit_val', default=10, type=int) +def show(limit_val): + if limit_val >= 0: + p = PatientCollection(*DEFAULT_PARAMS).limit(limit_val) + for i in p: + click.echo(i) + + +@click.command() +def count(): + click.echo(PatientCollection(*DEFAULT_PARAMS).session.query(Patient_DB).count()) + + +cli.add_command(create) +cli.add_command(show) +cli.add_command(count) + +if __name__ == '__main__': + cli() \ No newline at end of file diff --git a/homework/config.py b/homework/config.py index 955b991..982f963 100644 --- a/homework/config.py +++ b/homework/config.py @@ -1,11 +1,10 @@ -GOOD_LOG_FILE = "good_log.txt" +GOOD_LOG_FILE = "info_log.txt" ERROR_LOG_FILE = "error_log.txt" -CSV_PATH = "csv.csv" -PHONE_FORMAT = "79160000000" # Здесь запишите телефон +7-916-000-00-00 в том формате, в котором вы храните телефоны +CSV_PATH = "data.csv" +PHONE_FORMAT = "+79160000000" # Здесь запишите телефон +7-916-000-00-00 в том формате, в котором вы храните телефоны PASSPORT_TYPE = "паспорт" # тип документа, когда он паспорт -PASSPORT_FORMAT = "0000 000000" # Здесь запишите номер парспорта 0000 000000 в том формате, в котором вы его храните - +PASSPORT_FORMAT = "00 00 000000" # Здесь запишите номер парспорта 0000 000000 в том формате, в котором вы его храните INTERNATIONAL_PASSPORT_TYPE = "заграничный паспорт" # тип документа, если это загран INTERNATIONAL_PASSPORT_FORMAT = "00 0000000" # формат хранения заграна для номера 00 0000000 diff --git a/homework/db_config.py b/homework/db_config.py new file mode 100644 index 0000000..8e46c09 --- /dev/null +++ b/homework/db_config.py @@ -0,0 +1,38 @@ +from sqlalchemy import create_engine +from sqlalchemy import Column, String, Integer +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from sqlalchemy.exc import SQLAlchemyError, NoSuchTableError + +DEFAULT_PARAMS = ('postgres','317a251','localhost','5432') + +base = declarative_base() + + +class Patient_DB(base): + __tablename__ = 'patients' + + id = Column(Integer, primary_key=True) + first_name = Column(String) + last_name = Column(String) + birth_date = Column(String) + phone = Column(String) + document_type = Column(String) + document_id = Column(String) + + + + +def init_connection(user, password, host, port): + engine = create_engine("postgres://{0}:{1}@{2}:{3}/Patient_DB".format(user, password, host, port)) + return sessionmaker(engine)() + + + +#Flask Web Development +#Flask Mega Tutorial +#flask sqlalchemy +#flasj marshmellow +#ORM, многомодульность, хорошая бизнес-логика/техническая сложность + + diff --git a/homework/patient.py b/homework/patient.py index dad2526..7a03d59 100644 --- a/homework/patient.py +++ b/homework/patient.py @@ -1,17 +1,262 @@ +import datetime +import logging +from functools import wraps +from db_config import init_connection, Patient_DB, SQLAlchemyError, NoSuchTableError, DEFAULT_PARAMS + +logger_info = logging.getLogger("patient_log_info") +logger_info.setLevel(logging.INFO) +logger_error = logging.getLogger("patient_log_error") +logger_error.setLevel(logging.ERROR) + +formatter = logging.Formatter("%(filename)s[LINE:%(lineno)d]# %(levelname)-8s [%(asctime)s] %(message)s") + +error_logs = logging.FileHandler('error_log.txt', 'a', 'utf-8') +error_logs.setFormatter(formatter) + +info_logs = logging.FileHandler('info_log.txt', 'a', 'utf-8') +info_logs.setFormatter(formatter) + +logger_info.addHandler(info_logs) +logger_error.addHandler(error_logs) + +PASSPORT = 'паспорт' +INTERNATIONAL_PASS_1 = 'загранпаспорт' +INTERNATIONAL_PASS_2 = 'заграничный паспорт' +DRIVER_LICENSE_1 = 'водительское удостоверение' +DRIVER_LICENSE_2 = 'водительские права' + + +def logger_decorator_maker(id=None): + def logger_decorator(fun): + @wraps(fun) + def wrapper(value, *args): + + global checked_value # не знаю нужно ли, если переменная создаётся внутри блока try, но на всякий случай написал + try: + checked_value = fun(value, *args) + except ValueError as error: + logger_error.error(error.args[0]) + raise ValueError(error.args[0]) + except TypeError as error: + logger_error.error(error.args[0]) + raise TypeError(error.args[0]) + except AttributeError as error: + logger_error.error(error.args[0]) + raise AttributeError(error.args[0]) + except NoSuchTableError: + logger_error.error('The table does not exist or is not visible for the join.') + raise Exception('The table does not exist or is not visible for the join.') + except SQLAlchemyError: + logger_error.error('Error while working with database') + raise Exception('Error while working with database') + else: + if id == 'init': + logger_info.info("Patient added") + elif id == 'patient_set' and checked_value: + logger_info.info("Field " + args[1] + " were updated") + elif id == 'save': + logger_info.info("Patient saved") + elif id == 'init connection': + logger_info.info("Connect to DB successful") + + return checked_value + + return wrapper + + return logger_decorator + + +@logger_decorator_maker() +def name_check(name): + if not isinstance(name, str): + raise TypeError("Incorrect type of name or surname") + if not name.isalpha(): + raise ValueError("Name or surname contains invalid characters") + return name.capitalize() + + +@logger_decorator_maker() +def birth_check(born): + if not isinstance(born, str): + raise TypeError("Incorrect type of document birth date") + if len(born) != 10: + raise ValueError("Incorrect date length") + born = born[:4] + '-' + born[5:7] + '-' + born[8:] + for k, i in enumerate(born): + if i.isdigit() and k not in (4,7): + continue + elif (k == 4 or k == 7) and i == '-': + continue + else: + raise ValueError("Date contains invalid characters") + if str(datetime.date.today()) >= born: + return born + else: + raise ValueError("Date does not exist yet") + + +@logger_decorator_maker() +def phone_check(phone): + if not isinstance(phone, str): + raise TypeError("Incorrect type of phone") + phone = phone.replace('+', '') + phone = phone.replace('(', '') + phone = phone.replace(')', '') + phone = phone.replace('-', '') + phone = phone.replace(' ', '') + if len(phone) == 11: + if phone.isdigit(): + return "+7" + phone[1:] + else: + raise ValueError("Phone number contains invalid characters") + else: + raise ValueError("Incorrect phone number length") + + +@logger_decorator_maker() +def doc_check(doc): + if not isinstance(doc, str): + raise TypeError("Incorrect type of document") + if doc.lower() != PASSPORT and doc.lower() != INTERNATIONAL_PASS_1 and doc.lower() != INTERNATIONAL_PASS_2 and \ + doc.lower() != DRIVER_LICENSE_1 and doc.lower() != DRIVER_LICENSE_2: + raise ValueError("Incorrect document") + return doc.lower() + + +@logger_decorator_maker() +def doc_id_check(doc_id): + if not isinstance(doc_id, str): + raise TypeError("Incorrect type of document id") + doc_id = doc_id.replace(' ', '') + doc_id = doc_id.replace('-', '') + doc_id = doc_id.replace('/', '') + doc_id = doc_id.replace('\\', '') + if doc_id.isdigit(): + if len(doc_id) == 10: + return doc_id[:2] + ' ' + doc_id[2:4] + ' ' + doc_id[4:] + elif len(doc_id) == 9: + return doc_id[:2] + ' ' + doc_id[2:] + else: + raise ValueError("Incorrect document's number length") + else: + raise ValueError("Document number contains invalid characters") + + +# Descriptor +class DataAccess: + def __init__(self, name='атрибут', data_get=None, data_set=None, data_del=None, data_check=None): + self.data_get = data_get + self.data_set = data_set + self.data_del = data_del + self.data_check = data_check + self.name = name + + def __get__(self, obj, objtype): + return self.data_get(obj, self.name) + + @logger_decorator_maker() + def __set__(self, obj, val): + self.data_set(obj, self.data_check(val), self.name) + + def __delete__(self, obj): + self.data_del(obj, self.name) + + class Patient: - def __init__(self, *args, **kwargs): - pass + def get_field(self, key): + return getattr(self, "_" + key) + + @logger_decorator_maker('patient_set') + def set_field(self, value, key): + create_flag = True + if not hasattr(self, '_' + key): + create_flag = False + if key == 'first_name' and not hasattr(self, '_first_name'): + self._first_name = value + elif key == 'last_name' and not hasattr(self, '_last_name'): + self._last_name = value + elif (key == 'first_name' and hasattr(self, '_first_name')) or ( + key == 'last_name' and hasattr(self, '_last_name')): + raise AttributeError("Fields \"first_name\" and \"last_name\" mustn't be changed") + elif key == 'birth_date': + self._birth_date = value + elif key == 'phone': + self._phone = value + elif key == 'document_type': + self._document_type = value + elif key == 'document_id': + if ((self._document_type == PASSPORT or self._document_type == DRIVER_LICENSE_1 or + self._document_type == DRIVER_LICENSE_2) and len(value) == 12) or \ + ((self._document_type == INTERNATIONAL_PASS_1 or self._document_type == INTERNATIONAL_PASS_2) + and len(value) == 10): + self._document_id = value + else: + raise ValueError("Number of characters does not match the type of document") + return create_flag + + def del_field(self, key): + if key == 'first_name': + del self._first_name + elif key == 'last_name': + del self._last_name + elif key == 'birth_date': + del self._birth_date + elif key == 'phone': + del self._phone + elif key == 'document_type': + del self._document_type + elif key == 'document_id': + del self._document_id - def create(*args, **kwargs): - raise NotImplementedError() + first_name = DataAccess('first_name', get_field, set_field, del_field, name_check) + last_name = DataAccess('last_name', get_field, set_field, del_field, name_check) + birth_date = DataAccess('birth_date', get_field, set_field, del_field, birth_check) + phone = DataAccess('phone', get_field, set_field, del_field, phone_check) + document_type = DataAccess('document_type', get_field, set_field, del_field, doc_check) + document_id = DataAccess('document_id', get_field, set_field, del_field, doc_id_check) + @logger_decorator_maker('init') + def __init__(self, name, surname, born, phone, doc, doc_id): + self.first_name = name + self.last_name = surname + self.birth_date = born + self.phone = phone + self.document_type = doc + self.document_id = doc_id + + def __str__(self): + return (self.first_name + ' ' + self.last_name + ' ' + self.birth_date + ' ' + self.phone + ' ' + + self.document_type + ' ' + self.document_id) + + @logger_decorator_maker('save') def save(self): - pass + session = init_connection(*DEFAULT_PARAMS) + patient = Patient_DB(first_name=self.first_name, last_name=self.last_name, birth_date=self.birth_date, + phone=self.phone, document_type=self.document_type, document_id=self.document_id) + session.add(patient) + session.commit() + + @staticmethod + def create(name, surname, born, phone, doc, number): + return Patient(name, surname, born, phone, doc, number) class PatientCollection: - def __init__(self, log_file): - pass + @logger_decorator_maker('init connection') + def __init__(self, user, password, host, port): + if isinstance(user, str) and isinstance(password, str) and isinstance(host, str) and isinstance(port, str): + self.session = init_connection(user, password, host, port) + else: + raise TypeError("Incorrect type of input data") + + @logger_decorator_maker() + def __iter__(self): + for patient in self.session.query(Patient_DB): + yield Patient(patient.first_name, patient.last_name, patient.birth_date, patient.phone, + patient.document_type, patient.document_id) - def limit(self, n): - raise NotImplementedError() + @logger_decorator_maker() + def limit(self, limit_val): + for patient in self.session.query(Patient_DB).limit(limit_val): + yield Patient(patient.first_name, patient.last_name, patient.birth_date, patient.phone, + patient.document_type, patient.document_id) diff --git a/tests/test_patient.py b/tests/test_patient.py index a442599..fe5f507 100644 --- a/tests/test_patient.py +++ b/tests/test_patient.py @@ -2,7 +2,7 @@ import os from datetime import datetime import itertools - +import logging import pytest from homework.config import GOOD_LOG_FILE, ERROR_LOG_FILE, CSV_PATH, PHONE_FORMAT, PASSPORT_TYPE, PASSPORT_FORMAT, \ @@ -39,6 +39,7 @@ def setup_module(__main__): def teardown_module(__name__): for file in [GOOD_LOG_FILE, ERROR_LOG_FILE, CSV_PATH]: + logging.shutdown() os.remove(file)