Skip to main content

🏛️ NaloGO

PyPI version Python 3.11+ License: MIT Code style: black Async Coverage

Асинхронная Python библиотека для работы с API сервиса самозанятых "Мой налог" (lknpd.nalog.ru)

Порт PHP библиотеки shoman4eg/moy-nalog на асинхронный Python с полной типизацией.

Ключевые возможности

Аутентификация

  • ИНН/пароль - классическая аутентификация
  • SMS-аутентификация - безопасный вход по номеру телефона
  • Автообновление токенов - прозрачная ротация при истечении
  • Персистентное хранение - сохранение токенов в файл

Управление доходами

  • Создание чеков - одиночные позиции и множественные услуги
  • Юридические лица - поддержка корпоративных клиентов
  • Отмена чеков - с валидацией причин отмены
  • Точная арифметика - decimal.Decimal для финансовых расчетов

Работа с чеками

  • JSON данные - полная информация о чеке
  • URL печати - прямые ссылки для печати чеков
  • Валидация данных - автоматическая проверка корректности

Дополнительные API

  • Профиль пользователя - информация об аккаунте
  • Способы оплаты - управление банковскими картами
  • Налоговая отчетность - история и платежи по ОКТМО

Качество и безопасность

  • Покрытие тестами (актуальная цифра в бейдже выше)
  • Типизация mypy - статическая проверка типов
  • Безопасное логирование - маскировка чувствительных данных
  • CI/CD pipeline - автоматические проверки качества

Установка

Из PyPI (рекомендуется)

pip install nalogo

Для разработки

git clone https://github.com/Rusik636/nalogo.git
cd nalogo
pip install -e ".[dev]"

Быстрый старт

Базовая настройка

import asyncio
from nalogo import Client

# Простая инициализация
client = Client()

# С настройками
client = Client(
    base_url="https://lknpd.nalog.ru/api",  # Кастомный endpoint
    storage_path="./tokens.json",           # Файл для токенов
    device_id="my-device-123",              # Кастомный ID устройства
    timezone="Europe/Moscow"                # Зона для дат без смещения
)

Часовой пояс

Налоговая записывает чеки по московскому времени и игнорирует смещение в переданной строке — читает «настенные» цифры как МСК. Поэтому библиотека всегда отправляет время, приведённое к Europe/Moscow. Это не настраивается: любое другое значение на проводе означало бы чек не в тот час.

Параметр timezone управляет только одним: как понимать время без смещения.

Приём оплаты из разных регионов

Рекомендуемый способ — ISO 8601 строка со смещением. Не нужно ни собирать datetime, ни импортировать ZoneInfo, ни настраивать клиент: смещение в строке однозначно задаёт момент, поэтому настройка timezone к нему не применяется и ошибиться нельзя.

income_api = client.income()

# Платёж принят во Владивостоке в 12:00 местного
await income_api.create("Услуга", 5000, operation_time="2025-12-28T12:00:00+10:00")
# -> 2025-12-28T05:00:00+03:00

# Тот же чек, но из Екатеринбурга
await income_api.create("Услуга", 5000, operation_time="2025-12-28T12:00:00+05:00")
# -> 2025-12-28T10:00:00+03:00

# Время лежит в БД в UTC
await income_api.create("Услуга", 5000, operation_time="2025-12-28T12:00:00Z")
# -> 2025-12-28T15:00:00+03:00

Принимаются все формы, которые понимает datetime.fromisoformat():

Строка Как трактуется
"2025-12-28T12:00:00+10:00" смещение из строки
"2025-12-28T12:00:00Z" UTC
"2025-12-28 12:00:00+10:00" пробел вместо T (формат многих БД)
"2025-12-28T12:00:00.123+10:00" доли секунды отбрасываются
"2025-12-28T12:00:00" без смещения — берётся зона клиента
"2025-12-28" полночь в зоне клиента

Объекты datetime работают как прежде:

from datetime import datetime
from zoneinfo import ZoneInfo

op = datetime(2025, 12, 28, 12, 0, tzinfo=ZoneInfo("Asia/Vladivostok"))
await income_api.create("Услуга", 5000, operation_time=op)

Для значений без смещения зону можно задать на конкретный вызов:

await income_api.create(
    "Услуга", 5000,
    operation_time="2025-12-28T12:00:00",
    timezone="Asia/Vladivostok",   # перекрывает настройку клиента
)

Что выбирать:

Ситуация Решение
Регион меняется от чека к чеку строка со смещением: "...+10:00"
Время из БД в UTC строка с Z
Все операции в одном регионе Client(timezone="Asia/Omsk") один раз
datetime уже собран в коде передать объект как есть

Ошибки времени

Некорректные значения отсекаются до обращения к сети:

from nalogo import Client, DateTimeFormatException, InputException, TimezoneException

Client(timezone="Moscow")                                  # TimezoneException
await income_api.create(..., operation_time="28.12.2025")  # DateTimeFormatException

Оба класса наследуются от InputException, а тот — от DomainException и ValueError, поэтому ловить можно на любом уровне:

try:
    ...
except InputException:      # только ошибки ввода, до запроса
    ...
except DomainException:     # плюс все ошибки API
    ...

⚠️ До версии 1.1.0 время принудительно переводилось в UTC и отправлялось с суффиксом Z. Налоговая читала эти цифры как московские, из-за чего чек записывался со сдвигом на −3 часа. Если вы обходили это, прибавляя 3 часа вручную, — уберите такую компенсацию после обновления.

Аутентификация

По ИНН и паролю

async def auth_with_inn():
    client = Client()
    
    # Получение токена
    token = await client.create_new_access_token("123456789012", "your_password")
    
    # Активация клиента
    await client.authenticate(token)
    
    print("✅ Аутентификация успешна!")
    return client

По номеру телефона (SMS)

async def auth_with_phone():
    client = Client()
    
    # Шаг 1: Запрос SMS кода
    phone = "79001234567"
    challenge = await client.create_phone_challenge(phone)
    
    print(f"📱 SMS код отправлен. Токен: {challenge['challengeToken']}")
    
    # Шаг 2: Ввод SMS кода (получаете от пользователя)
    sms_code = input("Введите SMS код: ")
    
    # Шаг 3: Верификация и получение токена
    token = await client.create_new_access_token_by_phone(
        phone, challenge['challengeToken'], sms_code
    )
    
    # Шаг 4: Активация клиента
    await client.authenticate(token)
    
    print("✅ SMS аутентификация успешна!")
    return client

Создание чеков

Простой чек

async def create_simple_receipt():
    client = await auth_with_inn()  # Предполагаем аутентификацию
    
    income_api = client.income()
    
    result = await income_api.create(
        name="Консультационные услуги",
        amount=5000.00,  # Автоматически конвертируется в Decimal
        quantity=1
    )
    
    receipt_uuid = result["approvedReceiptUuid"]
    print(f"✅ Чек создан: {receipt_uuid}")
    
    return receipt_uuid

Чек с несколькими позициями

from nalogo.dto.income import IncomeServiceItem
from decimal import Decimal

async def create_multi_item_receipt():
    client = await auth_with_inn()
    income_api = client.income()
    
    # Создаем позиции
    services = [
        IncomeServiceItem(
            name="Разработка веб-сайта",
            amount=Decimal("50000.00"),
            quantity=Decimal("1")
        ),
        IncomeServiceItem(
            name="Техподдержка",
            amount=Decimal("5000.00"),
            quantity=Decimal("3")  # 3 месяца
        )
    ]
    
    result = await income_api.create_multiple_items(services)
    
    # Проверяем общую сумму: 50000 + (5000 * 3) = 65000
    total = sum(item.amount * item.quantity for item in services)
    print(f"💰 Общая сумма: {total}")
    
    return result["approvedReceiptUuid"]

Чек для юридического лица

from nalogo.dto.income import IncomeClient, IncomeType

async def create_legal_entity_receipt():
    client = await auth_with_inn()
    income_api = client.income()
    
    # Информация о юридическом лице
    legal_client = IncomeClient(
        contact_phone="+79001234567",
        display_name="ООО 'Инновационные решения'",
        income_type=IncomeType.FROM_LEGAL_ENTITY,
        inn="1234567890"  # ИНН организации
    )
    
    result = await income_api.create(
        name="Разработка ПО по договору",
        amount=250000.00,
        quantity=1,
        client=legal_client
    )
    
    print(f"🏢 Корпоративный чек: {result['approvedReceiptUuid']}")
    return result

Отмена чеков

from nalogo.dto.income import CancelCommentType

async def cancel_receipt():
    client = await auth_with_inn()
    income_api = client.income()
    
    receipt_uuid = "your-receipt-uuid"
    
    result = await income_api.cancel(
        receipt_uuid=receipt_uuid,
        comment_type=CancelCommentType.INCORRECT_DATA,
        request_time=datetime.now(timezone.utc)
    )
    
    print(f"❌ Чек отменен: {result}")

Получение данных чеков

async def get_receipt_info():
    client = await auth_with_inn()
    receipt_api = client.receipt()
    
    receipt_uuid = "your-receipt-uuid"
    
    # Получение JSON данных
    receipt_data = await receipt_api.json(receipt_uuid)
    print(f"📋 Сумма: {receipt_data.get('totalAmount')}")
    print(f"📅 Дата: {receipt_data.get('operationTime')}")
    
    # Генерация URL для печати
    print_url = receipt_api.print_url(receipt_uuid)
    print(f"🖨️ Печать: {print_url}")

Дополнительные API

Информация о пользователе

async def get_user_info():
    client = await auth_with_inn()
    user_api = client.user()
    
    user_data = await user_api.get()
    
    print(f"👤 Пользователь: {user_data['displayName']}")
    print(f"📋 ИНН: {user_data['inn']}")
    print(f"📧 Email: {user_data.get('email', 'Не указан')}")
    print(f"📱 Телефон: {user_data['phone']}")

Способы оплаты

async def manage_payment_types():
    client = await auth_with_inn()
    payment_api = client.payment_type()
    
    # Получение всех способов оплаты
    payment_types = await payment_api.table()
    print(f"💳 Найдено {len(payment_types)} способов оплаты")
    
    # Поиск избранного способа
    favorite = await payment_api.favorite()
    if favorite:
        print(f"⭐ Избранный: {favorite['bankName']}")
    else:
        print("⭐ Избранный способ не настроен")

Налоговая информация

async def get_tax_info():
    client = await auth_with_inn()
    tax_api = client.tax()
    
    # Текущие налоги
    tax_data = await tax_api.get()
    print("📊 Налоговая информация получена")
    
    # История по ОКТМО
    history = await tax_api.history(oktmo="12345678")
    print(f"📈 История операций получена")
    
    # Платежи (только оплаченные)
    payments = await tax_api.payments(oktmo="12345678", only_paid=True)
    print(f"💸 История платежей получена")

Безопасность

Хранение токенов

# ❌ Небезопасно - токены в памяти
client = Client()

# ✅ Рекомендуется - сохранение в файл
client = Client(storage_path="./secure_tokens.json")

# ✅ Продакшн - переменные окружения
import os
from pathlib import Path

token_path = Path(os.getenv("TOKEN_STORAGE_PATH", "./tokens.json"))
client = Client(storage_path=str(token_path))

Логирование

import logging

# Настройка логирования для отладки
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("nalogo")

# Библиотека автоматически маскирует чувствительные данные:
# - Токены доступа
# - Пароли
# - Номера телефонов
# - Персональные данные

Обработка ошибок

from nalogo.exceptions import (
    UnauthorizedException,
    ValidationException,
    PhoneException,
    DomainException
)

async def safe_operation():
    try:
        client = Client()
        token = await client.create_new_access_token("inn", "password")
        await client.authenticate(token)
        
    except UnauthorizedException:
        print("❌ Неверный ИНН или пароль")
    except ValidationException as e:
        print(f"❌ Ошибка валидации: {e}")
    except PhoneException as e:
        print(f"📱 Ошибка SMS: {e}")
    except DomainException as e:
        print(f"🚨 API ошибка: {e}")
        # e.response содержит httpx.Response для детального анализа

Конфигурация

Переменные окружения

Создайте файл .env:

# API настройки
NALOG_BASE_URL=https://lknpd.nalog.ru/api
NALOG_DEVICE_ID=my-unique-device-id

# Хранение токенов
TOKEN_STORAGE_PATH=./secure/tokens.json

# Аутентификация
NALOG_INN=123456789012
NALOG_PASSWORD=your_secure_password

Использование:

import os
from dotenv import load_dotenv

load_dotenv()

client = Client(
    base_url=os.getenv("NALOG_BASE_URL"),
    device_id=os.getenv("NALOG_DEVICE_ID"),
    storage_path=os.getenv("TOKEN_STORAGE_PATH")
)

Кастомизация HTTP клиента

from nalogo import Client
from nalogo._http import AsyncHTTPClient

# Клиент с кастомными настройками
class CustomHTTPClient(AsyncHTTPClient):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Увеличиваем таймаут
        self._client.timeout = 60.0
        
# Использование
client = Client()
client.http_client = CustomHTTPClient("https://lknpd.nalog.ru/api")

Тестирование

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

pip install -e ".[dev]"

Запуск тестов

# Все тесты
pytest

# С покрытием
pytest --cov=nalogo --cov-report=html

# Конкретные тесты
pytest tests/test_auth_async.py -v

# Асинхронные тесты
pytest tests/test_income_async.py::TestIncomeAPI::test_create_success -v

Запуск примеров

# Полный пример использования
python examples/async_example.py

# Локальные тесты с моками
python demo.py

Миграция с PHP библиотеки

Соответствие API

PHP Python Описание
$client->createNewAccessToken() await client.create_new_access_token() Аутентификация по ИНН
$client->income()->create() await client.income().create() Создание чека
$client->receipt()->printUrl() client.receipt().print_url() URL печати
$paymentTypes->favorite() await client.payment_type().favorite() Избранный способ оплаты

Основные различия

1. Асинхронность

// PHP - синхронный код
$result = $client->income()->create($name, $amount, $quantity);
# Python - асинхронный код
result = await client.income().create(name, amount, quantity)

2. Типизация

// PHP - динамическая типизация
$amount = "100.50"; // Строка
$quantity = 2; // Число
# Python - строгая типизация
from decimal import Decimal

amount = Decimal("100.50")  # Decimal для точности
quantity = Decimal("2")     # Decimal для консистентности

3. Обработка ошибок

// PHP - исключения базового класса
try {
    $result = $client->income()->create(...);
} catch (DomainException $e) {
    // Общая обработка
}
# Python - специфичные исключения
try:
    result = await client.income().create(...)
except ValidationException as e:
    # Конкретная ошибка валидации
except UnauthorizedException as e:
    # Ошибка авторизации

Шаблон миграции

# Шаблон для миграции PHP кода
async def migrate_from_php():
    # 1. Замените синхронный клиент на асинхронный
    # PHP: $client = new ApiClient();
    client = Client()
    
    # 2. Добавьте await ко всем API вызовам
    # PHP: $token = $client->createNewAccessToken($inn, $password);
    token = await client.create_new_access_token(inn, password)
    
    # 3. Замените ассоциативные массивы на объекты DTO
    # PHP: $client = ['contactPhone' => $phone, ...];
    from nalogo.dto.income import IncomeClient
    client_data = IncomeClient(contact_phone=phone, ...)
    
    # 4. Используйте Decimal для денежных операций
    # PHP: $amount = 100.50;
    from decimal import Decimal
    amount = Decimal("100.50")
    
    # 5. Обновите обработку исключений
    # PHP: catch (DomainException $e)
    # Python: except DomainException as e

Производительность

Бенчмарки

Операция PHP (sync) Python (async) Улучшение
Аутентификация ~2.1s ~0.8s 2.6x
Создание чека ~1.5s ~0.6s 2.5x
10 чеков последовательно ~15s ~6s 2.5x
10 чеков параллельно ~15s ~2s 7.5x

Оптимизация для высоких нагрузок

import asyncio
from nalogo import Client

async def bulk_receipts():
    client = await auth_with_inn()
    income_api = client.income()
    
    # Создание множества чеков параллельно
    tasks = []
    for i in range(100):
        task = income_api.create(f"Услуга {i}", 1000.00, 1)
        tasks.append(task)
    
    # Выполнение всех задач параллельно
    results = await asyncio.gather(*tasks, return_exceptions=True)
    
    success_count = sum(1 for r in results if not isinstance(r, Exception))
    print(f"✅ Создано {success_count} из {len(tasks)} чеков")

Известные ограничения

API ограничения

  • Invoice API не реализован (помечен как "Not implemented" в оригинальной PHP библиотеке)
  • API версии v1/v2 endpoints могут иметь различия в поведении
  • Лимиты запросов определяются сервисом Мой Налог

Совместимость

  • Python 3.11+ обязателен для современного async синтаксиса
  • Pydantic v2 требуется для корректной валидации
  • httpx рекомендуется версия 0.25.0+

Вклад в развитие

Настройка окружения разработки

# Клонирование
git clone https://github.com/Rusik636/nalogo.git
cd nalogo

# Создание виртуального окружения
python -m venv .venv
source .venv/bin/activate  # Linux/Mac
# или
.venv\Scripts\activate     # Windows

# Установка в режиме разработки с версиями инструментов как в CI
pip install -r requirements-dev.txt
pip install -e . --no-deps

# Настройка pre-commit хуков
pre-commit install

Запуск проверок качества

# Линтинг
ruff check .

# Форматирование
black .

# Типизация
mypy nalogo/

# Безопасность
bandit -r nalogo/

# Полная проверка (как в CI)
pytest --cov=nalogo --cov-fail-under=80

Создание PR

  1. Создайте ветку для фичи: git checkout -b feature/amazing-feature
  2. Напишите тесты для новой функциональности
  3. Убедитесь что все проверки проходят
  4. Создайте PR с подробным описанием изменений

Лицензия

MIT License - подробности в файле LICENSE.

Благодарности

Оригинальная PHP библиотека — Artem Dubinin (shoman4eg/moy-nalog).

Поддержка

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nalogo-1.1.0.tar.gz (45.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nalogo-1.1.0-py3-none-any.whl (34.9 kB view details)

Uploaded Python 3

File details

Details for the file nalogo-1.1.0.tar.gz.

File metadata

  • Download URL: nalogo-1.1.0.tar.gz
  • Upload date:
  • Size: 45.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nalogo-1.1.0.tar.gz
Algorithm Hash digest
SHA256 beaa73f3dda0d31ea7c35edcc84fb44db78c949420666b148dffae2e9ff5b923
MD5 b49b481b36c05f9d6da2562e5b9c1bad
BLAKE2b-256 7ff5892d8153f4a563442009a7089db14b9251e42c733144e01929f621b6ae10

See more details on using hashes here.

Provenance

The following attestation bundles were made for nalogo-1.1.0.tar.gz:

Publisher: release.yml on Rusik636/NaloGO

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nalogo-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: nalogo-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 34.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nalogo-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 880e2b0d192947560e858eafdd885ba700510caffe760acc79a9b3487eeac89f
MD5 14d47b46d3e976a983c17c6d20912816
BLAKE2b-256 6e36f4cf8edcd9452f4b3004b85d2d65d3f95c97a2088b4e673015b3222f9f0e

See more details on using hashes here.

Provenance

The following attestation bundles were made for nalogo-1.1.0-py3-none-any.whl:

Publisher: release.yml on Rusik636/NaloGO

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.2.0

2 files

This release

1.1.0 This release

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page