Python и 1С: микросервис на FastAPI для интеграции

Python + 1С: микросервис на FastAPI для интеграции с 1С
Видео: Python + 1С: микросервис на FastAPI для интеграции с 1С

Типичная ситуация: 1С нужно отдать данные во внешнюю систему или, наоборот, принять запрос снаружи, а встроенный HTTP-сервис 1С для этого неудобен — нет валидации входящих данных «из коробки», сложно обрабатывать долгие операции асинхронно, тяжело логировать и покрывать тестами. В итоге логику обвязки либо городят прямо в конфигурации 1С, либо тратят часы на ручную проверку каждого входящего JSON.

В этой статье соберём рабочий микросервис на Python и FastAPI, который принимает POST-запросы от 1С, автоматически валидирует тело запроса через Pydantic, возвращает предсказуемый ответ и поднимается в Docker одной командой. Дополнительно разберём защиту эндпоинта токеном, обработку ошибок на стороне 1С и сравним подход с аналогичным сервисом на Node.js.

Что вы узнаете

  • Как поднять минимальный сервис на FastAPI и подключить автоматическую валидацию через Pydantic v2
  • Как вызвать этот сервис из 1С через HTTPСоединение и корректно обработать ошибку
  • Как защитить эндпоинт токеном, чтобы к нему не мог обратиться кто угодно из интернета
  • Как упаковать микросервис в Docker и запустить его через docker-compose
  • Чем FastAPI отличается от аналогичного решения на Node.js/Express
  • Какие ошибки чаще всего допускают при интеграции 1С и Python

Зачем выносить логику из 1С в отдельный микросервис

Привет, на связи Илья Низамов. Периодически прилетают вопросы про интеграцию 1С и Python — и почти всегда за ними стоит одна из двух задач: либо нужно дёрнуть внешний API (LLM, платёжный шлюз, стороннюю CRM) из 1С, либо, наоборот, принять данные снаружи и что-то с ними сделать до того, как они попадут в базу.

Отдельная причина выносить логику наружу — защита кода. Часть разработчиков просит спрятать логику конфигурации или расширения от конечного пользователя. Городить обфускацию средствами 1С — плохая идея: тот, кому действительно надо, всё равно разберётся, а порядочным клиентам это только создаёт проблемы с обновлениями. Рабочий вариант — вынести чувствительную часть логики в микросервис на Python, скомпилировать его отдельно и разместить в облаке или на своём сервере. 1С в этом случае обращается к сервису по HTTP и не видит исходный код.

Быстрый старт: FastAPI + Pydantic

Ставим сам фреймворк:

pip install fastapi

И ASGI-сервер, на котором FastAPI будет работать — uvicorn:

pip install uvicorn

Создаём файл main.py. Импортируем сам FastAPI и BaseModel из pydantic — он отвечает за автоматическую валидацию входящих данных без единой строчки ручных проверок:

from fastapi import FastAPI, Header, HTTPException, status
from pydantic import BaseModel, Field

Создаём экземпляр приложения:

app = FastAPI(title="1C Integration Service")

Описываем схему данных, которые ждём от 1С. В Pydantic v2 это обычный класс-наследник BaseModel с типизированными полями — если 1С пришлёт не строку в name или вообще забудет это поле, FastAPI сам вернёт 422 с понятным описанием ошибки, до вашего кода дело не дойдёт:

class OnesData(BaseModel):
    # min_length защищает от пустых строк, которые 1С иногда шлёт вместо NULL
    name: str = Field(..., min_length=1, description="Значение, полученное из 1С")

Для реальной интеграции эндпоинт без авторизации — плохая идея: сервис торчит наружу, и запрос в него может отправить кто угодно. Добавляем простую проверку токена через заголовок запроса. Для продакшена токен читаем из переменной окружения, а не хардкодим в коде:

import os

API_TOKEN = os.environ.get("ONES_API_TOKEN", "dev-only-token")


def verify_token(x_api_key: str = Header(...)) -> None:
    # сравнение через == тут допустимо: токен не секрет уровня платёжной подписи,
    # для боевого продакшена лучше secrets.compare_digest
    if x_api_key != API_TOKEN:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Неверный токен")

Теперь сам эндпоинт. Обратите внимание на model_dump() — в Pydantic v1 использовался .dict(), в актуальной v2 это устаревший алиас, правильный вызов именно model_dump():

from fastapi import Depends


@app.post("/", status_code=201, dependencies=[Depends(verify_token)])
async def process_data(payload: OnesData):
    ones_data = payload.model_dump()  # Pydantic v2: .dict() устарел, используем model_dump()
    print(ones_data["name"])
    return {"newdata": ones_data["name"]}

Отдельно стоит добавить health-check — он не завязан на токен и нужен, чтобы Docker/оркестратор мог проверять, что сервис живой, не дёргая бизнес-логику:

@app.get("/health")
async def health_check():
    return {"status": "ok"}

Итоговый main.py

Собираем всё вместе — это уже полностью рабочий сервис с валидацией, авторизацией и health-check:

import os

from fastapi import Depends, FastAPI, Header, HTTPException, status
from pydantic import BaseModel, Field

app = FastAPI(title="1C Integration Service")

API_TOKEN = os.environ.get("ONES_API_TOKEN", "dev-only-token")


class OnesData(BaseModel):
    name: str = Field(..., min_length=1, description="Значение, полученное из 1С")


def verify_token(x_api_key: str = Header(...)) -> None:
    if x_api_key != API_TOKEN:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Неверный токен")


@app.get("/health")
async def health_check():
    return {"status": "ok"}


@app.post("/", status_code=201, dependencies=[Depends(verify_token)])
async def process_data(payload: OnesData):
    ones_data = payload.model_dump()
    print(ones_data["name"])
    return {"newdata": ones_data["name"]}

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

Запускаем сервер в режиме автоперезагрузки и сразу открываем автосгенерированную документацию — в ней можно отправить тестовый запрос прямо из браузера:

uvicorn main:app --reload

Документация доступна по адресу http://127.0.0.1:8000/docs. Когда сервис готов, фиксируем зависимости:

pip freeze > requirements.txt

Упаковываем микросервис в Docker

Dockerfile — используем актуальный slim-образ Python вместо устаревшего buster:

FROM python:3.12-slim

ENV PYTHONUNBUFFERED=1

WORKDIR /app

COPY ./requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

docker-compose.yml для локального запуска. Токен передаём через переменную окружения, а не через код:

services:
  python:
    container_name: ones-micro
    build: ./
    environment:
      ONES_API_TOKEN: ${ONES_API_TOKEN}
    restart: always
    ports:
      - "8000:8000"

.dockerignore — чтобы в образ не попадали виртуальное окружение, кеш и локальные настройки IDE:

__pycache__/
*.pyc
.venv/
venv/
.env
.git
.idea
Dockerfile
docker-compose.yml

Собираем образ и поднимаем контейнер:

docker-compose build
docker-compose up -d

Вызов микросервиса из 1С

На стороне 1С — обычный HTTPСоединение с таймаутом и заголовком авторизации. Ниже пример с комментариями по каждому шагу:

#Область ОбработчикиКомандФормы

&НаКлиенте
Процедура ЗапросКСервису(Команда)
	ОтветОтСервера = ЗапросКСервисуНаСервере(ТестоваяСтрока);
КонецПроцедуры

#КонецОбласти

#Область Сериализация

&НаСервереБезКонтекста
Функция СформироватьJSON(Знач Данные)

	ЗаписьJSON = Новый ЗаписьJSON;
	ЗаписьJSON.УстановитьСтроку();

	ЗаписатьJSON(ЗаписьJSON, Данные);

	Возврат ЗаписьJSON.Закрыть();
КонецФункции

#КонецОбласти

#Область HTTPЗапрос

&НаСервереБезКонтекста
Функция ЗапросКСервисуНаСервере(Знач name)
	Попытка
		// 30 секунд — таймаут соединения, без него зависший сервис повесит и 1С
		HTTPСоединение = Новый HTTPСоединение("127.0.0.1", 8000,,,, 30);

		Заголовки = Новый Соответствие;
		Заголовки.Вставить("Content-Type", "application/json");
		// токен должен совпадать с ONES_API_TOKEN на стороне сервиса,
		// иначе FastAPI вернёт 401 ещё до вызова process_data
		Заголовки.Вставить("X-Api-Key", "секрет-из-настроек-1с");

		HTTPЗапрос = Новый HTTPЗапрос("/", Заголовки);

		Данные = Новый Соответствие;
		Данные.Вставить("name", name);

		ДанныеJSON = СформироватьJSON(Данные);

		HTTPЗапрос.УстановитьТелоИзСтроки(ДанныеJSON, КодировкаТекста.UTF8);

		HTTPОтветОтСервера = HTTPСоединение.ВызватьHTTPМетод("POST", HTTPЗапрос);

		// код ответа стоит проверять отдельно: 401 (токен), 422 (валидация)
		// и 5xx (сервис упал) требуют разной реакции у вызывающего кода
		Если HTTPОтветОтСервера.КодСостояния <> 201 Тогда
			ЗаписьЖурналаРегистрации("FastAPI", УровеньЖурналаРегистрации.Предупреждение,,,
				"Код ответа: " + HTTPОтветОтСервера.КодСостояния);
		КонецЕсли;

		Возврат HTTPОтветОтСервера.ПолучитьТелоКакСтроку();
	Исключение
		ЗаписьЖурналаРегистрации("FastAPI", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
	КонецПопытки;

	Возврат Неопределено;
КонецФункции

#КонецОбласти 

1С как клиент: OAuth2-токен и защищённые вызовы

Статический X-Api-Key из примера выше подходит для внутреннего сервиса в закрытом контуре. Но чаще Python-бэкенд закрыт по OAuth2 (типичный случай для Django OAuth Toolkit, DRF, FastAPI с OAuth2): 1С сначала получает токен по логину и паролю, а потом ходит к защищённым эндпоинтам с заголовком Authorization: Bearer. Ниже — реальный код такого клиента из учебной конфигурации.

Здесь речь про сторону клиента — как 1С получает и носит чужой токен. Обратный сценарий, когда HTTP-сервис 1С сам выдаёт и проверяет токен, разобран отдельно: JWT-авторизация в 1С — выпуск нативным объектом ТокенДоступа и проверка подписи входящего запроса.

Сначала о хранении доступов. В коде ниже Объект.server, Объект.client_id, Объект.client_secret, Объект.username, Объект.password, Объект.access_token, Объект.refresh_token — это не строковые литералы в тексте процедуры, а реквизиты формы, за которыми стоят константы базы. Так и надо: секрет и пароль не хардкодят в модуле — их держат в константах (а ещё лучше — в защищённом хранилище), а пару access_token/refresh_token сохраняют между сеансами, чтобы не запрашивать токен на каждый вызов.

Получение токена — POST на o/token/ с телом grant_type=password. Ответ содержит access_token и refresh_token, оба сразу сохраняем:

&НаКлиенте
Процедура ПолучитьТокен(Команда)
	Если Объект.ssl Тогда
		SSL = Новый ЗащищенноеСоединениеOpenSSL;
	Иначе
		SSL = Неопределено;
	КонецЕсли;

	HTTPСоединение = Новый HTTPСоединение(Объект.server, Объект.port, Объект.client_id, Объект.client_secret,, 30, SSL);

	ДанныеЗапроса = СтрШаблон("grant_type=password&username=%1&password=%2", Объект.username, Объект.password);

	HTTPЗапрос = Новый HTTPЗапрос("o/token/");
	HTTPЗапрос.Заголовки.Вставить("Content-Type", "application/x-www-form-urlencoded");
	HTTPЗапрос.УстановитьТелоИзСтроки(ДанныеЗапроса);

	Результат = HTTPСоединение.ВызватьHTTPМетод("POST", HTTPЗапрос);
	Если Результат.КодСостояния = 200 Тогда
		Данные = JSONВОбъект(Результат.ПолучитьТелоКакСтроку());
		Объект.access_token = Данные["access_token"];
		Объект.refresh_token = Данные["refresh_token"];
	Иначе
		// 400/401 тут — почти всегда неверные client_id/client_secret или логин/пароль
		РезультатЗапроса = СтрШаблон("Ошибка. Код состояния: %1 Тело: %2", Результат.КодСостояния, Результат.ПолучитьТелоКакСтроку());
	КонецЕсли;
КонецПроцедуры

Путь o/token/ и формат тела здесь — от Django OAuth Toolkit. На другом бэкенде (DRF SimpleJWT, FastAPI с OAuth2) путь и набор полей grant_type будут другими — это первое, что стоит сверить с документацией конкретного сервиса.

Когда access_token истечёт, сервер начнёт отвечать 401. Второй токен нужен ровно для этого: обновление отличается от получения только телом запроса — grant_type=refresh_token вместо пароля, остальная процедура (o/token/, разбор ответа, сохранение новых токенов) идентична:

ДанныеЗапроса = СтрШаблон("grant_type=refresh_token&refresh_token=%1&client_id=%2&client_secret=%3",
	Объект.refresh_token, Объект.client_id, Объект.client_secret);

А вот сам защищённый вызов — GET api/v1/products с Bearer-токеном. Обратите внимание: код ответа проверяем отдельно, и 401 здесь означает не «нет доступа навсегда», а «токен протух» — правильная реакция это вызвать обновление токена и повторить запрос:

&НаКлиенте
Процедура ПолучитьТовары(Команда)
	Если Объект.ssl Тогда
		SSL = Новый ЗащищенноеСоединениеOpenSSL;
	Иначе
		SSL = Неопределено;
	КонецЕсли;

	HTTPСоединение = Новый HTTPСоединение(Объект.server, Объект.port, Объект.client_id, Объект.client_secret,, 30, SSL);

	HTTPЗапрос = Новый HTTPЗапрос("api/v1/products");
	HTTPЗапрос.Заголовки.Вставить("Authorization", СтрШаблон("Bearer %1", Объект.access_token));

	Результат = HTTPСоединение.ВызватьHTTPМетод("GET", HTTPЗапрос);
	Если Результат.КодСостояния = 200 Тогда
		Данные = JSONВОбъект(Результат.ПолучитьТелоКакСтроку());
	Иначе
		// 401 здесь = токен истёк -> вызвать ОбновитьТокен и повторить запрос
		РезультатЗапроса = СтрШаблон("Ошибка. Код состояния: %1 Тело: %2", Результат.КодСостояния, Результат.ПолучитьТелоКакСтроку());
	КонецЕсли;
КонецПроцедуры

Полный цикл получается такой: ПолучитьТокен один раз (или когда протух refresh) → рабочие вызовы с Bearer → на 401 вызвать ОбновитьТокен и повторить. Именно так устроена промышленная интеграция 1С с защищённым Python-сервисом, а не «зашили один токен и надеемся».

1С как сервер: HTTP-сервис для Python

Обратное направление интеграции: не 1С дёргает Python, а Python-скрипт обращается к 1С и забирает или отдаёт данные. Для этого в 1С создаётся объект HTTP-сервис с шаблонами URL, а его методы привязываются к HTTP-глаголам. Ниже — обработчики версионированного эндпоинта /v1/products: GET отдаёт список, POST принимает данные и логирует входящий запрос.

#Область API_V1

Функция ProductsV1GET(Запрос)
	ДанныеОтвета = СформироватьДанныеОтвета();
	Попытка
		Возврат ВернутьДанные(ДанныеОтвета, СформироватьСписокТоваров(), 200);
	Исключение
		Возврат ВернутьОшибку(ДанныеОтвета, ОписаниеОшибки(), 501);
	КонецПопытки;
КонецФункции

Функция ProductsV1POST(Запрос)
	ДанныеJSON = Запрос.ПолучитьТелоКакСтроку();
	Данные = ПрочитатьСтрокуJSON(ДанныеJSON);

	// логируем входящий запрос — потом легко отлаживать, что реально прислал Python
	ЗаписьЖурналаРегистрации("ProductsV1POST", УровеньЖурналаРегистрации.Информация,,, ДанныеJSON);

	ДанныеОтвета = СформироватьДанныеОтвета();
	Попытка
		Возврат ВернутьДанные(ДанныеОтвета, Новый Структура("message, data", "Товары добавлены в базу", Данные), 200);
	Исключение
		Возврат ВернутьОшибку(ДанныеОтвета, ОписаниеОшибки(), 501);
	КонецПопытки;
КонецФункции

#КонецОбласти

Ключевая деталь для стабильной интеграции — единый конверт ответа {"response": ..., "error": ...}: Python-клиент всегда парсит ответ одинаково, и на успехе, и на ошибке, а не гадает по коду состояния, какое поле читать. Собирается конверт в паре служебных функций:

Функция СформироватьОтвет(Данные, Код)
	Ответ = Новый HTTPСервисОтвет(Код);
	Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
	ДанныеJSON = ЗаписатьСтрокуJSON(Данные, Истина);
	Ответ.УстановитьТелоИзСтроки(ДанныеJSON, КодировкаТекста.UTF8, ИспользованиеByteOrderMark.НеИспользовать);
	Возврат Ответ;
КонецФункции

Функция ВернутьДанные(ДанныеОтвета, Данные, Код)
	ДанныеОтвета["response"] = Данные;
	Возврат СформироватьОтвет(ДанныеОтвета, Код);
КонецФункции

Функция ВернутьОшибку(ДанныеОтвета, Ошибка, Код)
	ДанныеОтвета["error"] = Ошибка;
	Возврат СформироватьОтвет(ДанныеОтвета, Код);
КонецФункции

Методы ProductsV1GET/ProductsV1POST привязываются к шаблону URL /v1/products/* в свойствах HTTP-сервиса (корневой URL api), а сам сервис нужно опубликовать на веб-сервере — только после публикации Python сможет достучаться до 1С по HTTP. Логирование тела в журнал регистрации (ЗаписьЖурналаРегистрации) — не украшение: когда обмен «молча не работает», журнал показывает, что именно прислал клиент.

FastAPI vs Node.js/Express: что выбрать для интеграции с 1С

Оба стека одинаково хорошо справляются с ролью «принять HTTP-запрос от 1С и что-то с ним сделать». Разница — в деталях, которые важны именно для интеграций с 1С:

КритерийFastAPI (Python)Node.js / Express
Валидация входящих данныхИз коробки через Pydantic — схема одновременно и валидирует, и документирует APIНужна отдельная библиотека (Zod, Joi) и ручная привязка к роутам
Документация APIАвтогенерация Swagger UI на /docs без дополнительной настройкиНужен отдельный пакет (swagger-jsdoc и подобные) и описание вручную
Экосистема для работы с 1СБольшинство существующих скриптов обмена, парсеров выгрузок 1С и ML/LLM-библиотек — на PythonСильнее в реалтайме (WebSocket, SSE) и там, где фронтенд и бэкенд на одном языке
Порог входа для разработчика 1ССинтаксис ближе к тому, что 1С-разработчик уже видел в скриптах на PythonНужно параллельно разбираться с npm-экосистемой и async/await в JS

Практический вывод: если микросервис — это в первую очередь приём/валидация данных от 1С и дальнейшая передача в Python-экосистему (пандас, LLM, ML-модели), берите FastAPI. Если сервис — часть более крупного Node.js-бэкенда или нужен постоянный канал (WebSocket) с фронтендом, разумнее остаться на Node.js/Express. Подробнее про интеграцию 1С с JavaScript и Node.js — в статье «1С и JavaScript/Node.js».

Частые ошибки при интеграции 1С и Python

  • Отсутствие таймаута в HTTPСоединение на стороне 1С — зависший микросервис повесит и сеанс 1С.
  • Синхронный блокирующий код внутри async def — обращение к БД или файлу без await блокирует весь event loop FastAPI, а не только один запрос.
  • Отсутствие авторизации на эндпоинте, который смотрит в интернет напрямую, без reverse-proxy и firewall-правил.
  • Игнорирование кода ответа на стороне 1С: 401 и 422 требуют разной реакции, чем 5xx, но часто в 1С просто проверяют «тело не пустое».
  • Хардкод токенов и паролей в коде микросервиса вместо переменных окружения — при коммите в git это моментально становится технической проблемой.

Похожие статьи

Полный курс по Python + 1С

Если хотите не просто повторить один пример, а системно разобраться в интеграции 1С и Python — от HTTP-сервисов 1С до защищённого прокси-сервера на Python и OAuth 2 — приходите на курс «Python + 1С: защищённый OAuth 2 сервер». Разбираем архитектуру интеграции, безопасность и типовые ошибки на реальных кейсах.

Если тема ближе к ИИ-интеграциям — LLM, RAG, MCP-серверы для 1С — на эту задачу отвечает курс «ИИ-агент для 1С: LangChain, RAG и MCP-серверы», где похожий микросервис на FastAPI используется как шлюз между 1С и языковой моделью.

Частые вопросы