Финтех python

Лекция 4

94210-67030

Кто не сделал 2 домашнюю работу?

typing

pep-484

class Message:
    ...

def some_func(data: dict) -> str:
    ...


def other_func(message: Message) -> None:
    ...

На кой оно надо?

Язык то динамический!

Duck typing

Если крякает и плавает

class Long:
    def __len__(self):
        return 282

assert len(Long()) == 282
def len(obj):
    return obj.__len__()

Статический анализ

В момент написания кода

class Message:
    ...

def some_func(data: dict) -> str:
    ...


def other_func(message: Message) -> None:
    ...

Как это работает?

some_func.__annotations__
{
    'data': dict,
    'return': str
}

other_func.__annotations__
{
    'message': __main__.Message, 
    'return': None
}

И все

  • Нет проверок
  • Нет ограничений
  • Только один дополнительный словарь

Что это нам дает?

def some_func() -> typing.Tuple[str, int]:
    return 'a', 1

def other_func() -> typing.List[int]:
    return [1, 2, 3, 4]

mypy

Утилита для проверки кода

mypy-lang.org

def add(x: int, y: int) -> int:
    return x + y + z

result = add('kek', 'lol') + '!'
error: Name 'z' is not defined
error: Argument 1 to "add" has incompatible type "str"; expected "int"
error: Argument 2 to "add" has incompatible type "str"; expected "int"
error: Unsupported operand types for + ("int" and "str")

mypy example

Подсветка синтаксиса

import typing


class Message(typing.NamedTuple):
    id: int
    timestamp: datetime.datetime
    text: str
from dataclasses import dataclass

@dataclass()
class Message:
    id: int
    timestamp: datetime.datetime
    text: str

"Проблемы"

Self refs

class Some:
    def some_method(self) -> Some:
        ...

Self refs

class Some:
    def some_method(self) -> 'Some':
        ...

Кольцевой импорт

# message.py
from .dialog import Dialog

class Message:
    def some_method(self) -> Dialog:
        ...

# dialog.py
from .message import Message

class Dialog:
    def other_method(self) -> Message:
        ...

Кольцевой импорт

# message.py
from .dialog import Dialog
# message.py
if typing.TYPE_CHECKING:
    from .dialog import Dialog

Кольцевой импорт

# message.py
if typing.TYPE_CHECKING:
    from .dialog import Dialog

class Message:
    def some_method(self) -> Dialog:
        ...

# dialog.py
if typing.TYPE_CHECKING:
    from .message import Message

class Dialog:
    def other_method(self) -> Message:
        ...

Метаклассы

Либы без типов

Stubs

Решение для метаклассов и для либ

typeshed - github.com/python/typeshed

others stubs - github.com/dropbox/sqlalchemy-stubs


class Path(PurePath):
    @classmethod
    def cwd(cls: Type[_P]) -> _P: ...
    def stat(self) -> os.stat_result: ...
    def chmod(self, mode: int) -> None: ...
    def exists(self) -> bool: ...
    def group(self) -> str: ...
    def is_dir(self) -> bool: ...
    def is_file(self) -> bool: ...
    def is_symlink(self) -> bool: ...
    def is_socket(self) -> bool: ...
# и т.д

Stubs

Файлы *.pyi

Stub gen

Сеть

Питон в сети

Сеть не надежна

Ваши примеры ненадежной сети

Клиент

Сеть

Сервис

t

t

t

Типичное решение

timeout'ы

Еще больше вопросов

  • Получил ли сервис наш запрос или запрос на обратном пути умер
  • Какой таймаут выставить
  • Каскадные сбои

Главное правило

Всегда выставлять timeout'ы!

Гнезда

sockets

import socket

with socket.socket(
    socket.AF_INET, 
    socket.SOCK_STREAM) as s:
    ...

TCP

import socket

with socket.socket() as s:
    ...
import socket

HOST = '127.0.0.1'
PORT = 65432
with socket.socket() 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)

Echo Server

  • socket()
  • bind()
  • listen()
  • accept()
  • connect()
  • connect_ex()
  • send()
  • recv()
  • close()

Socket API

import socket

HOST = '127.0.0.1'
PORT = 65432

with socket.socket() as s:
    s.connect((HOST, PORT))
    s.sendall(b'Hello')
    data = s.recv(1024)

print('Received', repr(data))

Echo Client

Типы сокетов

  1. AF_INET
  2. AF_UNIX

Типы потоков

  1. SOCK_STREAM
  2. SOCK_DGRAM
  3. SOCK_RAW

unix socket

unix:///some_file

sock raw

>>> from scapy.all import *
>>> conf.verb = 0
>>> p = IP(dst="tinkoff.ru") / TCP()
>>> r = sr1(p)
>>> print(r.summary())

IP / TCP 10.218.84.1:http > 
    172.18.57.5:ftp_data SA / Padding

Scapy

setsockopt

Позволяет настраивать свойства и поведение

  • IP_FREEBIND
  • IP_PKTINFO
  • SO_REUSEADDR
socket.setsockopt(socket.SOL_SOCKET, 
                  socket.SO_REUSEADDR, 1)

0.0.0.0

import socketserver

import socketserver


class MyTCPHandler(socketserver.BaseRequestHandler):
    def handle(self):
        self.data = self.request.recv(1024).strip()
        print('Connected by', self.client_address[0])
        self.request.sendall(self.data)


if __name__ == "__main__":
    HOST, PORT = "127.0.0.1", 65432

    with socketserver.TCPServer((HOST, PORT),
                                MyTCPHandler) as server:
        server.serve_forever()

Поддерживает потоки

import struct

Unpack bytes to values

In [1]: struct.pack('II', 459, 2123)
Out[1]: b'\xcb\x01\x00\x00K\x08\x00\x00'

In [2]: b = (b'\xff\xff\xff\xff'
   ...:      b'\xff\xff\xff\xff')

In [3]: struct.unpack('II', b)
Out[3]: (4294967295, 4294967295)

In [4]: struct.unpack('ff', b)
Out[4]: (nan, nan)
import ctypes

class UserPassword(ctypes.Structure):
    _fields_ = [
        ('id', ctypes.c_uint32),
        ('hash', ctypes.c_uint32)
    ]

    @classmethod
    def from_id_and_password(cls, user_id, password):
        pwd = hashlib.sha256(password.encode()).digest()

        record = cls()

        record.id = user_id
        record.hash = ctypes.c_uint32.from_buffer_copy(pwd)

        return record

import ctypes

Pack and unpack bytes to values

HTTP

curl http://tinkoff.ru

import socket

ip = socket.gethostbyname('tinkoff.ru')

http = b"""\
GET / HTTP/1.1
Host: tinkoff.ru
Connection: close\n
"""

with socket.socket() as s:
    s.connect((ip, 80))
    s.sendall(http)
    response = s.recv(1024)

print(response.decode('utf8'))
HTTP/1.1 301 Moved Permanently
Content-length: 0
Location: https://tinkoff.ru/
Connection: close

Результат

curl https://tinkoff.ru

Типы http запросов

  1. head
  2. get
  3. post
  4. put
  5. delete
  6. ...
  7. PROFIT

Статусы ответов

In [1]: from http import HTTPStatus

In [2]: HTTPStatus.OK
Out[2]: <HTTPStatus.OK: 200>

In [3]: HTTPStatus.NOT_FOUND
Out[3]: <HTTPStatus.NOT_FOUND: 404>

In [4]: HTTPStatus.BAD_GATEWAY == 502
Out[4]: True

In [5]: HTTPStatus(404)
Out[5]: <HTTPStatus.NOT_FOUND: 404>

import http

import http.server

python -m http.server 8000

from http.server import (BaseHTTPRequestHandler,
                         HTTPServer)

class SomeServer(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)

        self.send_header('Content-type', 'text/html')
        self.end_headers()

        self.wfile.write(b'Hello')
        return


if __name__ == '__main__':
    server_address = ('127.0.0.1', 8081)
    httpd = HTTPServer(server_address, SomeServer)
    httpd.serve_forever()

HTTP server

Остальные модули http

import urllib.parse

from urllib.parse import urlparse

o = urlparse('http://www.cwi.nl:80/%7Eguido/Python.html')
ParseResult(scheme='http', netloc='www.cwi.nl:80', 
            path='/%7Eguido/Python.html',
            params='', query='', fragment='')

urlparse

from urllib.parse import ParseResult

result = ParseResult('http', 'tinkoff.ru', 
                     '', '', '', '')

result.geturl()
'http://tinkoff.ru'

urljoin

import urllib

url = 'http://tinkoff.ru'
with urllib.request.urlopen(url) as f:
    print(f.read()[:10])
b'<!DOCTYPE '

pip install requests

import requests

r = requests.get('http://tinkoff.ru', 
                 allow_redirects=False)

r.content
b''

r.headers
{
'Content-length': '0', 
'Location': 'https://tinkoff.ru/'
}

pip install furl

>>> from furl import furl
>>> u = 'http://www.google.com/?one=1&two=2'
>>> f = furl(u)
>>> f.args['three'] = '3'
>>> del f.args['one']
>>> f.url
'http://www.google.com/?two=2&three=3'

pathlib.Path в мире url

Не делайте так

request.get('http://some_host/some_url?args=val')
request.get('http://some_host/some_url', 
            params={'args': 'val'})

Только так:

CGI

Common Gateway Interface

print("Content-Type: text/html")
print()

Минимальный CGI скрипт

import cgi

python -m http.server --cgi 8000

Минусы?

import wsgi

Web Server Gateway Interface

pep-3333

def simplest_wsgi_app(environ, start_response):
     headers = [(b'Content-Type', b'text/plain')]
     start_response(b'200 OK', headers)
     yield b'Hello, world!'

Minimal WSGI APP

WSGI протокол

  • python функция
  • два аргумента (environ, start_request)
  • environ это заголовки запроса
  • start_request это конец заголовков
  • результат функции это http response

WSGI server

Gateway

WSGI application \

framework

callable

import wsgiref

with make_server('', 8000, some_app) as server:
    server.serve_forever()

uWSGI

uwsgi --http :8080 --wsgi-file some_file.py
  • Быстрый
  • С багами
  • Сложный
  • Много прибамбасов

Gunicorn

gunicorn -w 4 some_file:app
  • Немного медленней чем uwsgi
  • Стабильней
  • Проще
  • Достаточно гибкий

Домашняя работа

Клиент серверное приложение на сокетах

Python fintech 4

By Denis Kataev

Python fintech 4

  • 433