Моята първа библиотека на Python, използваща EOD Historical Data (EODHD APIs)

В моята предишна статия представих API за търговски данни, който открих, наречен „EOD Historical Data (EODhd)“.



API има реален потенциал, тъй като обслужва над 70+ борси по целия свят, включително 150 000+ тикера, 20000+ ETF, 600+ индекса и 1100+ валутни двойки.

Много се интересувах да видя как мога да го включа в един от моите проекти. Те имат безплатно ниво, което позволява 20 API извиквания на ден и 20 API извиквания на минута. Регистрирах се за тяхната „платена услуга“ от начално ниво. £19,99 не е съвсем евтино, но за 100 000 API разговора на ден и 1000 API разговора на минута не можете да се оплачете. Това е мечтата на един алгоритмичен програмист!

Накара ме да се замисля дали имат крайна точка на WebSockets и ако да, колко струва. Поразрових се малко в сайта им и открих това:

[НОВ] API за данни в реално време (WebSockets)
https://eodhistoricaldata.com/financial-apis/new-real-time-data-api-websockets

... и вярвате ли, че е безплатно! (до 50 тикера)

Четох тяхната документация и е много ясна. Това, което ме изненада е, че все още никой не е създал библиотека на Python за това. Никога преди не съм създавал и публикувал библиотека, така че реших, че това е идеалната възможност.

Както обикновено, използвах любимия си ресурс Medium, за да видя дали някой вече е написал статия за него. Намерих три полезни статии. Един от Kia Eisinga, един от Aveek Das и другият от Joffrey Bienvenu.

Ще предоставя обобщените стъпки за това как създадох и публикувах първата си библиотека. Ако искате да се потопите по-дълбоко в детайлите, препоръчвам да прочетете статиите на авторите, споменати по-горе.

Стъпка 1: Инициализирайте нашия проект

Влязох в GitHub и създадох нов публичен проект. Нищо специално тук, просто нарекох моите „eodhistoricaldata“. Добра практика е да добавяте лиценз към проекти с отворен код и аз използвах „MIT License»“ за моя.

След като моето хранилище на GitHub беше създадено, го клонирах в моята локална система. Имаше инструкции как да завършите тази стъпка, след като хранилището бъде създадено.

Стъпка 2: Python, pip и виртуална среда

Ще ви трябва скорошна версия на Python, pip (мениджър на пакети на Python) и виртуална среда, за да продължите. Предполагам, че ако четете тази статия, вече имате настройка на средата и имате разумни работни познания за Python. И ако не, ето няколко ресурса, които да ви помогнат да започнете:

Инсталиране на Python:

https://realpython.com/installing-python

Инсталиране на pip:



Инсталиране на „venv“:

$ python3 -m pip install venv

След като инсталирате Python и pip, ще искате да създадете виртуална среда. Това се изисква при създаване на библиотека на Python, тъй като всичко, свързано с библиотеката, трябва да се съдържа на едно и също място.

Това е обобщение на стъпките, които изпълних:

% git clone https://github.com/whittlem/eodhistoricaldata
Cloning into 'eodhistoricaldata'...
remote: Enumerating objects: 51, done.
remote: Counting objects: 100% (51/51), done.
remote: Compressing objects: 100% (39/39), done.
remote: Total 51 (delta 15), reused 29 (delta 7), pack-reused 0
Receiving objects: 100% (51/51), 12.16 KiB | 1.10 MiB/s, done.
Resolving deltas: 100% (15/15), done.
% cd eodhistoricaldata 
eodhistoricaldata % python3 -m venv venv
eodhistoricaldata % source venv/bin/activate
(venv) eodhistoricaldata %

В този момент структурата на директорията на високо ниво изглежда така:

.
|____.git
|____.gitignore
|____LICENSE
|____README.md
|____venv

Вие също ще искате да надстроите pip във виртуалната среда:

eodhistoricaldata % python3 -m pip install --upgrade pip

Стъпка 3: Инсталирайте инструменти за улесняване на създаването на библиотека

eodhistoricaldata % python3 -m pip install wheel
eodhistoricaldata % python3 -m pip install twine

Стъпка 4: Създаване на структурата на директорията на библиотеката

Следващата стъпка е да създадете заместващи директории и файлове по-долу с удебелен шрифт. В моя случай библиотеката ми се нарича „eodhistoricaldata“, а файлът ми с клас се нарича „websocketclient“.

Когато приключим, нашият импорт ще изглежда така:
from eodhistoricaldata import WebSocketClient

Ако създавате своя библиотека, клас и функции, просто ще ги замените по подходящ начин.

.
|____.git
|____.gitignore
|____LICENSE
|____venv
|____requirements.txt (file: needed for Github unit tests)
|____tests (directory: unit test files)
| |______init__.py (file: empty file)
| |____test_websocketclient.py (file: empty file)
|____MANIFEST.in (file: empty file)
|____README.md
|____eodhistoricaldata (directory: library name)
| |______init__.py (file: empty file)
| |____websocketclient.py (file: empty file)
|____setup.py (file: empty file for now)

Стъпка 5: Писане на кода

Ще обясня файловете и директориите отгоре надолу.

requirements.txt
Този файл не е необходим за самата библиотека. Причината, поради която съществува, е, че добавих GitHub Workflow Actions за автоматизиране на модулни тестове в хранилището. Този файл трябва да съдържа всички необходими зависимости.

black==21.9b0
pylint==2.11.1
pytest==6.2.5
pytest-runner==5.3.1
websocket-client==1.2.1
websockets==10.0

Основните библиотеки са „websocket-client“ и „websockets“. Другите са програми за форматиране на код, линтери и „pytest“ за тестване на единици.

Удобна команда за намиране и идентифициране на какви библиотеки на зависимости се използват в проект на Python е:

eodhistoricaldata % python3 -m pip freeze

тестове
Това е директория, съдържаща модулните тестове за проекта.

tests/__init__.py
Този файл е необходим, за да се уверите, че директорията „tests” е включена в библиотеката. В повечето прости случаи можете просто да го оставите празно.

tests/test_websocket.py
Този файл съдържа модулните тестове за библиотеката. В моя случай току-що добавих няколко теста към един файл. За по-изчерпателно тестване можете да създадете един или повече файлове с префикса „test_‹name›.py“.

Само за да ви дам представа как изглеждат pytests:

eodhistoricaldata % cat tests/test_websocketclient.py 
"""Unit tests for WebSocketClient"""
import pytest
from eodhistoricaldata import WebSocketClient
def test_api_key_invalid():
    """API key is invalid"""
    with pytest.raises(ValueError) as execinfo:
        websocket = WebSocketClient(api_key="", endpoint="", symbols=[])
        assert isinstance(websocket, WebSocketClient)
    assert str(execinfo.value) == "API key is invalid"
def test_endpoint_invalid():
    """Endpoint is invalid"""
    with pytest.raises(ValueError) as execinfo:
        websocket = WebSocketClient(
            api_key="00000000000000000000000000000000", endpoint="", symbols=[]
        )
        assert isinstance(websocket, WebSocketClient)
    assert str(execinfo.value) == "Endpoint is invalid"
def test_symbols_empty():
    """No symbol(s) provided"""
    with pytest.raises(ValueError) as execinfo:
        websocket = WebSocketClient(
            api_key="00000000000000000000000000000000", endpoint="crypto", symbols=[]
        )
        assert isinstance(websocket, WebSocketClient)
    assert str(execinfo.value) == "No symbol(s) provided"
def test_symbols_invalid():
    """Symbol is invalid: !"""
    with pytest.raises(ValueError) as execinfo:
        websocket = WebSocketClient(
            api_key="00000000000000000000000000000000", endpoint="crypto", symbols=["!"]
        )
        assert isinstance(websocket, WebSocketClient)
    assert str(execinfo.value) == "Symbol is invalid: !"
def test_instantiate_success():
    """Instantiate success"""
    websocket = WebSocketClient(
        api_key="00000000000000000000000000000000", endpoint="crypto", symbols=["BTC-USD"]
    )
    assert isinstance(websocket, WebSocketClient

И да ги стартирате е много просто. Искам да добавя опцията “-v” за подробен изход.

eodhistoricaldata % pytest tests -v

MANIFEST.in
Този файл ви позволява да включите допълнителни ресурси към вашата библиотека, които иначе биха били изключени. В основно изпълнение като това можете просто да го оставите празно.

eodhistoricaldata
В основата на вашия проект трябва да имате директория, съдържаща кода на вашата библиотека. Трябва да е същото име като вашата библиотека. Например, имам проектна директория, наречена „eodhistoricaldata“, която е моето клонирано репо от GitHub, и в тази директория имам поддиректория, наречена „eodhistoricaldata“, която съдържа действителния код.

eodhistoricaldata/__init__.py
Този файл е необходим, за да се уверите, че директорията „eodhistoricaldata” е включена в библиотеката. Ще искате да включите командата „от“, както направих по-долу. Това ще означава, че вашата библиотека може да бъде заредена с „от eodhistoricaldata import WebSocketClient“ вместо „ffrom eodhistoricaldata.websocketclient import WebSocketClient“. И двете ще работят, но първото е по-хубаво. Ще видите също, че включих версията на библиотеката.

""" __init__.py """
from eodhistoricaldata.websocketclient import WebSocketClient
# Version of eodhistoricaldata package
__version__ = "0.2.0"

eodhistoricaldata/websocketclient.py
Този файл съдържа кода на библиотеката ми. Тази библиотека е много проста, така че е необходим само един файл.



setup.py
Това може би е най-важният файл в библиотеката. Това е конфигурационният файл, който определя вашата библиотека.

eodhistoricaldata % cat setup.py 
"""Setup file for PyPI"""
# To use a consistent encoding
from os import path
# Always prefer setuptools over distutils
from setuptools import setup, find_packages
# The directory containing this file
HERE = path.abspath(path.dirname(__file__))
# Get the long description from the README file
with open(path.join(HERE, 'README.md'), encoding='utf-8') as f:
    long_description = f.read()
# This call to setup() does all the work
setup(
    name="eodhistoricaldata",
    version="0.2.0",
    description="EOD Historical Data Python Library (Unofficial)",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://whittle.medium.com",
    author="Michael Whittle",
    author_email="[email protected]",
    license="MIT",
    classifiers=[
        "Intended Audience :: Developers",
        "License :: OSI Approved :: MIT License",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.7",
        "Operating System :: OS Independent"
    ],
    packages=find_packages(include=["eodhistoricaldata"]),
    include_package_data=True,
    install_requires=["websockets==10.0","websocket-client==1.2.1"],
    entry_points={
        "console_scripts": [
            "whittlem=eodhistoricaldata.__main__:main",
        ]
    },
    setup_requires=["pytest-runner"],
    tests_require=["pytest==6.2.5"],
    test_suite="tests"
)

Няма да навлизам в подробности какво означават всички тези опции за конфигуриране, тъй като те се обясняват сами по себе си. За вашата библиотека просто ще използвате това като основа и ще замените полетата по подходящ начин.

Стъпка 5: Изграждане на библиотеката за локално изпълнение

В моя случай във Visual Studio Code мога да стартирам директно „eodhistoricaldata/websockets.py“, защото добавих това в долната част.

Не се плашете, че виждате API ключове в кода по-долу. Тези API ключове са документирани на публичния уебсайт EOD Historical Data (EODhd) за целите на тестването.

def main() -> None:
    """Main"""
websocket = WebSocketClient(
        # Demo API key for testing purposes
        api_key="OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX", endpoint="crypto", symbols=["BTC-USD"]
        #api_key="OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX", endpoint="forex", symbols=["EURUSD"]
        #api_key="OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX", endpoint="us", symbols=["AAPL"]
    )
    websocket.start()
message_count = 0
    while True:
        if websocket:
            if (
                message_count != websocket.message_count
            ):
                print(websocket.message)
                message_count = websocket.message_count
                sleep(0.25)  # output every 1/4 second
if __name__ == "__main__":
    main()

Потвърдихме, че нашата библиотека работи и как искаме да я изградим и инсталираме локално, за да видим как работи.

Ако вече не сте го направили, уверете се, че сте във вашата виртуална среда.

eodhistoricaldata % source venv/bin/activate
(venv) eodhistoricaldata %

След това стартирайте изграждането.

(venv) eodhistoricaldata % python3 setup.py sdist bdist_wheel
running sdist
running egg_info
writing eodhistoricaldata.egg-info/PKG-INFO
writing dependency_links to eodhistoricaldata.egg-info/dependency_links.txt
writing entry points to eodhistoricaldata.egg-info/entry_points.txt
writing requirements to eodhistoricaldata.egg-info/requires.txt
writing top-level names to eodhistoricaldata.egg-info/top_level.txt
reading manifest file 'eodhistoricaldata.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
adding license file 'LICENSE'
writing manifest file 'eodhistoricaldata.egg-info/SOURCES.txt'
running check
creating eodhistoricaldata-0.2.0
creating eodhistoricaldata-0.2.0/eodhistoricaldata
creating eodhistoricaldata-0.2.0/eodhistoricaldata.egg-info
copying files to eodhistoricaldata-0.2.0...
copying LICENSE -> eodhistoricaldata-0.2.0
copying MANIFEST.in -> eodhistoricaldata-0.2.0
copying README.md -> eodhistoricaldata-0.2.0
copying setup.py -> eodhistoricaldata-0.2.0
copying eodhistoricaldata/__init__.py -> eodhistoricaldata-0.2.0/eodhistoricaldata
copying eodhistoricaldata/websocketclient.py -> eodhistoricaldata-0.2.0/eodhistoricaldata
copying eodhistoricaldata.egg-info/PKG-INFO -> eodhistoricaldata-0.2.0/eodhistoricaldata.egg-info
copying eodhistoricaldata.egg-info/SOURCES.txt -> eodhistoricaldata-0.2.0/eodhistoricaldata.egg-info
copying eodhistoricaldata.egg-info/dependency_links.txt -> eodhistoricaldata-0.2.0/eodhistoricaldata.egg-info
copying eodhistoricaldata.egg-info/entry_points.txt -> eodhistoricaldata-0.2.0/eodhistoricaldata.egg-info
copying eodhistoricaldata.egg-info/requires.txt -> eodhistoricaldata-0.2.0/eodhistoricaldata.egg-info
copying eodhistoricaldata.egg-info/top_level.txt -> eodhistoricaldata-0.2.0/eodhistoricaldata.egg-info
Writing eodhistoricaldata-0.2.0/setup.cfg
Creating tar archive
removing 'eodhistoricaldata-0.2.0' (and everything under it)
running bdist_wheel
running build
running build_py
installing to build/bdist.macosx-11-x86_64/wheel
running install
running install_lib
creating build/bdist.macosx-11-x86_64/wheel
creating build/bdist.macosx-11-x86_64/wheel/eodhistoricaldata
copying build/lib/eodhistoricaldata/__init__.py -> build/bdist.macosx-11-x86_64/wheel/eodhistoricaldata
copying build/lib/eodhistoricaldata/websocketclient.py -> build/bdist.macosx-11-x86_64/wheel/eodhistoricaldata
running install_egg_info
Copying eodhistoricaldata.egg-info to build/bdist.macosx-11-x86_64/wheel/eodhistoricaldata-0.2.0-py3.9.egg-info
running install_scripts
adding license file "LICENSE" (matched pattern "LICEN[CS]E*")
creating build/bdist.macosx-11-x86_64/wheel/eodhistoricaldata-0.2.0.dist-info/WHEEL
creating 'dist/eodhistoricaldata-0.2.0-py3-none-any.whl' and adding 'build/bdist.macosx-11-x86_64/wheel' to it
adding 'eodhistoricaldata/__init__.py'
adding 'eodhistoricaldata/websocketclient.py'
adding 'eodhistoricaldata-0.2.0.dist-info/LICENSE'
adding 'eodhistoricaldata-0.2.0.dist-info/METADATA'
adding 'eodhistoricaldata-0.2.0.dist-info/WHEEL'
adding 'eodhistoricaldata-0.2.0.dist-info/entry_points.txt'
adding 'eodhistoricaldata-0.2.0.dist-info/top_level.txt'
adding 'eodhistoricaldata-0.2.0.dist-info/RECORD'
removing build/bdist.macosx-11-x86_64/wheel

Това ще изгради нашата библиотека в новосъздадена директория „dist“.

(venv) eodhistoricaldata % ls dist    
eodhistoricaldata-0.2.0-py3-none-any.whl eodhistoricaldata-0.2.0.tar.gz

Файлът, който ни интересува, е „.whl”. Това е, което можем да инсталираме сега с pip.

(venv) eodhistoricaldata % python3 -m pip install dist/eodhistoricaldata-0.2.0-py3-none-any.whl 
Processing ./dist/eodhistoricaldata-0.2.0-py3-none-any.whl
Requirement already satisfied: websocket-client==1.2.1 in ./venv/lib/python3.9/site-packages (from eodhistoricaldata==0.2.0) (1.2.1)
Requirement already satisfied: websockets==10.0 in ./venv/lib/python3.9/site-packages (from eodhistoricaldata==0.2.0) (10.0)
Installing collected packages: eodhistoricaldata
  Attempting uninstall: eodhistoricaldata
    Found existing installation: eodhistoricaldata 0.1.0
    Uninstalling eodhistoricaldata-0.1.0:
      Successfully uninstalled eodhistoricaldata-0.1.0

Вече ще можете да зареждате библиотеката си в скриптове като този.

from eodhistoricaldata import WebSocketClient

Дотук добре, но сега е време да публикуваме нашата библиотека, за да могат други да я използват.

Стъпка 6: Регистрация в Test PyPI и PyPI

За да публикувате библиотека, ще ви трябва акаунт в Test PyPI и PyPI. Ако все още нямате акаунти в тези сайтове, моля, създайте ги сега.

https://test.pypi.org

https://pypi.org

Стъпка 7: Изпълнете тестове, за да потвърдите, че библиотеката изглежда добре за публикуване

(venv) eodhistoricaldata % twine check dist/*
Checking dist/eodhistoricaldata-0.2.0-py3-none-any.whl: PASSED
Checking dist/eodhistoricaldata-0.2.0.tar.gz: PASSED

Тестовете МИНАХА, така че сме готови.

Стъпка 8: Публикувайте нашата библиотека, за да тествате PyPI

(venv) eodhistoricaldata % twine upload --repository-url https://test.pypi.org/legacy/ dist/*
Uploading distributions to https://test.pypi.org/legacy/
Enter your username: <removed>
Enter your password: 
Uploading eodhistoricaldata-0.2.0-py3-none-any.whl
100%|████████████████████████████████████████████████████████████████████████████| 9.60k/9.60k [00:01<00:00, 6.50kB/s]
Uploading eodhistoricaldata-0.2.0.tar.gz
100%|████████████████████████████████████████████████████████████████████████████| 8.84k/8.84k [00:01<00:00, 6.72kB/s]
View at:
https://test.pypi.org/project/eodhistoricaldata/0.2.0/

Стъпка 9: Публикувайте нашата библиотека в PyPI

(venv) eodhistoricaldata % twine upload dist/*
Uploading distributions to https://upload.pypi.org/legacy/
Enter your username: <removed>
Enter your password: 
Uploading eodhistoricaldata-0.2.0-py3-none-any.whl
100%|████████████████████████████████████████████████████████████████████████████| 9.60k/9.60k [00:01<00:00, 5.18kB/s]
Uploading eodhistoricaldata-0.2.0.tar.gz
100%|████████████████████████████████████████████████████████████████████████████| 8.84k/8.84k [00:00<00:00, 9.13kB/s]
View at:
https://pypi.org/project/eodhistoricaldata/0.2.0/

И това е! Нека изпробваме нашата библиотека.

Стъпка 10: Използване на библиотеката

"""Sample script"""
from time import sleep
from eodhistoricaldata import WebSocketClient
def main() -> None:
    """Main"""
websocket = WebSocketClient(
        # Demo API key for testing purposes
        api_key="OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX", endpoint="crypto", symbols=["BTC-USD"]
        #api_key="OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX", endpoint="forex", symbols=["EURUSD"]
        #api_key="OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX", endpoint="us", symbols=["AAPL"]
    )
    websocket.start()
message_count = 0
    while True:
        if websocket:
            if (
                message_count != websocket.message_count
            ):
                print(websocket.message)
                message_count = websocket.message_count
                sleep(0.25)  # output every 1/4 second
if __name__ == "__main__":
    main()

Бонус материал

Ако създавате публични хранилища в GitHub, ще искате да ги конфигурирате по подходящ начин.

Първата важна стъпка е да включите лиценз с отворен код във вашето хранилище. В повечето случаи ще искате да използвате MIT License или Apache License Version 2. Лицензът ще се съхранява в корена на проекта като файл, наречен LICENSE. Когато създадете вашето GitHub хранилище, то ще ви попита дали искате да добавите лиценз. Ако създавате библиотеката си като публична, трябва да включите лиценз.

Частта, за която най-много се вълнувам да ви разкажа, е работен поток на GitHub „Действия“.

Действия“ ви позволяват да автоматизирате определени работни процеси във вашето хранилище. Когато натискам или обединявам нещо в моето хранилище, тестовете на модула ще се изпълняват автоматично. Това е удобно за поддържане на изправността на кода и минимизиране на риска от въвеждане на грешки. За да извлече пълната полза от това, той разчита до голяма степен на изчерпателни добре написани тестове на модула.

Настройването на автоматизирани тестове на единици е много лесно. В GitHub щракнете върху „Действия“ в менюто, след което щракнете върху „Нов работен поток“. След това под „Работни потоци, създадени за вашето хранилище“ в „Публикуване на Python пакет“ изберете „Настройване на този работен процес“.

Дайте име на вашия работен процес на модулни тестове. Нарекох моя „unit-tests.yml“. Добавих кода по-долу и щракнах върху „Започнете да се ангажирате“. Можете да намерите кода тук за справка.

Както можете да видите разпоредбите на работния процес, екземпляр на Ubuntu инсталира Python 3.9, инсталира код заклинание, инсталира всички зависимости от requirements.txt (ето защо казах, че ще ви трябва този файл по-рано), след това стартира flake8 и накрая изпълнява pytests. Предполагам, че вашите модулни тестове трябва да работят локално, за да работи това.

След това можете ръчно да стартирате своя работен процес на модулни тестове в GitHub, за да потвърдите, че работи според очакванията.

Също така конфигурирах работен процес за автоматично публикуване на библиотеката в PyPi, което е доста невероятно. Можете да го разгледате тук, ако проявявате интерес.

В моя безплатен проект за бот за търговия с отворен код („PyCryptoBot“) има „Действие“, което автоматично създава Docker изображение, когато се публикуват нови версии.

За да си сътрудничите с други сътрудници, ще трябва да настроите вашите правила за защита на клонове, както направих по-долу. Той ще защити вашето хранилище, като дефинира набор от правила/разрешения.

Майкъл Уитъл