Skip to content

ellyzing/python-by-coursera

Repository files navigation

Задания по программированию курса Погружение в Python

by Московский физико-технический институт, Mail.Ru Group & ФРОО

Неделя 1 "Введение в Python"

1 "Сумма цифр в строке"

Давайте начнем с несложной задачи. Ваша цель написать программу (скрипт), которая будет запускаться из командной строки. Программа принимает в качестве аргумента строку, состоящую из цифр. Гарантируется, что других символов в переданном параметре нет и на вход всегда подается не пустая строка. Программа должна вычислить сумму цифр из которых состоит строка и вывести полученный результат на печать в стандартный вывод.

Примеры работы программы:

$ python solution.py 12345
15
$ python solution.py 160438521039
42

Считать переданный параметр можно с помощью модуля стандартной библиотеки sys:

import sys
digit_string = sys.argv[1]

Выполнение этого кода, создаст переменную digit_string, значением которой будет строка, переданная в параметре при запуске вашей программы. Более подробно о модуле sys и передаче параметров в скрипт вы можете прочитать в документации к модулю sys.

Файл с программой должен называться solution.py. После написания и отладки вашего решения, вам необходимо загрузить файл solution.py на платформу для проверки.

2 "Рисуем лестницу"

Это задание чуть сложней предыдущего и потребует от вас размышлений. Необходимо написать скрипт, который «нарисует» (выведет на консоль) лестницу. Количество ступенек в лестнице передается скрипту в качестве параметра. Гарантируется, что на вход подаются только целые числа > 0. Чтение данных нужно произвести способом, аналогичным тому, что описан в предыдущем задании. Ступени должны отображаться с помощью символа решетки "#" и пробелов. Пример работы скрипта:

$ python solution.py 3
  #
 ##
###
$ python solution.py 5
    #
   ##
  ###
 ####
#####
$

На что обратить внимание? Вывод должен содержать только пробелы и символ "#". Первая строка вывода не должна быть пустой. Строки вывода лестницы не должны содержать лишних пробелов в начале и конце строки. Допускается наличие пустой строки после вывода последней строки, содержащей ступени. Например так:

$ python solution.py 5
    #
   ##
  ###
 ####
#####

$

3 "Корни квадратного уравнения"

Python очень активно применяют ученые со всего мира в своих исследованиях. В этом задании мы с вами попробуем с помощью Python найти корни квадратного уравнения.

Если вы не помните формулу (а мы и сами ее не помним!), позволяющую найти корни такого уравнения, обратитесь за помощью к статье в Википедии.

Ваша задача написать программу, которая запускается в командной строке с параметрами, вычисляет значения корней квадратного уравнения и выводит их на печать. На вход программе подаются коэффициенты a, b и c. На печать должно выводиться два корня квадратного уравнения. Обратите внимание на то, как выводятся корни - каждый с новой строки.

Чтобы не усложнять вашу задачу все коэффициенты, которые мы будем подавать вам на вход являются коэффициентами, которые в итоге дают 2 корня квадратного уравнения.

Прочитать коэффициенты a, b, c вы можете следующим образом:

import sys 
a = int(sys.argv[1]) 
b = int(sys.argv[2]) 
c = int(sys.argv[3])

Примеры работ:

$ python solution.py 1 -3 -4
4
-1
$ python solution.py 13 236 -396
1
-19

Корни должны быть приведены к целочисленному виду перед выводом на экран, порядок вывода корней произвольный.

Неделя 2 "Ключевые концепции"

1 "Key-value"

На этой неделе мы с вами реализуем собственное key-values хранилище. Данные будут сохраняться в файле storage.data. Добавление новых данных в хранилище и получение текущих значений осуществляется с помощью утилиты командной строки storage.py. Пример работы утилиты:

Сохранение значения value по ключу key_name:

$ storage.py --key key_name --val value

Получение значения по ключу key_name:

$ storage.py --key key_name

Вашей задачей будет написать реализацию утилиты storage.py.

Утилита может вызваться со следующими параметрами:

--key <имя ключа> , где <имя ключа> - ключ по которому сохраняются/получаются значения

--val <значение>, где <значение> - сохраняемое значение.

Если при запуске утилиты переданы оба ключа, происходит добавление переданного значения по ключу и сохранение данных в файле. Если передано только имя ключа, происходит чтение файла хранилища и вывод на печать значений, которые были сохранены по данному ключу. Обратите внимание, что значения по одному ключу не перезаписываются, а добавляются к уже сохраненным. Другими словами - по одному ключу могут храниться несколько значений. При выводе на печать, значения выводятся в порядке их добавления в хранилище. Формат вывода на печать для нескольких значений:

value_1, value_2

Обратите внимание на пробел после запятой. Если значений по ключу не было найдено, выведите пустую строку или None.

Для работы с аргументами командной строки используйте модуль argparse. Хранить данные в файле мы рекомендуем в формате JSON с помощью использования модуля стандартной библиотеки json. Прежде чем отправлять ваше решение на проверку, протестируйте работу вашей утилиты на добавление нескольких ключей и разных значений.

Файл следует создавать с помощью модуля tempfile.

import os
import tempfile

storage_path = os.path.join(tempfile.gettempdir(), 'storage.data')
with open(storage_path, 'w') as f:
  ...

Пример работы:

$ python storage.py --key key_name --val value
$ python storage.py --key key_name
value

$ python storage.py --key multi_key --val value1
$ python storage.py --key multi_key --val value2
$ python storage.py --key multi_key
value1, value2

2 "Декоратор to_json"

Чтобы передавать данные между функциями, модулями или разными системами используются форматы данных. Одним из самых популярных форматов является JSON. Напишите декоратор to_json, который можно применить к различным функциям, чтобы преобразовывать их возвращаемое значение в JSON-формат. Не забудьте про сохранение корректного имени декорируемой функции.

@to_json
def get_data():
  return {
    'data': 42
  }
  
get_data()  # вернёт '{"data": 42}'

Опишите декоратор в файле и загрузите его на платформу.

Неделя 3 "Объектно-ориентированное программирование"

1 "Реализация простого класса для чтения из файла"

Первое задание на этой неделе — не сложное, для разогрева. Ваша задача: написать python-модуль solution.py, внутрь которого необходимо поместить код класса FileReader. Конструктор этого класса принимает один параметр: путь до файла на диске. В классе FileReader должен быть реализован метод read, возвращающий строку - содержимое файла, путь к которому был указан при создании экземпляра класса. Python модуль должен быть написан таким образом, чтобы импорт класса FileReader из него не вызвал ошибок.

При написании реализации метода read, вам нужно учитывать случай, когда при инициализации был передан путь к несуществующему файлу. Требуется обработать возникающее при этом исключение FileNotFoundError и вернуть из метода read пустую строку.

Пример работы:

>>> from solution import FileReader
>>> reader = FileReader('not_exist_file.txt')
>>> text = reader.read()
>>> text
''
>>> with open('some_file.txt', 'w') as file:
...     file.write('some text')
...
9
>>> reader = FileReader('some_file.txt')
>>> text = reader.read()
>>> text
'some text'
>>> type(reader)
<class 'solution.FileReader'>
>>> 

2 "Классы и наследование"

Как правило задачи про классы не носят вычислительный характер. Обычно нужно написать классы, которые отвечают определенным интерфейсам. Насколько удобны эти интерфейсы и как сильно связаны классы между собой, определит легкость их использования в будущих программах.

Предположим есть данные о разных автомобилях и спецтехнике. Данные представлены в виде таблицы с характеристиками. Вся техника разделена на три вида: спецтехника, легковые и грузовые автомобили. Обратите внимание на то, что некоторые характеристики присущи только определенному виду техники. Например, у легковых автомобилей есть характеристика «кол-во пассажирских мест», а у грузовых автомобилей — габариты кузова: «длина», «ширина» и «высота».

Тип (car_type) Марка (brand) Кол-во пассажирских мест (passenger_seats_count) Фото (photo_file_name) Кузов ДxШxВ, м (body_whl) Грузоподъемность, Тонн (carrying) Дополнительно (extra) car Nissan xTtrail 4 f1.jpeg 2.5 truck Man f2.jpeg 8x3x2.5 20 car Mazda 6 4 f3.jpeg 2.5 spec_machine Hitachi f4.jpeg 1.2 Легкая техника для уборки снега Вам необходимо создать свою иерархию классов для данных, которые описаны в таблице. Классы должны называться CarBase (базовый класс для всех типов машин), Car (легковые автомобили), Truck (грузовые автомобили) и SpecMachine (спецтехника). Все объекты имеют обязательные атрибуты:

  • car_type, значение типа объекта и может принимать одно из значений: «car», «truck», «spec_machine».

  • photo_file_name, имя файла с изображением машины, допустимы названия файлов изображений с расширением из списка: «.jpg», «.jpeg», «.png», «.gif»

  • brand, марка производителя машины

  • carrying, грузоподъемность

В базовом классе CarBase нужно реализовать метод get_photo_file_ext для получения расширения файла изображения. Расширение файла можно получить при помощи os.path.splitext.

Для грузового автомобиля необходимо в конструкторе класса определить атрибуты: body_length, body_width, body_height, отвечающие соответственно за габариты кузова — длину, ширину и высоту. Габариты передаются в параметре body_whl (строка, в которой размеры разделены латинской буквой «x»). Обратите внимание на то, что характеристики кузова должны быть вещественными числами и характеристики кузова могут быть не валидными (например, пустая строка). В таком случае всем атрибутам, отвечающим за габариты кузова, присваивается значение равное нулю.

Также для класса грузового автомобиля необходимо реализовать метод get_body_volume, возвращающий объем кузова.

В классе Car должен быть определен атрибут passenger_seats_count (количество пассажирских мест), а в классе SpecMachine — extra (дополнительное описание машины).

Полная информация о атрибутах классов приведена в таблице ниже, где 1 - означает, что атрибут обязателен для объекта, 0 - атрибут должен отсутствовать.

Car Truck SpecMachine car_type 1 1 1 photo_file_name 1 1 1 brand 1 1 1 carrying 1 1 1 passenger_seats_count 1 0 0 body_width 0 1 0 body_height 0 1 0 body_length 0 1 0 extra 0 0 1 Обратите внимание, что у каждого объекта из иерархии должен быть свой набор атрибутов и методов. Например, у класса легковой автомобиль не должно быть метода get_body_volume в отличие от класса грузового автомобиля. Имена атрибутов и методов должны совпадать с теми, что описаны выше.

Далее вам необходимо реализовать функцию get_car_list, на вход которой подается имя файла в формате csv. Файл содержит данные, аналогичные строкам из таблицы. Вам необходимо прочитать этот файл построчно при помощи модуля стандартной библиотеки csv. Затем проанализировать строки на валидность и создать список объектов с автомобилями и специальной техникой. Функция должна возвращать список объектов.

Первая строка в исходном файле — это заголовок csv, который содержит имена колонок. Нужно пропустить первую строку из исходного файла. Обратите внимание на то, что в некоторых строках исходного файла , данные могут быть заполнены некорректно, например, отсутствовать обязательные поля или иметь не валидное значение. В таком случае нужно проигнорировать подобные строки и не создавать объекты. Строки с пустым или не валидным значением для body_whl игнорироваться не должны. Вы можете использовать стандартный механизм обработки исключений в процессе чтения, валидации и создания объектов из строк csv-файла. Проверьте работу вашего кода с входным файлом, прежде чем загружать задание для оценки.

Пример кода, демонстрирующего чтение csv файла:

import csv

with open(csv_filename) as csv_fd:
    reader = csv.reader(csv_fd, delimiter=';')
    next(reader)  # пропускаем заголовок
    for row in reader:
        print(row)

Ниже приведен шаблон кода для выполнения задания:

class CarBase:
    def __init__(self, brand, photo_file_name, carrying):
        pass


class Car(CarBase):
    def __init__(self, brand, photo_file_name, carrying, passenger_seats_count):
        pass


class Truck(CarBase):
    def __init__(self, brand, photo_file_name, carrying, body_whl):
        pass


class SpecMachine(CarBase):
    def __init__(self, brand, photo_file_name, carrying, extra):
        pass


def get_car_list(csv_filename):
    car_list = []
    return car_list

Несколько примеров работы:

>>> from solution import *
>>> car = Car('Bugatti Veyron', 'bugatti.png', '0.312', '2')
>>> print(car.car_type, car.brand, car.photo_file_name, car.carrying,
... car.passenger_seats_count, sep='\n')
car
Bugatti Veyron
bugatti.png
0.312
2
>>> truck = Truck('Nissan', 'nissan.jpeg', '1.5', '3.92x2.09x1.87')
>>> print(truck.car_type, truck.brand, truck.photo_file_name, truck.body_length,
... truck.body_width, truck.body_height, sep='\n')
truck
Nissan
nissan.jpeg
3.92
2.09
1.87
>>> spec_machine = SpecMachine('Komatsu-D355', 'd355.jpg', '93', 'pipelayer specs')
>>> print(spec_machine.car_type, spec_machine.brand, spec_machine.carrying,
... spec_machine.photo_file_name, spec_machine.extra, sep='\n')
spec_machine
Komatsu-D355
93.0
d355.jpg
pipelayer specs
>>> spec_machine.get_photo_file_ext()
'.jpg'
>>> cars = get_car_list('cars_week3.csv')
>>> len(cars)
4
>>> for car in cars:
...     print(type(car))
... 
<class 'solution.Car'>
<class 'solution.Truck'>
<class 'solution.Truck'>
<class 'solution.Car'>
>>> cars[0].passenger_seats_count
4
>>> cars[1].get_body_volume()
60.0
>>>

Неделя 4 "Углубленный Python"

1 "Файл с магическими методами"

В этом задании вам нужно создать интерфейс для работы с файлами. Интерфейс должен предоставлять следующие возможности по работе с файлами:

  • чтение из файла, метод read возвращает строку с текущим содержанием файла

  • запись в файл, метод write принимает в качестве аргумента строку с новым содержанием файла

  • сложение объектов типа File, результатом сложения является объект класса File, при этом создается новый файл и файловый объект, в котором содержимое второго файла добавляется к содержимому первого файла. Новый файл должен создаваться в директории, полученной с помощью функции tempfile.gettempdir. Для получения нового пути можно использовать os.path.join.

  • возвращать в качестве строкового представления объекта класса File полный путь до файла

  • поддерживать протокол итерации, причем итерация проходит по строкам файла

При создании экземпляра класса File в конструктор передается полный путь до файла на файловой системе. Если файла с таким путем не существует, он должен быть создан при инициализации.

Пример работы:

>>> import os.path
>>> from solution import File
>>> path_to_file = 'some_filename'
>>> os.path.exists(path_to_file)
False
>>> file_obj = File(path_to_file)
>>> os.path.exists(path_to_file)
True
>>> file_obj.read()
''
>>> file_obj.write('some text')
9
>>> file_obj.read()
'some text'
>>> file_obj.write('other text')
10
>>> file_obj.read()
'other text'
>>> file_obj_1 = File(path_to_file + '_1')
>>> file_obj_2 = File(path_to_file + '_2')
>>> file_obj_1.write('line 1\n')
7
>>> file_obj_2.write('line 2\n')
7
>>> new_file_obj = file_obj_1 + file_obj_2
>>> isinstance(new_file_obj, File)
True
>>> print(new_file_obj)
C:\Users\Media\AppData\Local\Temp\71b9e7b695f64d85a7488f07f2bc051c
>>> for line in new_file_obj:
....    print(ascii(line))  
'line 1\n'
'line 2\n'

2 "Дескриптор с комиссией"

Часто при зачислении каких-то средств на счет с нас берут комиссию. Давайте реализуем похожий механизм с помощью дескрипторов. Напишите дескриптор Value, который будет использоваться в нашем классе Account.

class Account:
    amount = Value()
    
    def __init__(self, commission):
        self.commission = commission

У аккаунта будет атрибут commission. Именно эту коммиссию и нужно вычитать при присваивании значений в amount.

new_account = Account(0.1)
new_account.amount = 100

print(new_account.amount)
90

Неделя 5 "Многопоточное и асинхронное программирование"

Клиент для отправки метрик

В крупных проектах, с большим количеством пользователей, необходимо тщательно наблюдать за всеми процессами, происходящими в нем. Информация о процессах может быть представлена различными численными показателями, например: количество запросов к вашему приложению, время ответа вашего сервиса на каждый запрос, количество пользователей в сутки и другие. Эти различные численные показатели мы будем называть метриками.

Для сбора, хранения и отображения подобных метрик существуют готовые решения, например Graphite, InfluxDB. Мы в рамках курса разработаем свою систему для сбора и хранения метрик, основанную на клиент-серверной архитектуре.

На этой неделе мы начнем с разработки клиента для отправки и получения метрик. На следующей неделе, в качестве финального задания, вам будет предложено реализовать сервер для хранения метрик.

Протокол взаимодействия Прежде, чем приступить к описанию задания, рассмотрим протокол взаимодействия, по которому будет происходить обмен данными между клиентом и сервером.

Клиент и сервер взаимодействуют между собой по простому текстовому протоколу через TCP сокеты. Текстовый протокол имеет главное преимущество – наглядность, можно просматривать диалог взаимодействия клиентской и серверной стороны без использования дополнительных инструментов.

Общий формат запросов и ответов. Протокол поддерживает два вида запросов к серверу со стороны клиента:

  • отправка данных для сохранения их на сервере

  • получения сохраненных данных

Общий формат запроса клиента:

<команда> <данные запроса><\n>

где:

  • <команда> - команда сервера (команда может принимать одно из двух значений: put — сохранить данные на сервере, get — вернуть сохраненные данные с сервера),

  • <данные запроса> - данные запроса (их формат мы подробно разберем ниже в примере),

  • <\n> - символ переноса строки.

Обратим ваше внимание на пробел между командой и данными запроса и его отсутствием между данными и символом перевода на новую строку.

Общий формат ответов сервера:

<статус ответа><\n><данные ответа><\n\n>

где:

  • <статус ответа> - статус выполнения команды, допустимы два варианта: «ok» - команда успешно выполнена на сервере и «error» - выполнение команды завершилось ошибкой

  • <данные ответа> - не обязательное поле (формат ответа и случаи его отсутствия будут рассмотрены в примере ниже)

  • <\n\n> - два символа переноса строки.

Обратите внимание, что статус ответа и данные ответа разделены символом перевода строки <\n>.

Пример взаимодействия сервера и клиента.

Для наглядности рассмотрим протокол взаимодействия между клиентом и сервером на конкретном примере. В примере мы будем, собирать метрики с данными о работе операционной системы: cpu (загрузка процессора), usage (потребление памяти), disk_usage (потребление места на жестком диске), network_usage (статистика сетевых интерфейсов). Такие данные могут понадобится для контроля загрузки серверов и прогноза по расширению парка железа компании - проще говоря для мониторинга.

Какие данные мы будем сохранять?

Для каждой метрики () мы будем хранить данные о ее значениях () и времени, когда производилось измерение () . Поскольку, в реальной жизни серверов может быть несколько, необходимо различать данные полученные от разных серверов (в нашем примере имеются в наличии два сервера palm и eardrum). Договоримся об именовании , в примере мы будем определять их по правилу:

<название сервера>.<название метрики><название сервера>.<название метрики>

Примеры названий метрик: "palm.cpu", "eardrum.memory".

Таким образом на сервере по каждому ключу будет сохранятся список данных конкретных измерений (пара: значение, время измерения).

Запросы клиента.

Рассмотрим пример отправки на сервер данных для сохранения. Пусть у нас имеются данные измерений - загрузка процессора «cpu» на сервере "palm" во время 1150864247 была равна 23.7 процента. Строка запроса в этом случае будет иметь вид:

put palm.cpu 23.7 1150864247\n

В запросе на сохранение мы можем передать данные только об одном измерении.

Чтобы получить с сервера данные, сохраненные по ключу «palm.cpu», необходимо в данных запроса просто передать имя ключа:

get palm.cpu\n

Для случая, когда необходимо получить все хранимые на сервере данные, в качестве ключа используется символ звездочки «*». Пример строки запроса:

get *\n

Ответы сервера.

Допустим, что на сервере хранятся данные:

key          | value | timestamp  
-----------------------------------

"palm.cpu"    |  2.0  | 1150864247

"palm.cpu"    |  0.5  | 1150864248

"eardrum.cpu" |  3.0  | 1150864250

Тогда в ответ на запрос о получении данных по ключу "palm.cpu" сервер отправит строку:

ok\npalm.cpu 2.0 1150864247\npalm.cpu 0.5 1150864248\n\n

Данные ответа содержат данные о каждой сохраненной записи с ключом "palm.cpu" (метрика, значение, временная метка разделенные пробелом), которые разделены символом перевода строки «\n».

Строка ответа сервера на запрос о получении всех хранящихся на сервере данных (в качестве ключа передано «*») в нашем случае будет таким:

ok\npalm.cpu 2.0 1150864247\npalm.cpu 0.5 1150864248\neardrum.cpu 3.0 1150864250\n\n

В случаях:

  • когда в запросе на получение данных передан не существующий ключ

  • успешного выполнения команды сохранения данных put

сервер отправляет клиенту строку со статусом «оk» и пустым полем с данными ответа:

ok\n\n

Если в параметре запроса переданы не валидные данные (например: нарушен формат запроса, ошибочная команда или значения value и timestamp не могут быть приведены к необходимому типу данных) сервер отправляет строку со статусом ответа «error» и данными ответа «wrong command»:

error\nwrong command\n\n

Реализация клиента. Необходимо реализовать класс Client, в котором будет инкапсулировано соединение с сервером, клиентский сокет и методы для получения (get) и отправки (put) метрик на сервер. Отправка и получение данных в методах get и put должна быть реализована в соответствии с протоколом, описанным выше. В конструктор класса Client должна передаваться адресная пара хост и порт, а также необязательный аргумент timeout (имеющий значение по умолчанию - None). Соединение с сервером устанавливается при создании экземпляра класса Client и не должно разрываться между запросами.

Пример создания объекта клиента и отправки запросов на сервер:

>>> from solution import Client

>>> client = Client("127.0.0.1", 8888, timeout=15)

>>> client.put("palm.cpu", 0.5, timestamp=1150864247)

>>> client.put("palm.cpu", 2.0, timestamp=1150864248)

>>> client.put("palm.cpu", 0.5, timestamp=1150864248)

>>> client.put("eardrum.cpu", 3, timestamp=1150864250)

>>> client.put("eardrum.cpu", 4, timestamp=1150864251)

>>> client.put("eardrum.memory", 4200000)

>>> print(client.get("*"))

Метод put. Метод put принимает в качестве параметров: название метрики, численное значение и необязательный именованный параметр timestamp. Если пользователь вызвал метод put без аргумента timestamp, то клиент автоматически должен подставить значение временной отметки, полученное с помощью вызова int(time.time()).

Метод put не возвращает ничего в случае успешной отправки и выбрасывает пользовательское исключение ClientError в случае не успешной.

Метод get. Метод get принимает в качестве параметра имя метрики, значения которой мы хотим получить. В качестве имени метрики можно использовать символ «*», о котором мы упоминали в описании протокола.

Метод get возвращает словарь с метриками (смотрите пример ниже) в случае успешного получения ответа от сервера и выбрасывает исключение ClientError в случае не успешного.

Клиент получает данные от сервера в текстовом виде, метод get должен обработать строку ответа и вернуть словарь с полученными ключами с сервера. Значением ключей в словаре является список кортежей:

[(timestamp1, metric_value1), (timestamp2, metric_value2), …]

Значение timestamp и metric_value должны быть преобразованы соответственно к типам int и float. Список должен быть отсортирован по значению timestamp (по возрастанию).

Пример возвращаемого значения при успешном вызове client.get("palm.cpu"):

{
  'palm.cpu': [
    (1150864247, 0.5),
    (1150864248, 0.5)
  ]
}

Обратите внимание, что сервер хранит данные с максимальным разрешением в одну секунду. Это означает, что если в одну и ту же секунду отправить две одинаковые метрики, то будет сохранено только одно значение, которое было обработано последним. Все остальные значения будут перезаписаны. По этой причине запрос по ключу "palm.cpu" вернул данные двух измерений.

Пример возвращаемого значения при успешном вызове client.get("*"):

{
  'palm.cpu': [
    (1150864247, 0.5),
    (1150864248, 0.5)
  ],
  'eardrum.cpu': [
    (1150864250, 3.0),
    (1150864251, 4.0)
  ],
  'eardrum.memory': [
    (1503320872, 4200000.0)
  ]
}

Если в ответ на get-запрос сервер вернул положительный ответ "ok\n\n", но без данных (то есть данных по запрашиваемому ключу нет), то метод get клиента должен вернуть пустой словарь.

Итак, в качестве решения вам необходимо предоставить модуль с реализованным в нем классом Client, пользовательским исключением ClientError. В классе Client должны быть доступны методы get и put с описанной выше сигнатурой. При вызове методов get и put клиент должен посылать сообщения в TCP-соединение с сервером (в соответствии с описанным текстовым протоколом), получать ответ от сервера и возвращать словарь с данными, в формате описанном выше.

Примечание. Не смотря на то, что на этой неделе вы изучали асинхронность, клиент должен быть синхронным. Не расстраивайтесь, если вы хотели попробовать свои силы в написании асинхронного кода, на следующей неделе вам представится такая возможность.

Неделя 6 "Финальный проект"

На предыдущей неделе вы разработали клиентское сетевое приложение — клиента для сервера метрик, который умеет отправлять и получать данные о различных численных показателях. Пришло время финального задания — в нем необходимо реализовать серверную часть.

Как обычно, вам необходимо разработать программу в одном файле-модуле, который вы загрузите на проверку обычным способом. Сервер должен соответствовать протоколу, который был описан в задании к предыдущей неделе. Он должен уметь принимать от клиентов команды put и get, разбирать их, и формировать ответ согласно протоколу. По запросу put требуется сохранять метрики в структурах данных в памяти процесса. По запросу get сервер обязан отдавать данные в правильной последовательности. При работе с клиентом сервер должен поддерживать сессии, соединение с клиентом между запросами не должно "разрываться".

На верхнем уровне вашего модуля должна быть объявлена функция run_server(host, port) — она принимает адрес и порт, на которых должен быть запущен сервер.

Для проверки правильности решения мы воспользуемся своей реализацией клиента и будем отправлять на ваш сервер put и get запросы, ожидая в ответ правильные данные от сервера (согласно объявленному протоколу). Все запросы будут выполняться с таймаутом — сервер должен отвечать за приемлемое время.

Сервер должен быть готов к неправильным командам со стороны клиента и отдавать клиенту ошибку в формате, оговоренном в протоколе. В этих случаях работа сервера не должна завершаться аварийно.

На последней неделе мы с вами разбирали пример tcp-сервера на asyncio:

$: telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
> get test_key
< ok
< 
> got test_key
< error
< wrong command
< 
> put test_key 12.0 1503319740
< ok
< 
> put test_key 13.0 1503319739
< ok
< 
> get test_key 
< ok
< test_key 13.0 1503319739
< test_key 12.0 1503319740
< 
> put another_key 10 1503319739
< ok
< 
> get *
< ok
< test_key 13.0 1503319739
< test_key 12.0 1503319740
< another_key 10.0 1503319739
< 

Данный код создает tcp-соединение для адреса 127.0.0.1:8181 и слушает все входящие запросы. При подключении клиента будет создан новый экземпляр класса ClientServerProtocol, а при поступлении новых данных вызовется метод этого объекта - data_received. Внутри asyncio.Protocol спрятана вся магия обработки запросов через корутины, остается реализовать протокол взаимодействия между клиентом и сервером.

Вы можете использовать этот код, как основу при написании вашей реализации сервера. Это не обязательное требование. Для реализации задачи вы можете использовать любые вызовы из стандартной библиотеки Python 3 (обратим ваше внимание, что в грейдере установлена версия Python 3.6). Сервер должен уметь обрабатывать запросы от нескольких клиентов одновременно.

В процессе разработки сервера для тестирования работоспособности вы можете использовать клиент, написанный на предыдущей неделе.

Давайте еще раз посмотрим на текстовый протокол в действии при использовании утилиты telnet:

"""
Это вспомогательный скрипт для тестирования сервера из задания на неделе 6.

Для запуска скрипта на локальном компьютере разместите рядом файл client.py,
где содержится код клиента, который открывается по прохождении задания
недели 5.

Сначала запускаете ваш сервер на адресе 127.0.0.1 и порту 8888, а затем
запускаете этот скрипт.
"""
import sys
from client import Client, ClientError


def run(host, port):
    client1 = Client(host, port, timeout=5)
    client2 = Client(host, port, timeout=5)
    command = "wrong command test\n"

    try:
        data = client1.get(command)
    except ClientError:
        pass
    except BaseException as err:
        print(f"Ошибка соединения с сервером: {err.__class__}: {err}")
        sys.exit(1)
    else:
        print("Неверная команда, отправленная серверу, должна возвращать ошибку протокола")
        sys.exit(1)

    command = 'some_key'
    try:
        data_1 = client1.get(command)
        data_2 = client1.get(command)
    except ClientError:
        print('Сервер вернул ответ на валидный запрос, который клиент определил, '
              'как не корректный.. ')
    except BaseException as err:
        print(f"Сервер должен поддерживать соединение с клиентом между запросами, "
              f"повторный запрос к серверу завершился ошибкой: {err.__class__}: {err}")
        sys.exit(1)

    assert data_1 == data_2 == {}, \
        "На запрос клиента на получения данных по не существующему ключу, сервер " \
        "вдолжен озвращать ответ с пустым полем данных."

    try:
        data_1 = client1.get(command)
        data_2 = client2.get(command)
    except ClientError:
        print('Сервер вернул ответ на валидный запрос, который клиент определил'
              ', как не корректный.. ')
    except BaseException as err:
        print(f"Сервер должен поддерживать соединение с несколькими клиентами: "
              f"{err.__class__}: {err}")
        sys.exit(1)

    assert data_1 == data_2 == {}, \
        "На запрос клиента на получения данных по не существующему ключу, сервер " \
        "должен возвращать ответ с пустым полем данных."

    try:
        client1.put("k1", 0.25, timestamp=1)
        client2.put("k1", 2.156, timestamp=2)
        client1.put("k1", 0.35, timestamp=3)
        client2.put("k2", 30, timestamp=4)
        client1.put("k2", 40, timestamp=5)
        client1.put("k2", 41, timestamp=5)
    except Exception as err:
        print(f"Ошибка вызова client.put(...) {err.__class__}: {err}")
        sys.exit(1)

    expected_metrics = {
        "k1": [(1, 0.25), (2, 2.156), (3, 0.35)],
        "k2": [(4, 30.0), (5, 41.0)],
    }

    try:
        metrics = client1.get("*")
        if metrics != expected_metrics:
            print(f"client.get('*') вернул неверный результат. Ожидается: "
                  f"{expected_metrics}. Получено: {metrics}")
            sys.exit(1)
    except Exception as err:
        print(f"Ошибка вызова client.get('*') {err.__class__}: {err}")
        sys.exit(1)

    expected_metrics = {"k2": [(4, 30.0), (5, 41.0)]}

    try:
        metrics = client2.get("k2")
        if metrics != expected_metrics:
            print(f"client.get('k2') вернул неверный результат. Ожидается: "
                  f"{expected_metrics}. Получено: {metrics}")
            sys.exit(1)
    except Exception as err:
        print(f"Ошибка вызова client.get('k2') {err.__class__}: {err}")
        sys.exit(1)

    try:
        result = client1.get("k3")
        if result != {}:
            print(
                f"Ошибка вызова метода get с ключом, который еще не был добавлен. "
                f"Ожидается: пустой словарь. Получено: {result}")
            sys.exit(1)
    except Exception as err:
        print(f"Ошибка вызова метода get с ключом, который еще не был добавлен: "
              f"{err.__class__} {err}")
        sys.exit(1)

    print("Похоже, что все верно! Попробуйте отправить решение на проверку.")


if __name__ == "__main__":
    run("127.0.0.1", 8888)

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages