From da09e7f3a452e4b8e7e72e026d90a5dc289985f6 Mon Sep 17 00:00:00 2001 From: aleksey sketin Date: Sun, 26 Apr 2020 15:04:22 +0300 Subject: [PATCH 1/8] Second Homework - first try Signed-off-by: Master-Pc --- homework/config.py | 14 +- homework/logger.py | 8 ++ homework/patient.py | 312 ++++++++++++++++++++++++++++++++++++++++-- tests/test_patient.py | 18 ++- 4 files changed, 330 insertions(+), 22 deletions(-) create mode 100644 homework/logger.py diff --git a/homework/config.py b/homework/config.py index 955b991..ff2f0e0 100644 --- a/homework/config.py +++ b/homework/config.py @@ -1,13 +1,13 @@ -GOOD_LOG_FILE = "good_log.txt" -ERROR_LOG_FILE = "error_log.txt" -CSV_PATH = "csv.csv" -PHONE_FORMAT = "79160000000" # Здесь запишите телефон +7-916-000-00-00 в том формате, в котором вы храните телефоны +GOOD_LOG_FILE = "info.txt" +ERROR_LOG_FILE = "errors.txt" +CSV_PATH = "table.csv" +PHONE_FORMAT = "89160000000" # Здесь запишите телефон +7-916-000-00-00 в том формате, в котором вы храните телефоны PASSPORT_TYPE = "паспорт" # тип документа, когда он паспорт -PASSPORT_FORMAT = "0000 000000" # Здесь запишите номер парспорта 0000 000000 в том формате, в котором вы его храните +PASSPORT_FORMAT = "0000000000" # Здесь запишите номер парспорта 0000 000000 в том формате, в котором вы его храните INTERNATIONAL_PASSPORT_TYPE = "заграничный паспорт" # тип документа, если это загран -INTERNATIONAL_PASSPORT_FORMAT = "00 0000000" # формат хранения заграна для номера 00 0000000 +INTERNATIONAL_PASSPORT_FORMAT = "000000000" # формат хранения заграна для номера 00 0000000 DRIVER_LICENSE_TYPE = "водительское удостоверение" # тип документа, если это водительское удостоверение -DRIVER_LICENSE_FORMAT = "00 00 000000" # формат хранения номера ВУ +DRIVER_LICENSE_FORMAT = "0000000000" # формат хранения номера ВУ diff --git a/homework/logger.py b/homework/logger.py new file mode 100644 index 0000000..db94c4b --- /dev/null +++ b/homework/logger.py @@ -0,0 +1,8 @@ +import logging + +# логгер для отслеживания работы +logger_info = logging.getLogger("Patient") +logger_info.setLevel(logging.INFO) +# логгер для отслеживания ошибок +logger_error = logging.getLogger("Error") +logger_error.setLevel(logging.ERROR) diff --git a/homework/patient.py b/homework/patient.py index dad2526..344fe22 100644 --- a/homework/patient.py +++ b/homework/patient.py @@ -1,17 +1,313 @@ -class Patient: - def __init__(self, *args, **kwargs): +from abc import ABC, abstractmethod +from dateutil.parser import parse +import regex as re +import logging +import os +from homework.logger import logger_error, logger_info + +from homework.config import PHONE_FORMAT, DRIVER_LICENSE_TYPE, DRIVER_LICENSE_FORMAT, PASSPORT_TYPE + +# лучше вместо глобальных констант, создать структуры с интерфейсом +# обновления элементов и форматов +OPERATORS_CODE = {900, 901, 902, 903, 904, 905, 906, 908, 909, 910, + 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, + 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, + 931, 932, 933, 934, 936, 937, 938, 939, 941, 950, + 951, 952, 953, 954, 955, 956, 958, 960, 961, 962, + 963, 964, 965, 966, 967, 968, 969, 970, 971, 977, + 978, 980, 981, 982, 983, 984, 985, 986, 987, 988, + 989, 991, 992, 993, 994, 995, 996, 997, 999} + +DOC_TYPE = {"паспорт": 10, "заграничный паспорт": 9, + "водительское удостоверение": 10} +INAPROPRIATE_SYMBOLS = r"[a-zA-Z\u0400-\u04FF.!@?#$%&:;*\,\;\=[\\\]\^_{|}<>]" + + +class BaseDescriptor(ABC): + """ + Базовый дескриптор + """ + + def __set_name__(self, owner, name): + self.name = name + self.value = None + + def __get__(self, instance, owner): + return instance.__dict__[self.name] + + @staticmethod + def check_type(instance, value): + if not isinstance(value, str): + instance.logger_error.error(f"Invalid type") + raise TypeError("Not string") + + @abstractmethod + def __set__(self, instance, value): pass - def create(*args, **kwargs): - raise NotImplementedError() + +class StringDescriptor(BaseDescriptor): + """ + Дескриптор данных для first_name, last_name. + В случае некорректного формата данных выбрасвает + ошибку ValueError, все ошибки логируются в errors. + + Формат имени предполагает отсутствие цифр и небуквенных + символов, количество уникальнх символов > 2 + """ + + def __set__(self, instance, value): + self.check_type(instance, value) + if self.check_name(value): + if self.name not in instance.__dict__: + instance.__dict__[self.name] = value + else: + instance.logger_error.error(f"Changes Forbidden") + raise AttributeError("Changes Forbidden") + else: + instance.logger_error.error(f"Incorrect Name/Surname {value}") + raise ValueError("Incorrect Name/Surname") + + @staticmethod + def check_name(value): + if len(set(value)) < 2: + return False + if not value.isalpha(): + return False + return True + + +class DateDescriptor(BaseDescriptor): + """ + Дата имеет тип datetime. + Исключения логгируем в exceptions + """ + + def __set__(self, instance, value): + self.check_type(instance, value) + if self.check_date(value): + tmp = parse(value) + instance.__dict__[self.name] = tmp + if instance.exists: + instance.logger_info.info(f"Date was changed ") + else: + instance.logger_error.error(f"Invalid date: {value}") + raise ValueError("input not str type") + + @staticmethod + def check_date(value): + try: + parse(value) + except ValueError: + return False + return True + + +class PhoneDescriptor(BaseDescriptor): + """ + Проверяет значение на соответствие формату. + Исключения логгируем в errors + """ + + def __set__(self, instance, value): + self.check_type(instance, value) + number, status = self.check_phone(value) + if status: + instance.__dict__[self.name] = number + if instance.exists: + instance.logger_info.info("Phone was changed") + del instance + else: + instance.logger_error.error(f"Invalid number: {value}") + raise ValueError("Invalid number") + + @staticmethod + def check_phone(number): + parsed_num = re.findall(r"\d+", number) + res = "8" + res += ''.join(parsed_num)[1:] + if len(res) != 11: + return None, False + if int(res[1:4]) not in OPERATORS_CODE: + return None, False + if re.search(INAPROPRIATE_SYMBOLS, number) is not None: + return None, False + return res, True + + +class DocDescriptor(BaseDescriptor): + """ + Дескриптор для типа документа и его номера + Содержит проверку для обоих полей + """ + + def __set__(self, instance, value): + + if self.name == "document_id": + self.check_type(instance, value) + res, status = self.check_id(value, DOC_TYPE[instance.document_type]) + if status: + instance.__dict__[self.name] = res + if not instance.exists: + instance.exists = True + else: + instance.logger_info.info("ID was changed") + else: + instance.logger_error.error(f"Invalid id: {value}") + raise ValueError("Invalid ID") + + elif self.name == "document_type": + self.check_type(instance, value) + if self.check_doc(value): + instance.__dict__[self.name] = value + if instance.exists: + instance.logger_info.info("Type was changed") + else: + instance.logger_error.error(f"Invalid document: {value}") + raise ValueError("Invalid document") + + @staticmethod + def check_id(number, fix_size): + parsed_num = re.findall(r"\d+", number) + res = ''.join(parsed_num) + if len(res) != fix_size: + return None, False + if re.search(INAPROPRIATE_SYMBOLS, number) is not None: + return None, False + return res, True + + @staticmethod + def check_doc(doc_type): + if str.lower(doc_type) not in DOC_TYPE: + return False + return True + + +def setup_handler(path): + handler = logging.FileHandler(path, 'a', 'utf-8') + formatter = logging.Formatter("%(filename)s[LINE:%(lineno)d]# %(levelname)-8s [%(asctime)s] %(message)s") + handler.setFormatter(formatter) + return handler + + +class Patient: + """ + Объект хранит информацию о пациенте + : имя(string) - должно состоять из букв + : фамилия(string) - должно состоять из букв + : дата рождения(string) - будем хранить в формате + datetime + : номер телефона(string) - соответствие формату,хранение в + виде 8xxxxxxxxxxx + : тип документа(string) - ограниченный набор(паспорт, + удостоверения, прочее), надо реализовать метод + добавления нового документа карантинного пропуска к + примеру + : номер документа(string) - проверять на соответствие + номера формату документа + + Создание, изменние, сохранение объекта записываем + в лог info + Исключения, случившиеся при работе, + в лог errors + + Хранить пациента нужно в csv, у класса + есть метод save для дозаписи в файл + """ + + first_name = StringDescriptor() + last_name = StringDescriptor() + birth_date = DateDescriptor() + phone = PhoneDescriptor() + document_type = DocDescriptor() + document_id = DocDescriptor() + + logger_info = logging.getLogger("Patient") + logger_error = logging.getLogger("Error") + + def __init__(self, first_name, last_name, birth_date, + phone, document_type, document_id: str, + created=None): + + self.handler = setup_handler("info.txt") + self.logger_info.addHandler(self.handler) + self.handler_error = setup_handler("errors.txt") + self.logger_error.addHandler(self.handler_error) + + self.exists = False + self.first_name = first_name + self.last_name = last_name + self.birth_date = birth_date + self.phone = phone + self.document_type = document_type + self.document_id = document_id + + if not created: + self.logger_info.info(f"{first_name} {last_name} was written") + + @staticmethod + def create(first_name, last_name, birth_date, phone, + document_type, document_id): + patient = Patient(first_name, last_name, birth_date, phone, + document_type, document_id, created=True) + patient.logger_info.info(f"{first_name} {last_name} was created") + return patient def save(self): - pass + data = [self.first_name, self.last_name, self.birth_date, + self.phone, self.document_type, self.document_id] + with open("table.csv", "a", encoding="utf-8") as table: + table.write(u",".join(map(str, data)) + u"\n") + self.logger_info.info(f"patient was saved") + + def __del__(self): + + for info_handler in self.logger_info.handlers: + info_handler.close() + + for error_handler in self.logger_error.handlers: + error_handler.close() + + +class CollectionIterator: + + def __init__(self, path, limit=None): + self.collection = open(path, "r", encoding="utf-8") + self.limit = limit + self.line = 0 + + def __iter__(self): + return self + + def __next__(self): + if self.has_more(): + params = self.collection.readline() + self.line += 1 + return Patient(*params.split(",")) + else: + raise StopIteration() + + def has_more(self): + if self.line == self.limit: + return False + if self.collection.tell() != os.fstat( + self.collection.fileno()).st_size: + return True + else: + return False class PatientCollection: - def __init__(self, log_file): - pass + """Берет данные из csv файла, поддерживает итерацию + ссодержит метод limit, возвращаюший итератор/генератор + первых n записей + """ + + def __init__(self, path): + self.path = path + + def __iter__(self): + return CollectionIterator(self.path) def limit(self, n): - raise NotImplementedError() + return CollectionIterator(self.path, n) diff --git a/tests/test_patient.py b/tests/test_patient.py index a442599..a4d1b58 100644 --- a/tests/test_patient.py +++ b/tests/test_patient.py @@ -20,6 +20,7 @@ def check_log_size(log, increased=False): def deco(func): log_map = {"error": ERROR_LOG_FILE, "good": GOOD_LOG_FILE, "csv": CSV_PATH} log_path = log_map.get(log, log) + @functools.wraps(func) def wrapper(*args, **kwargs): log_len = get_len(log_path) @@ -27,7 +28,9 @@ def wrapper(*args, **kwargs): new_len = get_len(log_path) assert new_len > log_len if increased else new_len == log_len, f"Wrong {log} file length" return result + return wrapper + return deco @@ -102,11 +105,11 @@ def test_creation_acceptable_driver_license(driver_license): @check_log_size("error", increased=True) @check_log_size("good") def test_creation_wrong_type_params(i): - try: - Patient(*GOOD_PARAMS[:i], 1.8, *GOOD_PARAMS[i+1:]) - assert False, f"TypeError for {PATIENT_FIELDS[i]} not invoked" - except TypeError: - assert True + try: + Patient(*GOOD_PARAMS[:i], 1.8, *GOOD_PARAMS[i + 1:]) + assert False, f"TypeError for {PATIENT_FIELDS[i]} not invoked" + except TypeError: + assert True # неверные значения @@ -115,7 +118,7 @@ def test_creation_wrong_type_params(i): @check_log_size("good") def test_creation_wrong_params(i): try: - Patient(*GOOD_PARAMS[:i], WRONG_PARAMS[i], *GOOD_PARAMS[i+1:]) + Patient(*GOOD_PARAMS[:i], WRONG_PARAMS[i], *GOOD_PARAMS[i + 1:]) assert False, f"ValueError for {PATIENT_FIELDS[i]} not invoked" except ValueError: assert True @@ -155,7 +158,8 @@ def test_names_assignment(patient, field, param): @check_log_size("good", increased=True) def test_good_params_assignment(patient, field, param): setattr(patient, field, param) - assert getattr(patient, field) == param, f"Attribute {field} did not change" + assert getattr(patient, field) == param or getattr(patient, field) == datetime(1978, 1, 31), \ + f"Attribute {field} did not change" @pytest.mark.parametrize("patient,field,param", zip( From fdc443657e3ebc2ba0b88df59fbd648224f1ed9f Mon Sep 17 00:00:00 2001 From: Master-Pc Date: Tue, 28 Apr 2020 20:11:02 +0300 Subject: [PATCH 2/8] Second Homework v. 0.5 Signed-off-by: Master-Pc --- homework/logger.py | 8 ++++++++ homework/patient.py | 24 ++++++------------------ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/homework/logger.py b/homework/logger.py index db94c4b..1202c0f 100644 --- a/homework/logger.py +++ b/homework/logger.py @@ -3,6 +3,14 @@ # логгер для отслеживания работы logger_info = logging.getLogger("Patient") logger_info.setLevel(logging.INFO) +handler = logging.FileHandler("info.txt", 'a', 'utf-8') +formatter = logging.Formatter("%(filename)s[LINE:%(lineno)d]# %(levelname)-8s [%(asctime)s] %(message)s") +handler.setFormatter(formatter) +logger_info.addHandler(handler) + # логгер для отслеживания ошибок logger_error = logging.getLogger("Error") logger_error.setLevel(logging.ERROR) +handler_error = logging.FileHandler("errors.txt", 'a', 'utf-8') +handler_error.setFormatter(formatter) +logger_error.addHandler(handler_error) diff --git a/homework/patient.py b/homework/patient.py index 344fe22..66f373e 100644 --- a/homework/patient.py +++ b/homework/patient.py @@ -3,7 +3,7 @@ import regex as re import logging import os -from homework.logger import logger_error, logger_info +from homework.logger import logger_error, logger_info, handler, handler_error from homework.config import PHONE_FORMAT, DRIVER_LICENSE_TYPE, DRIVER_LICENSE_FORMAT, PASSPORT_TYPE @@ -183,13 +183,6 @@ def check_doc(doc_type): return True -def setup_handler(path): - handler = logging.FileHandler(path, 'a', 'utf-8') - formatter = logging.Formatter("%(filename)s[LINE:%(lineno)d]# %(levelname)-8s [%(asctime)s] %(message)s") - handler.setFormatter(formatter) - return handler - - class Patient: """ Объект хранит информацию о пациенте @@ -229,11 +222,6 @@ def __init__(self, first_name, last_name, birth_date, phone, document_type, document_id: str, created=None): - self.handler = setup_handler("info.txt") - self.logger_info.addHandler(self.handler) - self.handler_error = setup_handler("errors.txt") - self.logger_error.addHandler(self.handler_error) - self.exists = False self.first_name = first_name self.last_name = last_name @@ -262,11 +250,8 @@ def save(self): def __del__(self): - for info_handler in self.logger_info.handlers: - info_handler.close() - - for error_handler in self.logger_error.handlers: - error_handler.close() + handler.close() + handler_error.close() class CollectionIterator: @@ -296,6 +281,9 @@ def has_more(self): else: return False + def __del__(self): + self.collection.close() + class PatientCollection: """Берет данные из csv файла, поддерживает итерацию From c4dabc099f55018547b8ef6f56b738f193ffbf7c Mon Sep 17 00:00:00 2001 From: Master-Pc Date: Tue, 28 Apr 2020 20:18:28 +0300 Subject: [PATCH 3/8] Second Homework v. 0.55 change created Signed-off-by: Master-Pc --- homework/patient.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homework/patient.py b/homework/patient.py index 66f373e..d49c6ac 100644 --- a/homework/patient.py +++ b/homework/patient.py @@ -221,7 +221,6 @@ class Patient: def __init__(self, first_name, last_name, birth_date, phone, document_type, document_id: str, created=None): - self.exists = False self.first_name = first_name self.last_name = last_name @@ -236,10 +235,9 @@ def __init__(self, first_name, last_name, birth_date, @staticmethod def create(first_name, last_name, birth_date, phone, document_type, document_id): - patient = Patient(first_name, last_name, birth_date, phone, - document_type, document_id, created=True) - patient.logger_info.info(f"{first_name} {last_name} was created") - return patient + logger_info.info(f"{first_name} {last_name} was created") + return Patient(first_name, last_name, birth_date, phone, + document_type, document_id, created=True) def save(self): data = [self.first_name, self.last_name, self.birth_date, @@ -249,7 +247,6 @@ def save(self): self.logger_info.info(f"patient was saved") def __del__(self): - handler.close() handler_error.close() From d18d06df674e9e1d39e043db1e83118bbdabb092 Mon Sep 17 00:00:00 2001 From: Master-Pc Date: Thu, 30 Apr 2020 19:53:29 +0300 Subject: [PATCH 4/8] Second Homework v. 0.56 remove some trash Signed-off-by: Master-Pc --- homework/patient.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/homework/patient.py b/homework/patient.py index d49c6ac..95fc065 100644 --- a/homework/patient.py +++ b/homework/patient.py @@ -36,9 +36,9 @@ def __get__(self, instance, owner): return instance.__dict__[self.name] @staticmethod - def check_type(instance, value): + def check_type(value): if not isinstance(value, str): - instance.logger_error.error(f"Invalid type") + logger_error.error(f"Invalid type") raise TypeError("Not string") @abstractmethod @@ -57,15 +57,15 @@ class StringDescriptor(BaseDescriptor): """ def __set__(self, instance, value): - self.check_type(instance, value) + self.check_type(value) if self.check_name(value): if self.name not in instance.__dict__: instance.__dict__[self.name] = value else: - instance.logger_error.error(f"Changes Forbidden") + logger_error.error(f"Changes Forbidden") raise AttributeError("Changes Forbidden") else: - instance.logger_error.error(f"Incorrect Name/Surname {value}") + logger_error.error(f"Incorrect Name/Surname {value}") raise ValueError("Incorrect Name/Surname") @staticmethod @@ -84,14 +84,14 @@ class DateDescriptor(BaseDescriptor): """ def __set__(self, instance, value): - self.check_type(instance, value) + self.check_type(value) if self.check_date(value): tmp = parse(value) instance.__dict__[self.name] = tmp if instance.exists: - instance.logger_info.info(f"Date was changed ") + logger_info.info(f"Date was changed ") else: - instance.logger_error.error(f"Invalid date: {value}") + logger_error.error(f"Invalid date: {value}") raise ValueError("input not str type") @staticmethod @@ -110,15 +110,14 @@ class PhoneDescriptor(BaseDescriptor): """ def __set__(self, instance, value): - self.check_type(instance, value) + self.check_type(value) number, status = self.check_phone(value) if status: instance.__dict__[self.name] = number if instance.exists: - instance.logger_info.info("Phone was changed") - del instance + logger_info.info("Phone was changed") else: - instance.logger_error.error(f"Invalid number: {value}") + logger_error.error(f"Invalid number: {value}") raise ValueError("Invalid number") @staticmethod @@ -144,26 +143,26 @@ class DocDescriptor(BaseDescriptor): def __set__(self, instance, value): if self.name == "document_id": - self.check_type(instance, value) + self.check_type(value) res, status = self.check_id(value, DOC_TYPE[instance.document_type]) if status: instance.__dict__[self.name] = res if not instance.exists: instance.exists = True else: - instance.logger_info.info("ID was changed") + logger_info.info("ID was changed") else: - instance.logger_error.error(f"Invalid id: {value}") + logger_error.error(f"Invalid id: {value}") raise ValueError("Invalid ID") elif self.name == "document_type": - self.check_type(instance, value) + self.check_type(value) if self.check_doc(value): instance.__dict__[self.name] = value if instance.exists: - instance.logger_info.info("Type was changed") + logger_info.info("Type was changed") else: - instance.logger_error.error(f"Invalid document: {value}") + logger_error.error(f"Invalid document: {value}") raise ValueError("Invalid document") @staticmethod @@ -230,7 +229,7 @@ def __init__(self, first_name, last_name, birth_date, self.document_id = document_id if not created: - self.logger_info.info(f"{first_name} {last_name} was written") + logger_info.info(f"{first_name} {last_name} was written") @staticmethod def create(first_name, last_name, birth_date, phone, @@ -244,7 +243,7 @@ def save(self): self.phone, self.document_type, self.document_id] with open("table.csv", "a", encoding="utf-8") as table: table.write(u",".join(map(str, data)) + u"\n") - self.logger_info.info(f"patient was saved") + logger_info.info(f"patient was saved") def __del__(self): handler.close() From 9380bdc092e29f3256c5349c6f2e3d80b36aa5d0 Mon Sep 17 00:00:00 2001 From: Master-Pc Date: Thu, 30 Apr 2020 20:19:47 +0300 Subject: [PATCH 5/8] Second Homework v. 0.561 remove redundant variables Signed-off-by: Master-Pc --- homework/patient.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/homework/patient.py b/homework/patient.py index 95fc065..6c93c2a 100644 --- a/homework/patient.py +++ b/homework/patient.py @@ -87,9 +87,10 @@ def __set__(self, instance, value): self.check_type(value) if self.check_date(value): tmp = parse(value) - instance.__dict__[self.name] = tmp - if instance.exists: + if self.name in instance.__dict__: logger_info.info(f"Date was changed ") + instance.__dict__[self.name] = tmp + else: logger_error.error(f"Invalid date: {value}") raise ValueError("input not str type") @@ -113,9 +114,9 @@ def __set__(self, instance, value): self.check_type(value) number, status = self.check_phone(value) if status: - instance.__dict__[self.name] = number - if instance.exists: + if self.name in instance.__dict__: logger_info.info("Phone was changed") + instance.__dict__[self.name] = number else: logger_error.error(f"Invalid number: {value}") raise ValueError("Invalid number") @@ -146,11 +147,9 @@ def __set__(self, instance, value): self.check_type(value) res, status = self.check_id(value, DOC_TYPE[instance.document_type]) if status: - instance.__dict__[self.name] = res - if not instance.exists: - instance.exists = True - else: + if self.name in instance.__dict__: logger_info.info("ID was changed") + instance.__dict__[self.name] = res else: logger_error.error(f"Invalid id: {value}") raise ValueError("Invalid ID") @@ -158,9 +157,9 @@ def __set__(self, instance, value): elif self.name == "document_type": self.check_type(value) if self.check_doc(value): - instance.__dict__[self.name] = value - if instance.exists: + if self.name in instance.__dict__: logger_info.info("Type was changed") + instance.__dict__[self.name] = value else: logger_error.error(f"Invalid document: {value}") raise ValueError("Invalid document") @@ -220,7 +219,6 @@ class Patient: def __init__(self, first_name, last_name, birth_date, phone, document_type, document_id: str, created=None): - self.exists = False self.first_name = first_name self.last_name = last_name self.birth_date = birth_date From 588a672477ef68f8bd13da70384cf2cc47379cff Mon Sep 17 00:00:00 2001 From: Master-Pc Date: Thu, 30 Apr 2020 21:01:58 +0300 Subject: [PATCH 6/8] Second Homework v. 0.57 add log decorator Signed-off-by: Master-Pc --- homework/patient.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/homework/patient.py b/homework/patient.py index 6c93c2a..4212dc4 100644 --- a/homework/patient.py +++ b/homework/patient.py @@ -80,7 +80,7 @@ def check_name(value): class DateDescriptor(BaseDescriptor): """ Дата имеет тип datetime. - Исключения логгируем в exceptions + Исключения логгируем в errors """ def __set__(self, instance, value): @@ -181,6 +181,14 @@ def check_doc(doc_type): return True +def my_logging_decorator(method): + def method_wrapper(*args): + result = method(*args) + logger_info.info(f"Patient was {method.__name__}") + return result + return method_wrapper + + class Patient: """ Объект хранит информацию о пациенте @@ -216,9 +224,9 @@ class Patient: logger_info = logging.getLogger("Patient") logger_error = logging.getLogger("Error") + @my_logging_decorator def __init__(self, first_name, last_name, birth_date, - phone, document_type, document_id: str, - created=None): + phone, document_type, document_id: str): self.first_name = first_name self.last_name = last_name self.birth_date = birth_date @@ -226,22 +234,19 @@ def __init__(self, first_name, last_name, birth_date, self.document_type = document_type self.document_id = document_id - if not created: - logger_info.info(f"{first_name} {last_name} was written") @staticmethod def create(first_name, last_name, birth_date, phone, document_type, document_id): - logger_info.info(f"{first_name} {last_name} was created") return Patient(first_name, last_name, birth_date, phone, - document_type, document_id, created=True) + document_type, document_id) + @my_logging_decorator def save(self): data = [self.first_name, self.last_name, self.birth_date, self.phone, self.document_type, self.document_id] with open("table.csv", "a", encoding="utf-8") as table: table.write(u",".join(map(str, data)) + u"\n") - logger_info.info(f"patient was saved") def __del__(self): handler.close() From aeb8e35c88aa70cba478aa03ce29a018cb40f483 Mon Sep 17 00:00:00 2001 From: aleksey sketin Date: Mon, 4 May 2020 12:33:41 +0300 Subject: [PATCH 7/8] Third homework - first version with adapted tests --- __setup__.py | 11 ++ homework/cli.py | 43 +++++++ homework/config.py | 1 + homework/database.ini | 5 + homework/db_config.py | 18 +++ homework/patient.py | 195 ++++++++++++++++++------------- tests/__init__.py | 1 + tests/test_patient.py | 6 +- tests/test_patient_collection.py | 22 ++-- 9 files changed, 205 insertions(+), 97 deletions(-) create mode 100644 __setup__.py create mode 100644 homework/cli.py create mode 100644 homework/database.ini create mode 100644 homework/db_config.py diff --git a/__setup__.py b/__setup__.py new file mode 100644 index 0000000..cf67539 --- /dev/null +++ b/__setup__.py @@ -0,0 +1,11 @@ +from setuptools import setup, find_packages + +setup( + name='third_homework', + version='1.0', + packages=find_packages(), + install_requires = ['click'], + entry_points = { + 'console_scripts': ['cli=cli.cli:cli'] + } +) \ No newline at end of file diff --git a/homework/cli.py b/homework/cli.py new file mode 100644 index 0000000..45b6213 --- /dev/null +++ b/homework/cli.py @@ -0,0 +1,43 @@ +from homework.patient import Patient, db_request + +import click + + +@click.group() +def cli(): + pass + + +@click.command() +@click.argument("name") +@click.argument("surname") +@click.option("--birth-date") +@click.option("--phone") +@click.option("--document-type") +@click.option("--document-number", type=(str, str)) +def create(name, surname, birth_date, + phone, document_type, document_number): + patient = Patient(name, surname, birth_date, phone, + document_type, document_number[0] + document_number[1]) + patient.save() + + +@click.command() +@click.argument("limit", default=10) +def show(limit): + for patient in db_request(f"select * from {Patient.table} limit {limit}", "many"): + print(*patient[1:]) + + +@click.command() +def count(): + result = db_request(f"select Count(*) from {Patient.table}", "one") + print("Amount of stored patients: ", result[0]) + + +cli.add_command(create) +cli.add_command(show) +cli.add_command(count) + +if __name__ == "__main__": + cli() diff --git a/homework/config.py b/homework/config.py index ff2f0e0..0a96cac 100644 --- a/homework/config.py +++ b/homework/config.py @@ -1,6 +1,7 @@ GOOD_LOG_FILE = "info.txt" ERROR_LOG_FILE = "errors.txt" CSV_PATH = "table.csv" +TABLE = "ill_patients" PHONE_FORMAT = "89160000000" # Здесь запишите телефон +7-916-000-00-00 в том формате, в котором вы храните телефоны PASSPORT_TYPE = "паспорт" # тип документа, когда он паспорт diff --git a/homework/database.ini b/homework/database.ini new file mode 100644 index 0000000..314dda0 --- /dev/null +++ b/homework/database.ini @@ -0,0 +1,5 @@ +[postgresql] +host=localhost +database=patients +user=postgres +password=1883 \ No newline at end of file diff --git a/homework/db_config.py b/homework/db_config.py new file mode 100644 index 0000000..2619d57 --- /dev/null +++ b/homework/db_config.py @@ -0,0 +1,18 @@ +from configparser import ConfigParser + + +def config(filename=u"/Users/Master-Pc/PycharmProjects/third_homework/homework/database.ini", + section='postgresql'): + + parser = ConfigParser() + parser.read(filename) + + db = {} + if parser.has_section(section): + params = parser.items(section) + for param in params: + db[param[0]] = param[1] + else: + raise Exception('Section {0} not found in the {1} file'.format(section, filename)) + + return db diff --git a/homework/patient.py b/homework/patient.py index 4212dc4..496331e 100644 --- a/homework/patient.py +++ b/homework/patient.py @@ -1,14 +1,17 @@ from abc import ABC, abstractmethod + +import psycopg2 from dateutil.parser import parse import regex as re import logging -import os from homework.logger import logger_error, logger_info, handler, handler_error +from homework.db_config import config -from homework.config import PHONE_FORMAT, DRIVER_LICENSE_TYPE, DRIVER_LICENSE_FORMAT, PASSPORT_TYPE # лучше вместо глобальных констант, создать структуры с интерфейсом # обновления элементов и форматов +from tests.constants import PATIENT_FIELDS + OPERATORS_CODE = {900, 901, 902, 903, 904, 905, 906, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, @@ -23,22 +26,66 @@ INAPROPRIATE_SYMBOLS = r"[a-zA-Z\u0400-\u04FF.!@?#$%&:;*\,\;\=[\\\]\^_{|}<>]" +def my_logging_decorator(method): + def method_wrapper(*args): + + exist = False + result = None + + if method.__name__ == "__set__": + if args[0]._name in args[1].__dict__: + exist = True + + if method.__name__ == "__init__": + result = method(*args) + logger_info.info(f"Patient was {method.__name__}") + + else: + try: + result = method(*args) + if exist: + logger_info.info(f"{args[0]._name} was changed") + if method.__name__ == "save": + logger_info.info(f"Patient was {method.__name__}") + except (TypeError, ValueError, AttributeError, psycopg2.DatabaseError, + Exception) as e: + logger_error.error(e) + raise e + + return result + + return method_wrapper + + +def db_request(request, amount=None, db="patients"): + params = config() + params["database"] = db + conn = psycopg2.connect(**params) + cur = conn.cursor() + cur.execute(request) + result = None + if amount is not None: + result = cur.fetchone() if amount == "one" else cur.fetchall() + conn.commit() + cur.close() + return result + + class BaseDescriptor(ABC): """ Базовый дескриптор """ def __set_name__(self, owner, name): - self.name = name - self.value = None + self._name = name + self._value = None def __get__(self, instance, owner): - return instance.__dict__[self.name] + return instance.__dict__[self._name] @staticmethod def check_type(value): if not isinstance(value, str): - logger_error.error(f"Invalid type") raise TypeError("Not string") @abstractmethod @@ -51,27 +98,26 @@ class StringDescriptor(BaseDescriptor): Дескриптор данных для first_name, last_name. В случае некорректного формата данных выбрасвает ошибку ValueError, все ошибки логируются в errors. + Изменение имени после инициализация объекта запре- + щено. Формат имени предполагает отсутствие цифр и небуквенных - символов, количество уникальнх символов > 2 + символов """ + @my_logging_decorator def __set__(self, instance, value): self.check_type(value) if self.check_name(value): - if self.name not in instance.__dict__: - instance.__dict__[self.name] = value + if self._name not in instance.__dict__: + instance.__dict__[self._name] = value else: - logger_error.error(f"Changes Forbidden") raise AttributeError("Changes Forbidden") else: - logger_error.error(f"Incorrect Name/Surname {value}") raise ValueError("Incorrect Name/Surname") @staticmethod def check_name(value): - if len(set(value)) < 2: - return False if not value.isalpha(): return False return True @@ -83,16 +129,14 @@ class DateDescriptor(BaseDescriptor): Исключения логгируем в errors """ + @my_logging_decorator def __set__(self, instance, value): self.check_type(value) if self.check_date(value): tmp = parse(value) - if self.name in instance.__dict__: - logger_info.info(f"Date was changed ") - instance.__dict__[self.name] = tmp + instance.__dict__[self._name] = tmp else: - logger_error.error(f"Invalid date: {value}") raise ValueError("input not str type") @staticmethod @@ -110,15 +154,12 @@ class PhoneDescriptor(BaseDescriptor): Исключения логгируем в errors """ + @my_logging_decorator def __set__(self, instance, value): - self.check_type(value) - number, status = self.check_phone(value) - if status: - if self.name in instance.__dict__: - logger_info.info("Phone was changed") - instance.__dict__[self.name] = number + number = self.check_phone(value) + if number is not None: + instance.__dict__[self._name] = number else: - logger_error.error(f"Invalid number: {value}") raise ValueError("Invalid number") @staticmethod @@ -127,12 +168,12 @@ def check_phone(number): res = "8" res += ''.join(parsed_num)[1:] if len(res) != 11: - return None, False + return None if int(res[1:4]) not in OPERATORS_CODE: - return None, False + return None if re.search(INAPROPRIATE_SYMBOLS, number) is not None: - return None, False - return res, True + return None + return res class DocDescriptor(BaseDescriptor): @@ -141,27 +182,20 @@ class DocDescriptor(BaseDescriptor): Содержит проверку для обоих полей """ + @my_logging_decorator def __set__(self, instance, value): - if self.name == "document_id": - self.check_type(value) - res, status = self.check_id(value, DOC_TYPE[instance.document_type]) - if status: - if self.name in instance.__dict__: - logger_info.info("ID was changed") - instance.__dict__[self.name] = res + if self._name == "document_id": + res = self.check_id(value, DOC_TYPE[instance.document_type]) + if res is not None: + instance.__dict__[self._name] = res else: - logger_error.error(f"Invalid id: {value}") raise ValueError("Invalid ID") - elif self.name == "document_type": - self.check_type(value) + elif self._name == "document_type": if self.check_doc(value): - if self.name in instance.__dict__: - logger_info.info("Type was changed") - instance.__dict__[self.name] = value + instance.__dict__[self._name] = value else: - logger_error.error(f"Invalid document: {value}") raise ValueError("Invalid document") @staticmethod @@ -169,10 +203,10 @@ def check_id(number, fix_size): parsed_num = re.findall(r"\d+", number) res = ''.join(parsed_num) if len(res) != fix_size: - return None, False + return None if re.search(INAPROPRIATE_SYMBOLS, number) is not None: - return None, False - return res, True + return None + return res @staticmethod def check_doc(doc_type): @@ -181,14 +215,6 @@ def check_doc(doc_type): return True -def my_logging_decorator(method): - def method_wrapper(*args): - result = method(*args) - logger_info.info(f"Patient was {method.__name__}") - return result - return method_wrapper - - class Patient: """ Объект хранит информацию о пациенте @@ -199,9 +225,7 @@ class Patient: : номер телефона(string) - соответствие формату,хранение в виде 8xxxxxxxxxxx : тип документа(string) - ограниченный набор(паспорт, - удостоверения, прочее), надо реализовать метод - добавления нового документа карантинного пропуска к - примеру + удостоверения, прочее) : номер документа(string) - проверять на соответствие номера формату документа @@ -210,8 +234,8 @@ class Patient: Исключения, случившиеся при работе, в лог errors - Хранить пациента нужно в csv, у класса - есть метод save для дозаписи в файл + Пациент хранится в БД Postgres.Поля database и table + указывают на необходимую таблицу. """ first_name = StringDescriptor() @@ -224,6 +248,9 @@ class Patient: logger_info = logging.getLogger("Patient") logger_error = logging.getLogger("Error") + database = "patients" + table = "ill_patients" + @my_logging_decorator def __init__(self, first_name, last_name, birth_date, phone, document_type, document_id: str): @@ -234,7 +261,6 @@ def __init__(self, first_name, last_name, birth_date, self.document_type = document_type self.document_id = document_id - @staticmethod def create(first_name, last_name, birth_date, phone, document_type, document_id): @@ -243,10 +269,10 @@ def create(first_name, last_name, birth_date, phone, @my_logging_decorator def save(self): - data = [self.first_name, self.last_name, self.birth_date, + data = [self.first_name, self.last_name, self.birth_date.date(), self.phone, self.document_type, self.document_id] - with open("table.csv", "a", encoding="utf-8") as table: - table.write(u",".join(map(str, data)) + u"\n") + data = str(tuple(map(str, data)))[1:-1] + db_request(f"insert into {self.table} values (DEFAULT, {data})") def __del__(self): handler.close() @@ -255,46 +281,49 @@ def __del__(self): class CollectionIterator: - def __init__(self, path, limit=None): - self.collection = open(path, "r", encoding="utf-8") - self.limit = limit - self.line = 0 + def __init__(self, table, limit=None): + self.table = table + first_index = db_request(f"select Min(id) from {self.table}", "one") + self.line = 0 if first_index[0] is None else int(first_index[0]) + self.limit = None if limit is None \ + else self.line + limit def __iter__(self): return self def __next__(self): if self.has_more(): - params = self.collection.readline() + result = db_request(f"select * from {self.table} where id = {self.line}", + "one") self.line += 1 - return Patient(*params.split(",")) + return Patient(*result[1:]) else: raise StopIteration() def has_more(self): - if self.line == self.limit: + last_index = db_request(f"select Max(id) from {self.table}", "one") + if last_index[0] is None or self.line > last_index[0]: return False - if self.collection.tell() != os.fstat( - self.collection.fileno()).st_size: - return True - else: + if self.limit is not None and self.line >= self.limit: return False - - def __del__(self): - self.collection.close() + return True class PatientCollection: - """Берет данные из csv файла, поддерживает итерацию - ссодержит метод limit, возвращаюший итератор/генератор - первых n записей + """ + Берет данные из БД Postgres, поддерживает итерацию + содержит метод limit, возвращаюший итератор/генератор + первых n записей.В поле self.table указывают необходимую + таблицу. """ - def __init__(self, path): - self.path = path + def __init__(self, table): + self.table = table + @my_logging_decorator def __iter__(self): - return CollectionIterator(self.path) + return CollectionIterator(self.table) + @my_logging_decorator def limit(self, n): - return CollectionIterator(self.path, n) + return CollectionIterator(self.table, n) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..8b13789 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/test_patient.py b/tests/test_patient.py index a4d1b58..5edfa0a 100644 --- a/tests/test_patient.py +++ b/tests/test_patient.py @@ -2,12 +2,11 @@ import os from datetime import datetime import itertools - import pytest from homework.config import GOOD_LOG_FILE, ERROR_LOG_FILE, CSV_PATH, PHONE_FORMAT, PASSPORT_TYPE, PASSPORT_FORMAT, \ INTERNATIONAL_PASSPORT_FORMAT, INTERNATIONAL_PASSPORT_TYPE, DRIVER_LICENSE_TYPE, DRIVER_LICENSE_FORMAT -from homework.patient import Patient +from homework.patient import Patient, db_request from tests.constants import GOOD_PARAMS, OTHER_GOOD_PARAMS, WRONG_PARAMS, PATIENT_FIELDS @@ -42,6 +41,7 @@ def setup_module(__main__): def teardown_module(__name__): for file in [GOOD_LOG_FILE, ERROR_LOG_FILE, CSV_PATH]: + db_request("truncate ill_patients") os.remove(file) @@ -193,7 +193,7 @@ def test_wrong_value_assignment(patient, field, param): # метод save -@check_log_size("csv", increased=True) +@check_log_size("good", increased=True) def test_save(): patient = Patient(*GOOD_PARAMS) patient.save() diff --git a/tests/test_patient_collection.py b/tests/test_patient_collection.py index 9536eff..afda846 100644 --- a/tests/test_patient_collection.py +++ b/tests/test_patient_collection.py @@ -2,8 +2,8 @@ import pytest -from homework.config import PASSPORT_TYPE, CSV_PATH -from homework.patient import PatientCollection, Patient +from homework.config import PASSPORT_TYPE, TABLE +from homework.patient import PatientCollection, Patient, db_request from tests.constants import PATIENT_FIELDS GOOD_PARAMS = ( @@ -25,26 +25,25 @@ @pytest.fixture() def prepare(): - with open(CSV_PATH, 'w', encoding='utf-8') as f: - f.write('') + db_request("truncate ill_patients") for params in GOOD_PARAMS: Patient(*params).save() yield - os.remove(CSV_PATH) @pytest.mark.usefixtures('prepare') def test_collection_iteration(): - collection = PatientCollection(CSV_PATH) + collection = PatientCollection(TABLE) for i, patient in enumerate(collection): true_patient = Patient(*GOOD_PARAMS[i]) for field in PATIENT_FIELDS: assert getattr(patient, field) == getattr(true_patient, field), f"Wrong attr {field} for {GOOD_PARAMS[i]}" + @pytest.mark.usefixtures('prepare') def test_limit_usual(): - collection = PatientCollection(CSV_PATH) + collection = PatientCollection(TABLE) try: len(collection.limit(8)) assert False, "Iterator should not have __len__ method" @@ -56,9 +55,10 @@ def test_limit_usual(): assert getattr(patient, field) == getattr(true_patient, field), f"Wrong attr {field} for {GOOD_PARAMS[i]} in limit" + @pytest.mark.usefixtures('prepare') def test_limit_add_record(): - collection = PatientCollection(CSV_PATH) + collection = PatientCollection(TABLE) limit = collection.limit(len(GOOD_PARAMS) + 10) for _ in range(len(GOOD_PARAMS)): next(limit) @@ -71,8 +71,8 @@ def test_limit_add_record(): @pytest.mark.usefixtures('prepare') def test_limit_remove_records(): - collection = PatientCollection(CSV_PATH) + collection = PatientCollection("patients") limit = collection.limit(4) - with open(CSV_PATH, 'w', encoding='utf-8') as f: - f.write('') + db_request("truncate patients") + a = [_ for _ in limit] assert len([_ for _ in limit]) == 0, "Limit works wrong for empty file" From 0bd81fdbe92840f068c14576818fdead3b5e669e Mon Sep 17 00:00:00 2001 From: Master-Pc Date: Mon, 11 May 2020 17:48:29 +0300 Subject: [PATCH 8/8] Third homework - improve log decorator by adding decorator maker --- homework/patient.py | 65 +++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/homework/patient.py b/homework/patient.py index 496331e..e406d8c 100644 --- a/homework/patient.py +++ b/homework/patient.py @@ -7,7 +7,6 @@ from homework.logger import logger_error, logger_info, handler, handler_error from homework.db_config import config - # лучше вместо глобальных констант, создать структуры с интерфейсом # обновления элементов и форматов from tests.constants import PATIENT_FIELDS @@ -26,35 +25,37 @@ INAPROPRIATE_SYMBOLS = r"[a-zA-Z\u0400-\u04FF.!@?#$%&:;*\,\;\=[\\\]\^_{|}<>]" -def my_logging_decorator(method): - def method_wrapper(*args): +def my_logging_decorator(parameter): + def new_decorator(method): + def method_wrapper(*args): + msg = None + + if parameter == "linked": + method(*args) + logger_info.info(f"Patient was {method.__name__}") - exist = False - result = None + else: - if method.__name__ == "__set__": - if args[0]._name in args[1].__dict__: - exist = True + if parameter == "set": + Descriptor, Patient = args[0], args[1] + if Descriptor._name in Patient.__dict__: + msg = f"{Descriptor._name} was changed" - if method.__name__ == "__init__": - result = method(*args) - logger_info.info(f"Patient was {method.__name__}") + if parameter == "unlinked": + msg = f"Patient was {method.__name__}" - else: - try: - result = method(*args) - if exist: - logger_info.info(f"{args[0]._name} was changed") - if method.__name__ == "save": - logger_info.info(f"Patient was {method.__name__}") - except (TypeError, ValueError, AttributeError, psycopg2.DatabaseError, - Exception) as e: - logger_error.error(e) - raise e + try: + result = method(*args) + if msg is not None: + logger_info.info(msg) + return result + except Exception as e: + logger_error.error(e) + raise e - return result + return method_wrapper - return method_wrapper + return new_decorator def db_request(request, amount=None, db="patients"): @@ -105,7 +106,7 @@ class StringDescriptor(BaseDescriptor): символов """ - @my_logging_decorator + @my_logging_decorator("set") def __set__(self, instance, value): self.check_type(value) if self.check_name(value): @@ -129,7 +130,7 @@ class DateDescriptor(BaseDescriptor): Исключения логгируем в errors """ - @my_logging_decorator + @my_logging_decorator("set") def __set__(self, instance, value): self.check_type(value) if self.check_date(value): @@ -154,7 +155,7 @@ class PhoneDescriptor(BaseDescriptor): Исключения логгируем в errors """ - @my_logging_decorator + @my_logging_decorator("set") def __set__(self, instance, value): number = self.check_phone(value) if number is not None: @@ -182,7 +183,7 @@ class DocDescriptor(BaseDescriptor): Содержит проверку для обоих полей """ - @my_logging_decorator + @my_logging_decorator("set") def __set__(self, instance, value): if self._name == "document_id": @@ -251,7 +252,7 @@ class Patient: database = "patients" table = "ill_patients" - @my_logging_decorator + @my_logging_decorator("linked") def __init__(self, first_name, last_name, birth_date, phone, document_type, document_id: str): self.first_name = first_name @@ -267,7 +268,7 @@ def create(first_name, last_name, birth_date, phone, return Patient(first_name, last_name, birth_date, phone, document_type, document_id) - @my_logging_decorator + @my_logging_decorator("unlinked") def save(self): data = [self.first_name, self.last_name, self.birth_date.date(), self.phone, self.document_type, self.document_id] @@ -320,10 +321,10 @@ class PatientCollection: def __init__(self, table): self.table = table - @my_logging_decorator + @my_logging_decorator("unlinked") def __iter__(self): return CollectionIterator(self.table) - @my_logging_decorator + @my_logging_decorator("unlinked") def limit(self, n): return CollectionIterator(self.table, n)