Сеть. Простое

веб-приложение

Tinkoff Python

Лекция 3

Пара слов обо мне

занимаюсь чат-ботами

Палий Антон

Тест

С чего начинается

веб-запрос?

Пара слов про транспортный уровень

TCP/UDP

TCP UDP
Transmission  User 
Control Datagram 
Protocol Protocol

Прикладной уровень

DNS

tinkoff.ru -> 178.248.236.218

HTTP

Hypertext Transfer Protocol

~$ telnet google.com 80                                                                                                                                    1 ↵ 
Trying 64.233.165.139...
Connected to google.com.
Escape character is '^]'.
GET /
HTTP/1.0 200 OK
Date: Sat, 22 Feb 2020 18:19:03 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-...
~$ telnet tinkoff.ru 80                                                                                                                                                                                                                                                                                                                                    ░▒▓ ✔ 
Trying 178.248.236.218...
Connected to tinkoff.ru.
Escape character is '^]'.
GET /
Connection closed by foreign host.

Что же пошло не так?

HTTPS

HTTP, в котором данные шифруются

Заголовки

key: value

Пара ключ-значения для передачи дополнительной инфомации между клиентом и сервером

  1. General Headers— есть в любом сообщении клиента и сервера.
  2. Request Headers —  только в запросах клиента.
  3. Response Headers — только для ответов от сервера.
  4. Entity Headers— сопровождают каждую сущность сообщения.

404

слайд не найден

200


  •  

from http import HTTPStatus

Как из программы получить доступ к сетевому интерфейсу?

Socket

Общее

  • Создание
  • Отправка 
  • Получение
  • Закрытие 

Клиент

  • Подключиться 

Сервер

  • Бинд 
  • Слушать 
  • Подтвердить 
import socket


HOST = '127.0.0.1' 
PORT = 1025

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()
    conn, addr = s.accept()
    with conn:
        print('Connected by', addr)
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)
~$ telnet localhost 1025
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /
GET /

Программное выполнение запросов

curl

Консольная утилита для выполнения запросов. 

~$ curl -i tinkoff.ru                                                                                                                                                                                                 2 ↵  flask-testing-mExZZ2-q-py3.7  
HTTP/1.1 301 Moved Permanently
Server: QRATOR
Date: Mon, 24 Feb 2020 16:03:37 GMT
Content-Length: 0
Connection: keep-alive
Keep-Alive: timeout=15
Location: https://tinkoff.ru/

Позволяет Не позволяет
Делать http запросы работать из python
Подключаться по https
Удобный для консоли синтаксис
Действие в одну команду

urllib.request

Стандартное решение из коробки

In [1]: import urllib                                                                                                          

In [2]: f = urllib.request.urlopen('https://tinkoff.ru/')                                                                       

In [3]: f                                                                                                                      
Out[3]: <http.client.HTTPResponse at 0x7f461c847d50>

In [4]: print(f.read(100).decode('utf-8'))                                                                                     
<!DOCTYPE html>
<html class="no-js" lang="ru">
<head>
<meta charset="UTF-8">
<title data-meta-dynami

При работе имеет множество ньюансов, да и в своей же документации рекомендует использовать

Requests

In [1]: import requests                                                                                                        

In [2]: from http import HTTPStatus                                                                                            

In [3]: r = requests.get('https://tinkoff.ru/')                                                                                

In [4]: r.status_code                                                                                                          
Out[4]: 200

In [5]: assert r.status_code == HTTPStatus.OK
  
In [5]: r.headers                                                                                                              
Out[5]: {'Date': 'Tue, 25 Feb 2020 08:30:31 GMT', 'Content-Type': 'text/html; charset=utf-8', 'Transfer-Encoding': 'chunked', 'x-xss-protection': '1', 'x-frame-options': 'sameorigin', 'content-security-policy': "connect-src sync.datamind.ru dpm.demdex.net tinkoffcreditsystems.d3.sc.omtrdc.net assets.adobedtm.com *.omniture.com *.g.doubleclick.net geocode-maps.yandex.ru/1.x/ *.google-analytics.com *.datamind.ru www.cdn-tinkoff.ru 'self' *.tinkoff.ru *.tcsbank.ru www.tinkoff.ru cfg.tinkoff.ru api.tinkoff.ru acdn.tinkoff.ru business.tinkoff.ru; script-src sync.datamind.ru www.google-analytics.com www.googletagmanager.com www.googleadservices.com *.g.doubleclick.net assets.adobedtm.com dpm.demdex.net tinkoffcreditsystems.d3.sc.omtrdc.net *.omniture.com *.google-analytics.com googleads.g.doubleclick.net www.google.com www.google.ru connect.ok.ru vk.com *.datamind.ru s.ytimg.com connect.facebook.net *.tinkoff.ru *.tcsbank.ru www.cdn-tinkoff.ru 'self' 'unsafe-eval' 'unsafe-inline'; img-src *.datamind.ru dpm.demdex.net www.google-analytics.com tinkoffcreditsystems.d3.sc.omtrdc.net cm.everesttech.net stats.g.doubleclick.net dp.adsdata.ru www.google.com www.google.ru top-fwz1.mail.ru vk.com login.vk.com mc.yandex.ru ad.mail.ru adfocus.ru www.facebook.com ad.doubleclick.net *.g.doubleclick.net www.googleadservices.com *.google.com *.yandex.ru *.yandex.net *.2o7.net *.demdex.net cx.atdmt.com analytics.twitter.com t.co eu-sonar.sociomantic.com *.sravni.ru www.banki.ru *.pool.datamind.ru statad.ru googletagmanager.com www.facebook.com/tr/ www.cdn-tinkoff.ru *.tinkoff.ru manalyticshub.com 'self' data: *.tcsbank.ru; frame-src *.tinkoff.demdex.net *.omniture.com bid.g.doubleclick.net www.facebook.com *.demdex.net vk.com static.datamind.ru platform.twitter.com connect.ok.ru *.datamind.ru www.cdn-tinkoff.ru 'self' *.tinkoff.ru *.tcsbank.ru; report-uri https://www.tinkoff.ru/api/front/log/csp-error; default-src 'self' www.cdn-tinkoff.ru *.tinkoff.ru data:; style-src 'unsafe-inline' 'self' *.tinkoff.ru *.tcsbank.ru www.cdn-tinkoff.ru", 'expires': '0', 'pragma': 'no-cache', 'cache-control': 'no-cache, no-store, must-revalidate', 'set-cookie': 'dco.id=8da2752b-1ed0-4751-b6ce-0fb74d9c0b94; Path=/; Expires=Thu, 25 Feb 2021 08:30:31 GMT; SameSite=Lax', 'x-envoy-upstream-service-time': '154', 'vary': 'Accept-Encoding', 'content-encoding': 'gzip', 'Server': 'MSX Turbo R (R900) Web Server 1.13', 'X-Powered-By': 'MSX Basic Web Clustered Engine (WCE pi) - NG'}

In [6]: r.text                                                                                                                 
Out[6]: '<!DOCTYPE html>\n<html class="no-js" lang="ru">\n<head>\n<meta charset="UTF-8">\n<title data-meta-dynamic="true">Тинькофф — Кредитные и дебетовые карт

timeout

всегда указывайте явно

In [1]: import requests                                                                                                        

In [2]: r = requests.get('https://tinkoff.ru/')                                                                                

In [3]: r                                                                                                                      
Out[3]: <Response [200]>

In [3]: r = requests.get('https://tinkoff.ru/', timeout=0.01)
---------------------------------------------------------------------------
timeout                   Traceback (most recent call last)

Перерыв

Объединим сеть и сокет

Выберем фреймфорк

  • Django
  • Flask
  • Sanic
  • FastAPI
  • Bottle
  • Falcon

Напишем простое приложение

from flask import Flask

app = Flask(__name__)

@app.route('/')
def main():
    return "kek"

FLASK_APP=app.py PYTHONPATH=$(pwd)/app_module flask run 
* Tip: There are .env or .flaskenv files present. Do "pip install python-dotenv" to use them.
 * Serving Flask app "app.py"
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
telnet localhost 5000                                                                                
Connected to localhost.
Escape character is '^]'.
GET /

kekConnection closed by foreign host.

Добавим немного красоты

# добавим h1 тег к нашему тексту
from flask import Flask

app = Flask(__name__)

@app.route('/')
def main():
    return "<h1>lol</h1>"

Тесты

import pytest

from app import app  


@pytest.fixture
def client():
    app.config['TESTING'] = True
    with app.test_client() as client:
        yield client
    

def test_connection(client):
    result = client.get('/')
    assert result.status == '200 OK'
    assert result.data == b'<h1>lol</h1>'
from flask import Flask

app = Flask(__name__)

@app.route('/')
def main():
    return "kek"

FLASK_APP=app.py PYTHONPATH=$(pwd)/app_module flask run 
* Tip: There are .env or .flaskenv files present. Do "pip install python-dotenv" to use them.
 * Serving Flask app "app.py"
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

WSGI

Стандарт взаимодействия для веб-серверов типа nginx и веб-приложений на языке python.

Является низкоуровневым описанием работы приложения.

Pep 3333

  • должно быть вызываемым (callable) объектом (обычно это функция или метод)

  • принимать два параметра:

    • словарь переменных окружения (environ)

    • обработчик запроса (start_response)

  • вызывать обработчик запроса с кодом HTTP-ответа и HTTP-заголовками

  • возвращать итерируемый объект с телом ответа

HELLO_WORLD = b"Hello world!\n"


def simple_app(environ, start_response):
    """Simplest possible application object"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [HELLO_WORLD]
  
  
if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    srv = make_server('localhost', 8080, simple_app)
    srv.serve_forever()

Gunicorn

Web Server Gateway Interface HTTP server

Gunicorn решает несколько проблем

  1. Запуск приложения
  2. Масштабирование приложения 
  3. Конфигурирование приложения
  4. Балансировка трафика
  5. Контроль приложения (Перезагрузки, размер очереди и пр)
  6. SSL
  7. Обработка сигналов
  8. Работа с веб серверами
  9. и пр
gunicorn \
  --workers 4 \
  --timeout 2000 \
  --max-requests 100000 \
  --bind 0.0.0.0:5000 \
    app_module:app()

Напишем приложение посложнее

HTML

from uuid import uuid4

from flask import Flask

app = Flask(__name__)
response_base_body = """<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    {}
</body>
</html>"""

@app.route('/')
def main():
    return response_base_body.format(uuid4())

Теперь мы отдаём правильный HTML, но красивый ли код?

Файлы?

Модули?

Шаблоны!

jinja

  • Автоматизировать работу с шаблонами
  • Автоматически избегать XSS инъекции
  • Наследовать шаблоны
  • Шаблозизировать
  • Избавлять код от html

Jinja позволяет

├── project
│   └── app.py
└── templates
    └── base.html
from uuid import uuid4

from flask import Flask, render_template

server = Flask('some_name')

@server.route('/')
def main():
    return render_template('base.html', data=uuid4())
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
	{{ data }}
</body>
</html>

Text

Что-то сложнее

├── project
│   └── app.py
└── templates
    └── base.html
@server.route('/')
def main():
    users = {}
    for i in range(100):
        users[i] = uuid4()
    return render_template('base.html', users=users)
<body>
  <table style="width:100%">
    <tr>
      <th>id</th>
      <th>data</th>
    </tr>
    {% for user_id, user_name in users.items() %}
    <tr>
      <th> {{ user_id }} </th>
      <th> {{ user_name }} </th>
    </tr>
    {% endfor %}
  </table>
</body>
{% ... %} Выражения

{{ ... }} Вывод значения

{# ... #} Комментарии

Основные разделители в jinja

Выражения

{% for var in vars %}

{% endfor %}

{% if a == '' %}

{% endif %}

{% macro input(name) -%}
    <div> "{{ name }}" </div>
{%- endmacro %}

{% extends "base.html" %}

{% block title %}some data{% endblock %}

Вывод значения

{{ name }}

{{ 1 + 1 }}

{{ var is some_test }}
{% if variable is defined %}

{{ name | filter }}
{{ my_variable|default('kek') }}

Линтеры

Flake8

$ flake8 path/to/code/
app/__init__.py:1:1: W391 blank line at end of file
app/utils.py:7:2: E225 missing whitespace around operator
app/utils.py:10:1: E402 module level import not at top of file
app/utils.py:10:1: F401 'os' imported but unused
app/__main__.py:1:1: F401 '.utils' imported but unused

Flake8 plugins

  • flake8-builtins
  • flake8-comprehensions
  • flake8-eradicate
  • flake8-isort
  • flake8-logging-format
  • flake8-pytest
  • pep8-naming
  • etc.

Pylint

$ pylint path/to/code
************* Module app.utils
app/utils.py:7:1: C0326: Exactly one space required around assignment
x=2
 ^ (bad-whitespace)
app/utils.py:1:0: C0111: Missing module docstring (missing-docstring)
app/utils.py:5:0: C0103: Constant name "x" doesn't conform to UPPER_CASE naming style (invalid-name)
app/utils.py:7:0: C0103: Constant name "x" doesn't conform to UPPER_CASE naming style (invalid-name)
app/utils.py:10:0: C0413: Import "import os" should be placed at the top of the module (wrong-import-position)
app/utils.py:10:0: W0611: Unused import os (unused-import)
# my_strange_module.py
# pylint:disable=missing-docstring
import os # noqa: F401

Отключение проверок

Настройки линтеров

[flake8]
enable-extensions = G
exclude = .git
ignore =
    A003 ; 'id' is a python builtin, consider renaming the class attribute
    W504 ; Line break occurred after a binary operator
max-complexity = 20
max-line-length = 80
show-source = true

[pylint]  ; `pylint --rcfile=setup.cfg my_code`
good-names=i,j,k,e,x,_,pk,id
max-module-lines=300
output-format = colorized

disable=
    C0103, ; Constant name "api" doesn't conform to UPPER_CASE naming style (invalid-name)
    C0111, ; Missing module docstring (missing-docstring)
    C0330, ; Wrong hanging indentation before block (add 4 spaces)
    E0213, ; Method should have "self" as first argument (no-self-argument) - N805 for flake8
    R0201, ; Method could be a function (no-self-use)
    R0901, ; Too many ancestors (m/n) (too-many-ancestors)
    R0903, ; Too few public methods (m/n) (too-few-public-methods)
    W0511, ; TODO needed? (fixme)

setup.cfg

Автоформатирование кода

Black

# BEFORE
x = {  'a':37,'b':42,

'c':927}

y = 'hello ''world'
z = 'hello '+'world'
a = 'hello {}'.format('world')
class foo  (     object  ):
  def f    (self   ):
    return       37*-+2
  def g(self, x,y=42):
      return y
def f  (   a ) :
  return      37+-+a[42-x :  y**3]
# AFTER
x = {"a": 37, "b": 42, "c": 927}

y = "hello " "world"
z = "hello " + "world"
a = "hello {}".format("world")


class foo(object):
    def f(self):
        return 37 * -+2

    def g(self, x, y=42):
        return y


def f(a):
    return 37 + -+a[42 - x : y ** 3]
$ black app

Isort

$ isort --apply --recursive app
...
from my_lib import Object

print("Hey")

import os

from my_lib import Object3

from my_lib import Object2

import sys

from third_party import libA
import sys

from third_party import libB

print("yo")
import os
import sys

from third_party import libA, libB

from my_lib import Object, Object2, Object3

print("Hey")
print("yo")

Работа с зависимостями

Сеть

By antonpaliy

Сеть

  • 343