{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "6bc66b73",
   "metadata": {},
   "source": [
    "# Клиент для отправки метрик #"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "454171d9",
   "metadata": {},
   "source": [
    "## Хранение метрик ##\n",
    "\n",
    "Если вы разрабатываете настоящий проект, у которого есть большое количество пользователей, то необходимо наблюдать за всеми процессами, происходящими в нем. Для этого нужно смотреть за численными показателями в проекте. Показатели могут быть самыми разными - количество запросов к вашему приложению, время ответа вашего сервиса на каждый запрос, количество пользователей в сутки, и т.д. Эти всевозможные численные показатели мы будем называть метриками.\n",
    "\n",
    "Для сбора, хранения и отображения подобных метрик существуют готовые решения, например `Graphite`, `InfluxDB`. Мы в рамках курса разработаем свою систему для сбора метрик - сервер и клиент.\n",
    "\n",
    "В этом блоке мы начнем с разработки клиента для отправки подобных метрик на сервер, где они хранятся, и могут быть запрошены в любой момент времени. Затем в качестве финального задания в шестом блоке вам будет предложено реализовать и сам сервер."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "08938207",
   "metadata": {},
   "source": [
    "## Протокол взаимодействия ##\n",
    "\n",
    "Итак, в этом блоке вам необходимо разработать сетевую программу-клиент, при помощи которой можно отправлять различные метрики на сервер. Клиент и сервер должны взаимодействовать между собой по простому текстовому протоколу через TCP сокеты. Текстовый протокол имеет главное преимущество – он наглядный – можно просмотреть диалог взаимодействия клиентской и серверной стороны без использования дополнительных инструментов."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "77376f2a",
   "metadata": {},
   "source": [
    "Прежде чем реализовывать клиентское приложение давайте рассмотрим взаимодействие между клиентом и сервером на конкретных примерах."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "80bace3a",
   "metadata": {},
   "source": [
    "Предположим, необходимо собирать метрики о работе операционной системы: `cpu` (загрузка процессора), `memory usage` (потребление памяти), `disk usage` (потребление места на жестком диске), `network usage` (статистика сетевых интерфейсов) и т.д. Это понадобится для контроля загрузки серверов и прогноза по расширению парка железа компании - проще говоря для мониторинга.\n",
    "\n",
    "Пусть у нас имеется в наличии два сервера `huginn` и `muninn`. Мы будем получать загрузку центрального процессора на сервере и отправлять метрику с названием `имя_сервера.cpu`\n",
    "\n",
    "```\n",
    "client -> server: put huginn.cpu 10.6 1642667947\\n\n",
    "server -> client: ok\\n\\n\n",
    "client -> server: put muninn.cpu 15.3 1642667959\\n\n",
    "server -> client: ok\\n\\n\n",
    "```\n",
    "\n",
    "Чтобы отправить метрику на сервер, вы отправляете в TCP-соединение строку вида:\n",
    "\n",
    "```\n",
    "put huginn.cpu 10.6 1642667947\\n\n",
    "```\n",
    "\n",
    "Ключевое слово `put` означает команду отправки метрики. За ней через пробел следует название (имя) самой метрики, например `huginn.cpu`, далее опять через пробел значение метрики, и через еще один пробел временная метка `unix timestamp`. Таким образом, во время `1642667947` значение метрики `huginn.cpu` было равно `10.6`. Наконец, команда заканчивается символом переноса строки `\\n`.\n",
    "\n",
    "В ответ на эту команду `put` сервер присылает уведомление об успешном сохранении метрики в виде строки:\n",
    "\n",
    "```\n",
    "ok\\n\\n\n",
    "```\n",
    "\n",
    "Два переноса строки в данном случае означают маркер конца сообщения от сервера клиенту."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a090a6c9",
   "metadata": {},
   "source": [
    "## Команды ##\n",
    "\n",
    "Необходимо реализовать две команды:\n",
    "\n",
    "`put` - для сохранения метрик на сервере.\n",
    "\n",
    "`get` - для получения метрик.\n",
    "\n",
    "Формат команды `put` для отправки метрик — это строка вида:\n",
    "\n",
    "```\n",
    "put <key> <value> <timestamp>\\n\n",
    "```\n",
    "\n",
    "Успешный ответ от сервера:\n",
    "\n",
    "```\n",
    "ok\\n\\n\n",
    "```\n",
    "\n",
    "Ошибка сервера:\n",
    "\n",
    "```\n",
    "error\\nwrong command\\n\\n\n",
    "```\n",
    "\n",
    "Обратите внимание на то, что за каждым ответом сервера указано два символа `\\n`. В качестве значения метрики `value` используется вещественное число.\n",
    "\n",
    "Данные нужно не только отправлять на сервер, но и запрашивать их. Это может потребоваться для визуализации и анализа нужных метрик в определенные промежутки времени.\n",
    "\n",
    "Формат команды `get` для получения метрик — это строка вида:\n",
    "\n",
    "```\n",
    "get <key>\\n\n",
    "```\n",
    "\n",
    "В качестве ключа можно указывать символ `*`, для этого символа будут возвращены все доступные метрики. В данном задании мы никак не ограничиваем количество метрик, которые должен вернуть сервер – сервер должен возвращать все метрики, удовлетворяющие ключу.\n",
    "\n",
    "Успешный ответ от сервера:\n",
    "\n",
    "```\n",
    "ok\\nhuginn.cpu 10.5 1642667947\\nmuninn.cpu 15.3 1642667959\\n\\n\n",
    "```\n",
    "\n",
    "Если ни одна метрика не удовлетворяет условиям поиска, то вернется ответ:\n",
    "\n",
    "```\n",
    "ok\\n\\n\n",
    "```\n",
    "\n",
    "Обратите внимание, что каждая успешная операция начинается с `\"ok\"`, а за ответом сервера всегда указано два символа `\\n`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f37c929b",
   "metadata": {},
   "source": [
    "## Реализация клиента. ##\n",
    "\n",
    "Необходимо реализовать класс `Client`, в котором будет инкапсулировано соединение с сервером, клиентский сокет и методы для получения и отправки метрик на сервер. В конструктор класса `Client` должна передаваться адресная пара хост и порт, а также необязательный аргумент `timeout` (`timeout=None` по умолчанию). У класса `Client` должно быть 2 метода: `put` и `get`, соответствующих протоколу выше.\n",
    "\n",
    "Пример вызова клиента для отправки метрик и затем их получения:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "6f42bcf1",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'huginn.cpu': [(1642667947, 0.5), (1642667948, 2.0), (1642667948, 0.5)], 'muninn.cpu': [(1642667950, 3.0), (1642667951, 4.0)], 'muninn.memory': [(1642748845, 4200000.0)]}\n"
     ]
    }
   ],
   "source": [
    "from client import Client\n",
    "\n",
    "client = Client(\"127.0.0.1\", 8888, timeout=15)\n",
    "\n",
    "client.put(\"huginn.cpu\", 0.5, timestamp=1642667947)\n",
    "client.put(\"huginn.cpu\", 2.0, timestamp=1642667948)\n",
    "client.put(\"huginn.cpu\", 0.5, timestamp=1642667948)\n",
    "\n",
    "client.put(\"muninn.cpu\", 3, timestamp=1642667950)\n",
    "client.put(\"muninn.cpu\", 4, timestamp=1642667951)\n",
    "client.put(\"muninn.memory\", 4200000)\n",
    "\n",
    "print(client.get(\"*\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4acccf13",
   "metadata": {},
   "source": [
    "Клиент получает данные в текстовом виде, метод `get` должен возвращать словарь с полученными ключами с сервера. Значением ключа в словаре является список кортежей `[(timestamp, metric_value), ...]`, отсортированный по `timestamp` от меньшего к большему. Значение timestamp должно быть преобразовано к целому числу `int`. Значение метрики `metric_value` нужно преобразовать к числу с плавающей точкой `float`.\n",
    "\n",
    "Метод `put` принимает первым аргументом название метрики, вторым численное значение, третьим - необязательный именованный аргумент `timestamp`. Если пользователь вызвал метод `put` без аргумента `timestamp`, то клиент автоматически должен подставить текущее время в команду `put - int(time.time())`\n",
    "\n",
    "Метод `put` не возвращает ничего в случае успешной отправки и выбрасывает исключение `ClientError` в случае неуспешной.\n",
    "\n",
    "Метод `get` принимает первым аргументом имя метрики, значения которой мы хотим выгрузить. Также вместо имени метрики можно использовать символ `*`, о котором говорилось в описании протокола.\n",
    "\n",
    "Метод `get` возвращает словарь с метриками (смотрите ниже пример) в случае успешного получения ответа от сервера и выбрасывает исключение `ClientError` в случае неуспешного.\n",
    "\n",
    "Пример возвращаемого значения при успешном вызове `client.get(\"huginn.cpu\")`:\n",
    "\n",
    "```python\n",
    "{\n",
    "  'huginn.cpu': [\n",
    "    (1642667947, 0.5),\n",
    "    (1642667948, 0.5)\n",
    "  ]\n",
    "}\n",
    "```\n",
    "\n",
    "Пример возвращаемого значения при успешном вызове `client.get(\"*\")`:\n",
    "\n",
    "```python\n",
    "{\n",
    "  'huginn.cpu': [\n",
    "    (1642667947, 0.5),\n",
    "    (1642667948, 0.5)\n",
    "  ],\n",
    "  'muninn.cpu': [\n",
    "    (1642667950, 3.0),\n",
    "    (1642667951, 4.0)\n",
    "  ],\n",
    "  'muninn.memory': [\n",
    "    (1642668557, 4200000.0)\n",
    "  ]\n",
    "}\n",
    "```\n",
    "\n",
    "Если в ответ на get-запрос сервер вернул положительный ответ `ok\\n\\n`, но без данных (то есть данных по запрашиваемому ключу нет), то метод `get` клиентадолжен вернуть пустой словарь:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "c83223c7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{}\n"
     ]
    }
   ],
   "source": [
    "print(client.get(\"non_existing_key\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc53e7c0",
   "metadata": {},
   "source": [
    "Обратите внимание, что сервер хранит данные с максимальным разрешением в одну секунду. Это означает, что если в одну и ту же секунду отправить две одинаковые метрики, то будет сохранено только одно значение, которое было обработано последним. Все остальные значения будут перезаписаны.\n",
    "\n",
    "Итак, вам необходимо предоставить модуль с классом `Client`, исключением `ClientError`. В этом классе `Client` должны быть доступны методы `get` и `put` с описанной выше сигнатурой. При вызове методов `get` и `put` клиент должен посылать сообщения в TCP-соединение с сервером в соответствии с описанным текстовым протоколом, получать ответ от сервера, преобразовывать его в удобный для использования формат, описанный выше.\n",
    "\n",
    "Код клиента неудобно разрабатывать и отлаживать без сервера. Для удобства тестирования во время разработки кода клиента мы разработали `unittest`-ты.\n",
    "\n",
    "[test_client.py](test_client.py \"test_client.py\")\n",
    "\n",
    "Используйте данный `unittest` для проверки работы Вашего клиента для отправки метрик. Это ускорит процесс разработки клиента и упростит отладку."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "91854869",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ".....\r\n",
      "----------------------------------------------------------------------\r\n",
      "Ran 5 tests in 0.001s\r\n",
      "\r\n",
      "OK\r\n"
     ]
    }
   ],
   "source": [
    "! python -m unittest test_client.py"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "62e177a7",
   "metadata": {},
   "source": [
    "Успехов при выполнении задания!"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
