From d12360d4bc4c76108537a6fb0e5c780c11f517ef Mon Sep 17 00:00:00 2001 From: Frumka Date: Thu, 30 Apr 2020 01:23:59 +0300 Subject: [PATCH 1/6] patient somewhat done --- homework/config.py | 8 +- homework/patient.py | 94 ++++++++- homework/patient_attribute_descriptors.py | 231 ++++++++++++++++++++++ homework/patient_logger.py | 19 ++ 4 files changed, 343 insertions(+), 9 deletions(-) create mode 100644 homework/patient_attribute_descriptors.py create mode 100644 homework/patient_logger.py diff --git a/homework/config.py b/homework/config.py index 955b991..484868d 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 в том формате, в котором вы храните телефоны +CSV_PATH = "patients.csv" +PHONE_FORMAT = "7 916 000 00 00" # Здесь запишите телефон +7-916-000-00-00 в том формате, в котором вы храните телефоны PASSPORT_TYPE = "паспорт" # тип документа, когда он паспорт PASSPORT_FORMAT = "0000 000000" # Здесь запишите номер парспорта 0000 000000 в том формате, в котором вы его храните -INTERNATIONAL_PASSPORT_TYPE = "заграничный паспорт" # тип документа, если это загран +INTERNATIONAL_PASSPORT_TYPE = "загран" # тип документа, если это загран INTERNATIONAL_PASSPORT_FORMAT = "00 0000000" # формат хранения заграна для номера 00 0000000 -DRIVER_LICENSE_TYPE = "водительское удостоверение" # тип документа, если это водительское удостоверение +DRIVER_LICENSE_TYPE = "водительские права" # тип документа, если это водительское удостоверение DRIVER_LICENSE_FORMAT = "00 00 000000" # формат хранения номера ВУ diff --git a/homework/patient.py b/homework/patient.py index dad2526..0bde998 100644 --- a/homework/patient.py +++ b/homework/patient.py @@ -1,13 +1,96 @@ +from homework.patient_attribute_descriptors import * +import csv +from homework.patient_logger import * + +CSV_LOG_NAME = 'patients.csv' + + +class Document: + document_type: str + document_type = DocumentTypeDescriptor() + + document_id: str + document_id = DocumentIdDescriptor() + + def __init__(self, document_type, document_id): + self.success_logger = logging.getLogger("patient_success") + self.error_logger = logging.getLogger("patient_errors") + + self.document_type = document_type + self.document_id = document_id + + class Patient: - def __init__(self, *args, **kwargs): - pass + first_name: str + first_name = NameNoModifyDescriptor() - def create(*args, **kwargs): - raise NotImplementedError() + last_name: str + last_name = NameNoModifyDescriptor() + + birth_date: str + birth_date = DateDescriptor() + + phone: str + phone = PhoneDescriptor() + + document_type: str + document_type = DocumentReadOnlyDescriptor() + + document_id: str + document_id = DocumentReadOnlyDescriptor() + + __document: Document + + def __init__(self, first_name, last_name, birth_date, phone, document_type, document_id): + self.success_logger = logging.getLogger("patient_success") + self.error_logger = logging.getLogger("patient_errors") + + self.first_name = first_name + self.last_name = last_name + self.birth_date = birth_date + self.phone = phone + self.__document = Document(document_type, document_id) + + @staticmethod + def create(first_name, last_name, birth_date, phone, document_type, document_id): + return Patient(first_name, last_name, birth_date, phone, document_type, document_id) + + def update_document(self, document_type, document_id): + self.__document.document_type = document_type + self.__document.document_id = document_id + + def __str__(self): + return ', '.join([self.first_name, self.last_name, self.birth_date, self.phone, + self.document_type, self.document_id]) def save(self): - pass + data = [self.first_name, self.last_name, self.birth_date, self.phone, self.document_type, self.document_id] + try: + with open(CSV_LOG_NAME, "a", newline="", encoding='utf-8') as file: + csv.writer(file).writerow(data) + + except UnicodeError: + message = 'Problem with encoding in file {}'.format(CSV_LOG_NAME) + self.error_logger.error(message) + raise UnicodeError(message) + + except PermissionError: + message = "Permission required to write to file" + self.error_logger.error(message) + raise PermissionError(message) + + except RuntimeError: + message = "Error in Runtime while saving patient" + self.error_logger.error(message) + raise RuntimeError(message) + + else: + self.success_logger.info("Patient {} successfully saved".format(self.first_name + ' ' + self.last_name)) + + def __del__(self): + success_handler.close() + error_handler.close() class PatientCollection: def __init__(self, log_file): @@ -15,3 +98,4 @@ def __init__(self, log_file): def limit(self, n): raise NotImplementedError() + diff --git a/homework/patient_attribute_descriptors.py b/homework/patient_attribute_descriptors.py new file mode 100644 index 0000000..23bd31a --- /dev/null +++ b/homework/patient_attribute_descriptors.py @@ -0,0 +1,231 @@ +import re +from collections.abc import Sized, Iterable + +MIN_LENGTH_OF_PHONE_NUMBER = 11 # digits only, for 1-digit country codes +MAX_LENGTH_OF_PHONE_NUMBER = 14 # digits only, for 4-digit country codes + + +def sum_of_lens(iterable: Iterable): + """Returns number of elements in 1/1.5/2 dim list""" + return sum(map(lambda x: len(x) if isinstance(x, Sized) else 1, iterable)) + + +class ModifyError(Exception): + def __init__(self, text=None): + self.txt = text + + +class NameNoModifyDescriptor: + def __set_name__(self, owner, name): + self.name = name + + def __get__(self, instance, owner): + return instance.__dict__[self.name] + + def __set__(self, instance, value): + if self.name in instance.__dict__: + # Не уверен, что так стоит работать с персональными данными, но для примера пусть будет + message = "Tried modifying {} of patient: {}".format(self.name, str(instance)) + instance.error_logger.error(message) + raise ModifyError(message) + + if not isinstance(value, str): + message = "Argument {} is not an instance of str".format(value) + instance.error_logger.error(message) + raise TypeError(message) + + if not value.isalpha(): + message = "Argument {} is in unsupported form".format(value) + instance.error_logger.error(message) + raise ValueError(message) + + instance.__dict__[self.name] = value.capitalize() + instance.success_logger.info("{} assigned as {}".format(self.name, instance.__dict__[self.name])) + + +class DateDescriptor: + @staticmethod + def is_date(value: str): + """Check if value follows format of date: yyyy/mm/dd for any regular separators""" + + if len(value) != 10: + return False + + blocks = re.findall(r"[\w']+", value) + + if list(map(lambda x: len(x), blocks)) != [4, 2, 2]: + return False + + for e in blocks: + if not e.isdigit(): + return False + + return True + + def __set_name__(self, owner, name): + self.name = name + + def __get__(self, instance, owner): + return instance.__dict__[self.name] + + def __set__(self, instance, value): + if not isinstance(value, str): + message = "Argument {} is not an instance of str".format(value) + instance.error_logger.error(message) + raise TypeError(message) + + if not self.is_date(value): + message = "Argument {} is in unsupported form".format(value) + instance.error_logger.error(message) + raise ValueError(message) + + instance.__dict__[self.name] = value + instance.success_logger.info("{} assigned as {}".format(self.name, instance.__dict__[self.name])) + + +class PhoneDescriptor: + @staticmethod + def is_phone_number(value: str): + """Check if value contain number of digits from MIN_LENGTH_OF_PHONE_NUMBER to MAX_LENGTH_OF_PHONE_NUMBER""" + + blocks_of_digits = re.findall(r"[1-9]+", str(value)) + number_of_digits = sum_of_lens(blocks_of_digits) + + return MIN_LENGTH_OF_PHONE_NUMBER <= number_of_digits <= MAX_LENGTH_OF_PHONE_NUMBER + + @staticmethod + def cast_phone_to_format(value: str): + """Cast value to format ddd ddd dd dd + - country code from 1 digit up to 4""" + value = re.sub(r"[\\ +._-]", "", value) + + code_len = len(value) - 10 + + if code_len == 1 and value[0] == '8': + value = '7' + value[0:] + + return ' '.join([value[0: code_len], value[code_len: code_len + 3], + value[code_len + 3: code_len + 6], value[code_len + 6: code_len + 8], + value[code_len + 8: code_len + 10], value[code_len + 10:]]) + + def __set_name__(self, owner, name): + self.name = name + + def __get__(self, instance, owner): + return instance.__dict__[self.name] + + def __set__(self, instance, value): + if type(value) != str: + message = "Argument {} is not an instance of str".format(value) + instance.error_logger.error(message) + raise TypeError(message) + + if not self.is_phone_number(value): + message = "Argument {} is in unsupported form".format(value) + instance.error_logger.error(message) + raise ValueError(message) + + instance.__dict__[self.name] = self.cast_phone_to_format(value) + instance.success_logger.info("{} assigned as {}".format(self.name, instance.__dict__[self.name])) + + +class DocumentReadOnlyDescriptor: + def __set_name__(self, owner, name): + self.name = name + + def __get__(self, instance, owner): + return instance.__dict__['_Patient__document'].__dict__[self.name] + + def __set__(self, instance, value): + # Ибо менять тип не меняя номера - странно(да и наоборот иногда тоже) + raise ModifyError("Use Patient.update_document(...) to update document information") + + +class DocumentTypeDescriptor: + def __set_name__(self, owner, name): + self.name = name + + def __get__(self, instance, owner): + return instance.__dict__[self.name] + + def __set__(self, instance, value): + allowed_types = ["паспорт", "загран", "водительские права"] + + if not isinstance(value, str): + message = "Argument {} is not an instance of str".format(value) + instance.error_logger.error(message) + raise TypeError(message) + + value = value.lower() + + if value not in allowed_types: + message = "Argument {} is in unsupported form".format(value) + instance.error_logger.error(message) + raise ValueError(message) + + instance.__dict__[self.name] = value + instance.success_logger.info("{} assigned as {}".format(self.name, instance.__dict__[self.name])) + + +def all_digits_to_str(value): + """From iterable with digits and other stuff form string of only digits""" + + digits = "" + for e in value: + if e.isdigit(): + digits += e + + return digits + + +def cast_to_passport(number: str): + """ Cast number with correct number of digits to PASSPORT_FORMAT = '0000 000000' """ + digits = all_digits_to_str(number) + return ' '.join((digits[:4], digits[4:])) + + +def cast_to_international_passport(number: str): + """ Cast number with correct number of digits to INTERNATIONAL_PASSPORT_FORMAT = '00 0000000' """ + digits = all_digits_to_str(number) + return ' '.join((digits[:2], digits[2:])) + + +def cast_to_driver_licence(number: str): + """ Cast number with correct number of digits to DRIVER_LICENSE_FORMAT = "00 00 000000" """ + digits = all_digits_to_str(number) + return ' '.join((digits[:2], digits[2:4], digits[4:])) + + +def cast_to_document_type(number: str, document_type: str): + """ Calls cast that fits type""" + if document_type == "паспорт": + return cast_to_passport(number) + + if document_type == "загран": + return cast_to_international_passport(number) + + if document_type == "водительские права": + return cast_to_driver_licence(number) + + +class DocumentIdDescriptor: + def __set_name__(self, owner, name): + self.name = name + + def __get__(self, instance, owner): + return instance.__dict__[self.name] + + def __set__(self, instance, value): + types_len = {"паспорт": 10, "загран": 9, "водительские права": 10} # Все по тестам + document_type = instance.__dict__['document_type'] + + blocks_of_digits = re.findall(r"[0-9]+", str(value)) + number_of_digits = sum_of_lens(blocks_of_digits) + + if types_len[document_type] != number_of_digits: + message = "Argument {} is in unsupported form".format(value) + instance.error_logger.error(message) + raise ValueError(message) + + instance.__dict__[self.name] = cast_to_document_type(value, document_type) + instance.success_logger.info("{} assigned as {}".format(self.name, instance.__dict__[self.name])) diff --git a/homework/patient_logger.py b/homework/patient_logger.py new file mode 100644 index 0000000..c3ba54d --- /dev/null +++ b/homework/patient_logger.py @@ -0,0 +1,19 @@ +import logging + +success_logger = logging.getLogger("patient_success") +success_logger.setLevel(logging.INFO) + +error_logger = logging.getLogger("patient_errors") +error_logger.setLevel(logging.ERROR) + +formatter = logging.Formatter("%(filename)s[LINE:%(lineno)d]# %(levelname)-8s [%(asctime)s] %(message)s") + +success_handler = logging.FileHandler("good_log.txt", 'a', 'utf-8') + +error_handler = logging.FileHandler("error_log.txt", 'a', 'utf-8') + +success_handler.setFormatter(formatter) +error_handler.setFormatter(formatter) + +success_logger.addHandler(success_handler) +error_logger.addHandler(error_handler) \ No newline at end of file From 7a7151460c468662539cee7fea180c77b24dfd62 Mon Sep 17 00:00:00 2001 From: Frumka Date: Thu, 30 Apr 2020 01:26:32 +0300 Subject: [PATCH 2/6] tests manual update --- tests/test_patient.py | 195 ------------------------------- tests/test_patient_collection.py | 18 +-- 2 files changed, 9 insertions(+), 204 deletions(-) delete mode 100644 tests/test_patient.py diff --git a/tests/test_patient.py b/tests/test_patient.py deleted file mode 100644 index 125bd59..0000000 --- a/tests/test_patient.py +++ /dev/null @@ -1,195 +0,0 @@ -import functools -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 tests.constants import GOOD_PARAMS, OTHER_GOOD_PARAMS, WRONG_PARAMS, PATIENT_FIELDS - - -def get_len(file): - with open(file, encoding='utf-8') as f: - return len(f.readlines()) - - -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) - result = func(*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 - - -def setup(): - for file in [GOOD_LOG_FILE, ERROR_LOG_FILE, CSV_PATH]: - with open(file, 'w', encoding='utf-8') as f: - f.write('') - - -def teardown(): - for file in [GOOD_LOG_FILE, ERROR_LOG_FILE, CSV_PATH]: - os.remove(file) - - -@check_log_size("error") -@check_log_size("good", increased=True) -def test_creation_all_good_params(): - patient = Patient("Кондрат", "Коловрат", "1978-01-31", PHONE_FORMAT, PASSPORT_TYPE, PASSPORT_FORMAT) - assert patient.first_name == "Кондрат", "Wrong attribute first_name" - assert patient.last_name == "Коловрат", "Wrong attribute last_name" - assert patient.birth_date == "1978-01-31" or patient.birth_date == datetime(1978, 1, 31), \ - "Wrong attribute birth_date" - assert patient.phone == PHONE_FORMAT, "Wrong attribute phone" - assert patient.document_type == PASSPORT_TYPE, "Wrong attribute document_type" - assert patient.document_id == PASSPORT_FORMAT, "Wrong attribute document_id" - - patient = Patient("Кондрат", "Коловрат", "1978-01-31", PHONE_FORMAT, INTERNATIONAL_PASSPORT_TYPE, - INTERNATIONAL_PASSPORT_FORMAT) - assert patient.document_type == INTERNATIONAL_PASSPORT_TYPE, "Wrong attribute document_type" - assert patient.document_id == INTERNATIONAL_PASSPORT_FORMAT, "Wrong attribute document_id" - - patient = Patient("Кондрат", "Коловрат", "1978-01-31", PHONE_FORMAT, DRIVER_LICENSE_TYPE, DRIVER_LICENSE_FORMAT) - assert patient.document_type == DRIVER_LICENSE_TYPE, "Wrong attribute document_type" - assert patient.document_id == DRIVER_LICENSE_FORMAT, "Wrong attribute document_id" - - -@pytest.mark.parametrize('default,new', itertools.permutations(["(916)", "916", "-916-"], 2)) -@check_log_size("error") -@check_log_size("good", increased=True) -def test_creation_acceptable_phone(default, new): - patient = Patient("Кондрат", "Коловрат", "1978-01-31", PHONE_FORMAT.replace(default, new), - PASSPORT_TYPE, PASSPORT_FORMAT) - assert patient.phone == PHONE_FORMAT, "Wrong attribute phone" - - -@pytest.mark.parametrize('passport', ("00 00 000 000", "0000-000000", "0 0 0 0 0 0 0 0 0 0", "0000/000-000")) -@check_log_size("error") -@check_log_size("good", increased=True) -def test_creation_acceptable_passport(passport): - patient = Patient("Кондрат", "Коловрат", "1978-01-31", PHONE_FORMAT, PASSPORT_TYPE, passport) - assert patient.document_id == PASSPORT_FORMAT, "Wrong attribute document_id" - - -@pytest.mark.parametrize('passport', ("00 0000000", "00-0000000", "0 0 0 0 0 0 0 0 0", "00/000-0000")) -@check_log_size("error") -@check_log_size("good", increased=True) -def test_creation_acceptable_international_passport(passport): - patient = Patient("Кондрат", "Коловрат", "1978-01-31", PHONE_FORMAT, INTERNATIONAL_PASSPORT_TYPE, passport) - assert patient.document_id == INTERNATIONAL_PASSPORT_FORMAT, "Wrong attribute document_id" - - -@pytest.mark.parametrize('driver_license', ("00 00 000 000", "0000-000000", "0 0 0 0 0 0 0 0 0 0", "0000/000000")) -@check_log_size("error") -@check_log_size("good", increased=True) -def test_creation_acceptable_driver_license(driver_license): - patient = Patient("Кондрат", "Коловрат", "1978-01-31", PHONE_FORMAT, DRIVER_LICENSE_TYPE, driver_license) - assert patient.document_id == DRIVER_LICENSE_FORMAT, "Wrong attribute document_id" - - -# неверный тип -@pytest.mark.parametrize("i", list(range(len(GOOD_PARAMS)))) -@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 - - -# неверные значения -@pytest.mark.parametrize("i", list(range(len(GOOD_PARAMS)))) -@check_log_size("error", increased=True) -@check_log_size("good") -def test_creation_wrong_params(i): - try: - 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 - - -# метод create -@check_log_size("error") -@check_log_size("good", increased=True) -def test_create_method_good_params(): - patient = Patient.create(*GOOD_PARAMS) - for param, field in zip(GOOD_PARAMS, PATIENT_FIELDS): - assert getattr(patient, field) in (param, datetime(1978, 1, 31)), f"Wrong attribute {field}" - - -# обновление параметров -@pytest.mark.parametrize("patient,field,param", zip( - [Patient(*OTHER_GOOD_PARAMS)] * len(PATIENT_FIELDS[:2]), - PATIENT_FIELDS[:2], - GOOD_PARAMS[:2] -)) -@check_log_size("error", increased=True) -@check_log_size("good") -def test_names_assignment(patient, field, param): - try: - setattr(patient, field, param) - assert False, f"Attribute error should be invoked for {field} changing" - except AttributeError: - assert True - - -@pytest.mark.parametrize("patient,field,param", zip( - [Patient(*OTHER_GOOD_PARAMS)] * len(PATIENT_FIELDS[2:]), - PATIENT_FIELDS[2:], - GOOD_PARAMS[2:] -)) -@check_log_size("error") -@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" - - -@pytest.mark.parametrize("patient,field,param", zip( - [Patient(*OTHER_GOOD_PARAMS)] * len(PATIENT_FIELDS[2:]), - PATIENT_FIELDS[2:], - [1.4] * len(PATIENT_FIELDS[2:]) -)) -@check_log_size("error", increased=True) -@check_log_size("good") -def test_wrong_type_assignment(patient, field, param): - try: - setattr(patient, field, param) - assert False, f"TypeError for {field} assignment not invoked" - except TypeError: - assert True - - -@pytest.mark.parametrize("patient,field,param", zip( - [Patient(*OTHER_GOOD_PARAMS)] * len(PATIENT_FIELDS[2:]), - PATIENT_FIELDS[2:], - WRONG_PARAMS[2:] -)) -@check_log_size("error", increased=True) -@check_log_size("good") -def test_wrong_type_assignment(patient, field, param): - try: - setattr(patient, field, param) - assert False, f"ValueError for {field} assignment not invoked" - except ValueError: - assert True - - -# метод save -@check_log_size("csv", 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 d1036e5..0099d1f 100644 --- a/tests/test_patient_collection.py +++ b/tests/test_patient_collection.py @@ -7,17 +7,17 @@ from tests.constants import PATIENT_FIELDS GOOD_PARAMS = ( - ("Кондрат", "Рюрик", "1971-01-31", "79160000000", PASSPORT_TYPE, "0228 000000"), - ("Евпатий", "Коловрат", "1972-01-31", "79160000001", PASSPORT_TYPE, "0228 000001"), + ("Кондрат", "Рюрик", "1971-01-11", "79160000000", PASSPORT_TYPE, "0228 000000"), + ("Евпатий", "Коловрат", "1972-01-11", "79160000001", PASSPORT_TYPE, "0228 000001"), ("Ада", "Лавлейс", "1978-01-21", "79160000002", PASSPORT_TYPE, "0228 000002"), - ("Миртл", "Плакса", "1880-01-31", "79160000003", PASSPORT_TYPE, "0228 000003"), - ("Евлампия", "Фамилия", "1999-01-31", "79160000004", PASSPORT_TYPE, "0228 000004"), - ("Кузя", "Кузьмин", "2000-01-31", "79160000005", PASSPORT_TYPE, "0228 000005"), - ("Гарри", "Поттер", "2020-01-31", "79160000006", PASSPORT_TYPE, "0228 000006"), - ("Рон", "Уизли", "1900-04-31", "79160000007", PASSPORT_TYPE, "0228 000007"), + ("Миртл", "Плакса", "1880-01-11", "79160000003", PASSPORT_TYPE, "0228 000003"), + ("Евлампия", "Фамилия", "1999-01-21", "79160000004", PASSPORT_TYPE, "0228 000004"), + ("Кузя", "Кузьмин", "2000-01-21", "79160000005", PASSPORT_TYPE, "0228 000005"), + ("Гарри", "Поттер", "2020-01-11", "79160000006", PASSPORT_TYPE, "0228 000006"), + ("Рон", "Уизли", "1900-04-20", "79160000007", PASSPORT_TYPE, "0228 000007"), ("Билл", "Гейтс", "1978-12-31", "79160000008", PASSPORT_TYPE, "0228 000008"), ("Владимир", "Джугашвили", "1912-01-31", "79160000009", PASSPORT_TYPE, "0228 000009"), - ("Вован", "ДеМорт", "1978-11-31", "79160000010", PASSPORT_TYPE, "0228 000010"), + ("Вован", "ДеМорт", "1978-11-30", "79160000010", PASSPORT_TYPE, "0228 000010"), ("Гопник", "Районный", "1978-01-25", "79160000011", PASSPORT_TYPE, "0228 000011"), ("Фёдор", "Достоевский", "1978-01-05", "79160000012", PASSPORT_TYPE, "0228 000012"), ) @@ -75,4 +75,4 @@ def test_limit_remove_records(): limit = collection.limit(4) with open(CSV_PATH, 'w', encoding='utf-8') as f: f.write('') - assert len([_ for _ in limit]) == 0, "Limit works wrong for empty file" + assert len([_ for _ in limit]) == 0, "Limit works wrong for empty file" \ No newline at end of file From 4f9cbd21fa5f43f908f1016745f79ef18040b4ff Mon Sep 17 00:00:00 2001 From: Frumka Date: Thu, 30 Apr 2020 02:11:47 +0300 Subject: [PATCH 3/6] patient rly done --- homework/patient.py | 42 +++++++++--------- homework/patient_attribute_descriptors.py | 53 ++++++++++++++++++----- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/homework/patient.py b/homework/patient.py index 0bde998..66e54b1 100644 --- a/homework/patient.py +++ b/homework/patient.py @@ -5,19 +5,19 @@ CSV_LOG_NAME = 'patients.csv' -class Document: - document_type: str - document_type = DocumentTypeDescriptor() - - document_id: str - document_id = DocumentIdDescriptor() - - def __init__(self, document_type, document_id): - self.success_logger = logging.getLogger("patient_success") - self.error_logger = logging.getLogger("patient_errors") - - self.document_type = document_type - self.document_id = document_id +# class Document: +# document_type: str +# document_type = DocumentTypeDescriptor() +# +# document_id: str +# document_id = DocumentIdDescriptor() +# +# def __init__(self, document_type, document_id): +# self.success_logger = logging.getLogger("patient_success") +# self.error_logger = logging.getLogger("patient_errors") +# +# self.document_type = document_type +# self.document_id = document_id class Patient: @@ -34,12 +34,10 @@ class Patient: phone = PhoneDescriptor() document_type: str - document_type = DocumentReadOnlyDescriptor() + document_type = DocumentTypeDescriptor() document_id: str - document_id = DocumentReadOnlyDescriptor() - - __document: Document + document_id = DocumentIdDescriptor() def __init__(self, first_name, last_name, birth_date, phone, document_type, document_id): self.success_logger = logging.getLogger("patient_success") @@ -49,16 +47,14 @@ def __init__(self, first_name, last_name, birth_date, phone, document_type, docu self.last_name = last_name self.birth_date = birth_date self.phone = phone - self.__document = Document(document_type, document_id) + self.document_type = document_type + self.document_id = document_id + self.success_logger.info("Patient {} successfully created".format(self.first_name + ' ' + self.last_name)) @staticmethod def create(first_name, last_name, birth_date, phone, document_type, document_id): return Patient(first_name, last_name, birth_date, phone, document_type, document_id) - def update_document(self, document_type, document_id): - self.__document.document_type = document_type - self.__document.document_id = document_id - def __str__(self): return ', '.join([self.first_name, self.last_name, self.birth_date, self.phone, self.document_type, self.document_id]) @@ -92,6 +88,7 @@ def __del__(self): success_handler.close() error_handler.close() + class PatientCollection: def __init__(self, log_file): pass @@ -99,3 +96,4 @@ def __init__(self, log_file): def limit(self, n): raise NotImplementedError() + diff --git a/homework/patient_attribute_descriptors.py b/homework/patient_attribute_descriptors.py index 23bd31a..7ed3209 100644 --- a/homework/patient_attribute_descriptors.py +++ b/homework/patient_attribute_descriptors.py @@ -5,6 +5,11 @@ MAX_LENGTH_OF_PHONE_NUMBER = 14 # digits only, for 4-digit country codes +def log_update(self, instance, value): + full_name = instance.__dict__['first_name'] + ' ' + instance.__dict__['last_name'] + instance.success_logger.info("{} updated to {} for patient {}".format(self.name, value, full_name)) + + def sum_of_lens(iterable: Iterable): """Returns number of elements in 1/1.5/2 dim list""" return sum(map(lambda x: len(x) if isinstance(x, Sized) else 1, iterable)) @@ -27,7 +32,7 @@ def __set__(self, instance, value): # Не уверен, что так стоит работать с персональными данными, но для примера пусть будет message = "Tried modifying {} of patient: {}".format(self.name, str(instance)) instance.error_logger.error(message) - raise ModifyError(message) + raise AttributeError(message) if not isinstance(value, str): message = "Argument {} is not an instance of str".format(value) @@ -40,7 +45,6 @@ def __set__(self, instance, value): raise ValueError(message) instance.__dict__[self.name] = value.capitalize() - instance.success_logger.info("{} assigned as {}".format(self.name, instance.__dict__[self.name])) class DateDescriptor: @@ -79,8 +83,12 @@ def __set__(self, instance, value): instance.error_logger.error(message) raise ValueError(message) + write_to_log = self.name in instance.__dict__ + instance.__dict__[self.name] = value - instance.success_logger.info("{} assigned as {}".format(self.name, instance.__dict__[self.name])) + + if write_to_log: + log_update(self, instance, value) class PhoneDescriptor: @@ -88,7 +96,7 @@ class PhoneDescriptor: def is_phone_number(value: str): """Check if value contain number of digits from MIN_LENGTH_OF_PHONE_NUMBER to MAX_LENGTH_OF_PHONE_NUMBER""" - blocks_of_digits = re.findall(r"[1-9]+", str(value)) + blocks_of_digits = re.findall(r"[0-9]+", str(value)) number_of_digits = sum_of_lens(blocks_of_digits) return MIN_LENGTH_OF_PHONE_NUMBER <= number_of_digits <= MAX_LENGTH_OF_PHONE_NUMBER @@ -97,7 +105,7 @@ def is_phone_number(value: str): def cast_phone_to_format(value: str): """Cast value to format ddd ddd dd dd - country code from 1 digit up to 4""" - value = re.sub(r"[\\ +._-]", "", value) + value = re.sub(r"[\\() +._-]", "", value) code_len = len(value) - 10 @@ -106,7 +114,7 @@ def cast_phone_to_format(value: str): return ' '.join([value[0: code_len], value[code_len: code_len + 3], value[code_len + 3: code_len + 6], value[code_len + 6: code_len + 8], - value[code_len + 8: code_len + 10], value[code_len + 10:]]) + value[code_len + 8: code_len + 10]]) def __set_name__(self, owner, name): self.name = name @@ -115,7 +123,7 @@ def __get__(self, instance, owner): return instance.__dict__[self.name] def __set__(self, instance, value): - if type(value) != str: + if not isinstance(value, str): message = "Argument {} is not an instance of str".format(value) instance.error_logger.error(message) raise TypeError(message) @@ -125,8 +133,14 @@ def __set__(self, instance, value): instance.error_logger.error(message) raise ValueError(message) - instance.__dict__[self.name] = self.cast_phone_to_format(value) - instance.success_logger.info("{} assigned as {}".format(self.name, instance.__dict__[self.name])) + value = self.cast_phone_to_format(value) + + write_to_log = self.name in instance.__dict__ + + instance.__dict__[self.name] = value + + if write_to_log: + log_update(self, instance, value) class DocumentReadOnlyDescriptor: @@ -163,8 +177,12 @@ def __set__(self, instance, value): instance.error_logger.error(message) raise ValueError(message) + write_to_log = self.name in instance.__dict__ + instance.__dict__[self.name] = value - instance.success_logger.info("{} assigned as {}".format(self.name, instance.__dict__[self.name])) + + if write_to_log: + log_update(self, instance, value) def all_digits_to_str(value): @@ -219,6 +237,11 @@ def __set__(self, instance, value): types_len = {"паспорт": 10, "загран": 9, "водительские права": 10} # Все по тестам document_type = instance.__dict__['document_type'] + if not isinstance(value, str): + message = "Argument {} is not an instance of str".format(value) + instance.error_logger.error(message) + raise TypeError(message) + blocks_of_digits = re.findall(r"[0-9]+", str(value)) number_of_digits = sum_of_lens(blocks_of_digits) @@ -227,5 +250,11 @@ def __set__(self, instance, value): instance.error_logger.error(message) raise ValueError(message) - instance.__dict__[self.name] = cast_to_document_type(value, document_type) - instance.success_logger.info("{} assigned as {}".format(self.name, instance.__dict__[self.name])) + value = cast_to_document_type(value, document_type) + + write_to_log = self.name in instance.__dict__ + + instance.__dict__[self.name] = value + + if write_to_log: + log_update(self, instance, value) From 11e1067a5aa0ca3d66f32b8627b94da92a766418 Mon Sep 17 00:00:00 2001 From: Frumka Date: Thu, 30 Apr 2020 03:04:49 +0300 Subject: [PATCH 4/6] patient collection done --- homework/patient.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/homework/patient.py b/homework/patient.py index 66e54b1..a5d03e9 100644 --- a/homework/patient.py +++ b/homework/patient.py @@ -90,10 +90,31 @@ def __del__(self): class PatientCollection: - def __init__(self, log_file): - pass + def __init__(self, log_file_path, limit = -1): + self.path = log_file_path + self.line_byte_number = 0 + self.count = limit + + def __iter__(self): + return self + + def __next__(self): + with open(self.path, 'r', encoding='utf-8') as patients_csv: + patients_csv.seek(self.line_byte_number) + data = patients_csv.readline() + self.line_byte_number = patients_csv.tell() + + if not data or self.count == 0: + raise StopIteration + + self.count -= 1 + return Patient(*data.split(',')) def limit(self, n): - raise NotImplementedError() + self.count = n + return self.__iter__() +x = PatientCollection("patients.csv") +for i, e in enumerate(x): + print(i, e) From 0367d1d9caff21b6eec247ca3ebd52fca1dc3076 Mon Sep 17 00:00:00 2001 From: Frumka Date: Thu, 30 Apr 2020 03:08:13 +0300 Subject: [PATCH 5/6] cleanup --- homework/patient.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homework/patient.py b/homework/patient.py index a5d03e9..64a7890 100644 --- a/homework/patient.py +++ b/homework/patient.py @@ -90,7 +90,7 @@ def __del__(self): class PatientCollection: - def __init__(self, log_file_path, limit = -1): + def __init__(self, log_file_path, limit=-1): self.path = log_file_path self.line_byte_number = 0 self.count = limit @@ -113,8 +113,3 @@ def __next__(self): def limit(self, n): self.count = n return self.__iter__() - - -x = PatientCollection("patients.csv") -for i, e in enumerate(x): - print(i, e) From 4772cf59071783a939ffe361fa26308352ca566c Mon Sep 17 00:00:00 2001 From: Frumka Date: Thu, 30 Apr 2020 03:11:44 +0300 Subject: [PATCH 6/6] =?UTF-8?q?=D0=92=D0=B7=D0=B2=D1=80=D0=B0=D1=89=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=BB=D1=83=D0=B4=D0=BD=D1=8B=D1=85?= =?UTF-8?q?=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20=C2=AF\=5F(=E3=83=84)?= =?UTF-8?q?=5F/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_patient.py | 195 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 tests/test_patient.py diff --git a/tests/test_patient.py b/tests/test_patient.py new file mode 100644 index 0000000..19287a4 --- /dev/null +++ b/tests/test_patient.py @@ -0,0 +1,195 @@ +import functools +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 tests.constants import GOOD_PARAMS, OTHER_GOOD_PARAMS, WRONG_PARAMS, PATIENT_FIELDS + + +def get_len(file): + with open(file, encoding='utf-8') as f: + return len(f.readlines()) + + +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) + result = func(*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 + + +def setup_module(__main__): + for file in [GOOD_LOG_FILE, ERROR_LOG_FILE, CSV_PATH]: + with open(file, 'w', encoding='utf-8') as f: + f.write('') + + +def teardown_module(__name__): + for file in [GOOD_LOG_FILE, ERROR_LOG_FILE, CSV_PATH]: + os.remove(file) + + +@check_log_size("error") +@check_log_size("good", increased=True) +def test_creation_all_good_params(): + patient = Patient("Кондрат", "Коловрат", "1978-01-31", PHONE_FORMAT, PASSPORT_TYPE, PASSPORT_FORMAT) + assert patient.first_name == "Кондрат", "Wrong attribute first_name" + assert patient.last_name == "Коловрат", "Wrong attribute last_name" + assert patient.birth_date == "1978-01-31" or patient.birth_date == datetime(1978, 1, 31), \ + "Wrong attribute birth_date" + assert patient.phone == PHONE_FORMAT, "Wrong attribute phone" + assert patient.document_type == PASSPORT_TYPE, "Wrong attribute document_type" + assert patient.document_id == PASSPORT_FORMAT, "Wrong attribute document_id" + + patient = Patient("Кондрат", "Коловрат", "1978-01-31", PHONE_FORMAT, INTERNATIONAL_PASSPORT_TYPE, + INTERNATIONAL_PASSPORT_FORMAT) + assert patient.document_type == INTERNATIONAL_PASSPORT_TYPE, "Wrong attribute document_type" + assert patient.document_id == INTERNATIONAL_PASSPORT_FORMAT, "Wrong attribute document_id" + + patient = Patient("Кондрат", "Коловрат", "1978-01-31", PHONE_FORMAT, DRIVER_LICENSE_TYPE, DRIVER_LICENSE_FORMAT) + assert patient.document_type == DRIVER_LICENSE_TYPE, "Wrong attribute document_type" + assert patient.document_id == DRIVER_LICENSE_FORMAT, "Wrong attribute document_id" + + +@pytest.mark.parametrize('default,new', itertools.permutations(["(916)", "916", "-916-"], 2)) +@check_log_size("error") +@check_log_size("good", increased=True) +def test_creation_acceptable_phone(default, new): + patient = Patient("Кондрат", "Коловрат", "1978-01-31", PHONE_FORMAT.replace(default, new), + PASSPORT_TYPE, PASSPORT_FORMAT) + assert patient.phone == PHONE_FORMAT, "Wrong attribute phone" + + +@pytest.mark.parametrize('passport', ("00 00 000 000", "0000-000000", "0 0 0 0 0 0 0 0 0 0", "0000/000-000")) +@check_log_size("error") +@check_log_size("good", increased=True) +def test_creation_acceptable_passport(passport): + patient = Patient("Кондрат", "Коловрат", "1978-01-31", PHONE_FORMAT, PASSPORT_TYPE, passport) + assert patient.document_id == PASSPORT_FORMAT, "Wrong attribute document_id" + + +@pytest.mark.parametrize('passport', ("00 0000000", "00-0000000", "0 0 0 0 0 0 0 0 0", "00/000-0000")) +@check_log_size("error") +@check_log_size("good", increased=True) +def test_creation_acceptable_international_passport(passport): + patient = Patient("Кондрат", "Коловрат", "1978-01-31", PHONE_FORMAT, INTERNATIONAL_PASSPORT_TYPE, passport) + assert patient.document_id == INTERNATIONAL_PASSPORT_FORMAT, "Wrong attribute document_id" + + +@pytest.mark.parametrize('driver_license', ("00 00 000 000", "0000-000000", "0 0 0 0 0 0 0 0 0 0", "0000/000000")) +@check_log_size("error") +@check_log_size("good", increased=True) +def test_creation_acceptable_driver_license(driver_license): + patient = Patient("Кондрат", "Коловрат", "1978-01-31", PHONE_FORMAT, DRIVER_LICENSE_TYPE, driver_license) + assert patient.document_id == DRIVER_LICENSE_FORMAT, "Wrong attribute document_id" + + +# неверный тип +@pytest.mark.parametrize("i", list(range(len(GOOD_PARAMS)))) +@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 + + +# неверные значения +@pytest.mark.parametrize("i", list(range(len(GOOD_PARAMS)))) +@check_log_size("error", increased=True) +@check_log_size("good") +def test_creation_wrong_params(i): + try: + 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 + + +# метод create +@check_log_size("error") +@check_log_size("good", increased=True) +def test_create_method_good_params(): + patient = Patient.create(*GOOD_PARAMS) + for param, field in zip(GOOD_PARAMS, PATIENT_FIELDS): + assert getattr(patient, field) in (param, datetime(1978, 1, 31)), f"Wrong attribute {field}" + + +# обновление параметров +@pytest.mark.parametrize("patient,field,param", zip( + [Patient(*OTHER_GOOD_PARAMS)] * len(PATIENT_FIELDS[:2]), + PATIENT_FIELDS[:2], + GOOD_PARAMS[:2] +)) +@check_log_size("error", increased=True) +@check_log_size("good") +def test_names_assignment(patient, field, param): + try: + setattr(patient, field, param) + assert False, f"Attribute error should be invoked for {field} changing" + except AttributeError: + assert True + + +@pytest.mark.parametrize("patient,field,param", zip( + [Patient(*OTHER_GOOD_PARAMS)] * len(PATIENT_FIELDS[2:]), + PATIENT_FIELDS[2:], + GOOD_PARAMS[2:] +)) +@check_log_size("error") +@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" + + +@pytest.mark.parametrize("patient,field,param", zip( + [Patient(*OTHER_GOOD_PARAMS)] * len(PATIENT_FIELDS[2:]), + PATIENT_FIELDS[2:], + [1.4] * len(PATIENT_FIELDS[2:]) +)) +@check_log_size("error", increased=True) +@check_log_size("good") +def test_wrong_type_assignment(patient, field, param): + try: + setattr(patient, field, param) + assert False, f"TypeError for {field} assignment not invoked" + except TypeError: + assert True + + +@pytest.mark.parametrize("patient,field,param", zip( + [Patient(*OTHER_GOOD_PARAMS)] * len(PATIENT_FIELDS[2:]), + PATIENT_FIELDS[2:], + WRONG_PARAMS[2:] +)) +@check_log_size("error", increased=True) +@check_log_size("good") +def test_wrong_value_assignment(patient, field, param): + try: + setattr(patient, field, param) + assert False, f"ValueError for {field} assignment not invoked" + except ValueError: + assert True + + +# метод save +@check_log_size("csv", increased=True) +def test_save(): + patient = Patient(*GOOD_PARAMS) + patient.save() \ No newline at end of file