5 Commits

Author SHA1 Message Date
avm
90a83f4f08 Merge pull request 'Parse worksheets without dimensions' (#13) from feature/frontend-exchange-corrections-20260722 into main
All checks were successful
Deploy Customer Main / Build, Push, Deploy (push) Successful in 4m18s
CI/CD Pipeline / Code Quality Checks (push) Successful in 7m48s
CI/CD Pipeline / Run Tests (push) Successful in 30m15s
CI/CD Pipeline / Build and Push Dev Images (push) Has been skipped
CI/CD Pipeline / Deploy Dev via Compose (push) Has been skipped
2026-07-22 18:21:32 +03:00
08632be39f fix: parse worksheets without dimensions
All checks were successful
CI/CD Pipeline / Code Quality Checks (pull_request) Successful in 9m1s
CI/CD Pipeline / Run Tests (pull_request) Successful in 24m1s
CI/CD Pipeline / Build and Push Dev Images (pull_request) Has been skipped
CI/CD Pipeline / Deploy Dev via Compose (pull_request) Has been skipped
2026-07-22 17:20:39 +02:00
avm
bde320efb5 Merge pull request 'Fix customer deploy transport authentication' (#12) from feature/customer-deploy-transport-fix-20260722 into main
Some checks failed
CI/CD Pipeline / Run Tests (push) Successful in 2m26s
CI/CD Pipeline / Code Quality Checks (push) Successful in 2m48s
CI/CD Pipeline / Build and Push Dev Images (push) Has been skipped
CI/CD Pipeline / Deploy Dev via Compose (push) Has been skipped
Deploy Customer Main / Build, Push, Deploy (push) Failing after 4m34s
2026-07-22 16:33:39 +03:00
b5968acedb fix: authenticate customer deploy transport
All checks were successful
CI/CD Pipeline / Code Quality Checks (pull_request) Successful in 2m8s
CI/CD Pipeline / Run Tests (pull_request) Successful in 2m37s
CI/CD Pipeline / Build and Push Dev Images (pull_request) Has been skipped
CI/CD Pipeline / Deploy Dev via Compose (pull_request) Has been skipped
2026-07-22 15:29:57 +02:00
avm
ef6eb67015 Merge pull request 'Fix customer registry credential selection' (#11) from feature/customer-registry-auth-fix-20260722 into main
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Successful in 2m50s
Deploy Customer Main / Build, Push, Deploy (push) Failing after 2s
CI/CD Pipeline / Run Tests (push) Successful in 3m3s
CI/CD Pipeline / Build and Push Dev Images (push) Has been skipped
CI/CD Pipeline / Deploy Dev via Compose (push) Has been skipped
2026-07-22 15:50:40 +03:00
3 changed files with 75 additions and 4 deletions

View File

@@ -132,25 +132,42 @@ jobs:
- name: Deploy customer stack - name: Deploy customer stack
env: env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
GITEA_TOKEN: ${{ gitea.token }}
CUSTOMER_DEPLOY_SSH_KEY: ${{ secrets.CUSTOMER_DEPLOY_SSH_KEY }} CUSTOMER_DEPLOY_SSH_KEY: ${{ secrets.CUSTOMER_DEPLOY_SSH_KEY }}
CUSTOMER_DEPLOY_SSH_KEY_B64: ${{ secrets.CUSTOMER_DEPLOY_SSH_KEY_B64 }} CUSTOMER_DEPLOY_SSH_KEY_B64: ${{ secrets.CUSTOMER_DEPLOY_SSH_KEY_B64 }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: | run: |
set -euo pipefail set -euo pipefail
registry_user="${REGISTRY_USERNAME:-${REGISTRY_USER:-${GITHUB_ACTOR:-}}}"
registry_password="${REGISTRY_PASSWORD:-${REGISTRY_TOKEN:-${GITEA_TOKEN:-}}}"
case "${registry_user}" in
*[!A-Za-z0-9._@-]*)
echo "Registry user contains unsupported characters" >&2
exit 1
;;
esac
home_dir="${HOME:-/root}" home_dir="${HOME:-/root}"
mkdir -p "${home_dir}/.ssh" mkdir -p "${home_dir}/.ssh"
key_path="${home_dir}/.ssh/customer_deploy_key" key_path="${home_dir}/.ssh/customer_deploy_key"
if [ -n "${CUSTOMER_DEPLOY_SSH_KEY_B64:-}" ]; then if [ -f "/root/.ssh/ci-key" ]; then
cp "/root/.ssh/ci-key" "${key_path}"
elif [ -f "${home_dir}/.ssh/ci-key" ]; then
cp "${home_dir}/.ssh/ci-key" "${key_path}"
elif [ -n "${CUSTOMER_DEPLOY_SSH_KEY_B64:-}" ]; then
printf '%s' "${CUSTOMER_DEPLOY_SSH_KEY_B64}" | base64 -d > "${key_path}" printf '%s' "${CUSTOMER_DEPLOY_SSH_KEY_B64}" | base64 -d > "${key_path}"
elif [ -n "${DEPLOY_SSH_KEY:-}" ]; then elif [ -n "${DEPLOY_SSH_KEY:-}" ]; then
printf '%s' "${DEPLOY_SSH_KEY}" | base64 -d > "${key_path}" printf '%s' "${DEPLOY_SSH_KEY}" | base64 -d > "${key_path}"
elif [ -n "${CUSTOMER_DEPLOY_SSH_KEY:-}" ]; then elif [ -n "${CUSTOMER_DEPLOY_SSH_KEY:-}" ]; then
printf '%s\n' "${CUSTOMER_DEPLOY_SSH_KEY}" > "${key_path}" printf '%s\n' "${CUSTOMER_DEPLOY_SSH_KEY}" > "${key_path}"
elif [ -f "${home_dir}/.ssh/ci-key" ]; then
cp "${home_dir}/.ssh/ci-key" "${key_path}"
else else
cp "/root/.ssh/ci-key" "${key_path}" echo "Customer deploy SSH key is unavailable" >&2
exit 1
fi fi
chmod 600 "${key_path}" chmod 600 "${key_path}"
@@ -169,4 +186,7 @@ jobs:
flock -w 1800 /tmp/ecos-customer-deploy.lock /bin/sh -c 'cd /ecos && FORCE_PULL=1 COMPOSE_FILE=\"${CUSTOMER_COMPOSE_FILE}\" \"${CUSTOMER_DEPLOY_SCRIPT}\" && docker image prune -f'" flock -w 1800 /tmp/ecos-customer-deploy.lock /bin/sh -c 'cd /ecos && FORCE_PULL=1 COMPOSE_FILE=\"${CUSTOMER_COMPOSE_FILE}\" \"${CUSTOMER_DEPLOY_SCRIPT}\" && docker image prune -f'"
ssh "${ssh_common[@]}" -o "ProxyCommand=${proxy_command}" "${CUSTOMER_DEPLOY_USER}@${CUSTOMER_DEPLOY_HOST}" "true" ssh "${ssh_common[@]}" -o "ProxyCommand=${proxy_command}" "${CUSTOMER_DEPLOY_USER}@${CUSTOMER_DEPLOY_HOST}" "true"
printf '%s' "${registry_password}" \
| ssh "${ssh_common[@]}" -o "ProxyCommand=${proxy_command}" "${CUSTOMER_DEPLOY_USER}@${CUSTOMER_DEPLOY_HOST}" \
"docker login '${CUSTOMER_REGISTRY_HOST}' -u '${registry_user}' --password-stdin"
ssh "${ssh_common[@]}" -o "ProxyCommand=${proxy_command}" "${CUSTOMER_DEPLOY_USER}@${CUSTOMER_DEPLOY_HOST}" "${remote_command}" ssh "${ssh_common[@]}" -o "ProxyCommand=${proxy_command}" "${CUSTOMER_DEPLOY_USER}@${CUSTOMER_DEPLOY_HOST}" "${remote_command}"

View File

@@ -363,6 +363,8 @@ class BaseExcelParser(ABC, Generic[T]):
self._workbook = openpyxl.load_workbook(content, read_only=True, data_only=True) self._workbook = openpyxl.load_workbook(content, read_only=True, data_only=True)
self._sheet = self._workbook.active self._sheet = self._workbook.active
if self._sheet.max_row is None or self._sheet.max_column is None:
self._sheet.calculate_dimension(force=True)
def _parse_row(self, row_num: int) -> RowData | None: def _parse_row(self, row_num: int) -> RowData | None:
"""Парсит одну строку Excel.""" """Парсит одну строку Excel."""

View File

@@ -1,6 +1,9 @@
"""Tests for core excel parser.""" """Tests for core excel parser."""
import re
from io import BytesIO
from unittest.mock import MagicMock from unittest.mock import MagicMock
from zipfile import ZIP_DEFLATED, ZipFile
from apps.core.excel import ( from apps.core.excel import (
BaseExcelParser, BaseExcelParser,
@@ -15,6 +18,7 @@ from apps.core.excel import (
validate_okpo, validate_okpo,
) )
from django.test import TestCase from django.test import TestCase
from openpyxl import Workbook
class ValidatorsTest(TestCase): class ValidatorsTest(TestCase):
@@ -168,3 +172,48 @@ class BaseExcelParserTest(TestCase):
self.assertEqual(len(mappings), 2) self.assertEqual(len(mappings), 2)
self.assertEqual(mappings[0].field_name, "inn") self.assertEqual(mappings[0].field_name, "inn")
def test_parse_recovers_missing_read_only_worksheet_dimension(self):
workbook = Workbook()
sheet = workbook.active
sheet.append(["Организация", "ОКПО", "ОГРН", "ИНН"])
sheet.append(
["Тестовая организация", "12345678", "1234567890123", "1234567890"]
)
source = BytesIO()
workbook.save(source)
workbook.close()
source.seek(0)
without_dimension = BytesIO()
with ZipFile(source, "r") as input_archive, ZipFile(
without_dimension,
"w",
compression=ZIP_DEFLATED,
) as output_archive:
for item in input_archive.infolist():
content = input_archive.read(item.filename)
if item.filename == "xl/worksheets/sheet1.xml":
content = re.sub(rb"<dimension[^>]*/>", b"", content, count=1)
output_archive.writestr(item, content)
without_dimension.seek(0)
created_rows = []
class TestParser(BaseExcelParser):
def get_column_mappings(self):
return []
def get_next_batch_id(self) -> int:
return 1
def create_record(self, row_data, batch_id):
created_rows.append((row_data, batch_id))
return MagicMock()
result = TestParser().parse(without_dimension)
self.assertEqual(result.loaded_count, 1)
self.assertEqual(result.skipped_count, 0)
self.assertEqual(created_rows[0][0].organization_name, "Тестовая организация")
self.assertEqual(created_rows[0][1], 1)