Skip to content

Commit

Permalink
Изменение логики итерации по строкам раздела и обработке пустых строк…
Browse files Browse the repository at this point in the history
…. Баг-фиксы, рефакторинг
  • Loading branch information
WoolenSweater committed Apr 13, 2021
1 parent 539affd commit 16b4dda
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 70 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# CHANGELOG

### [1.2.0] - 2021-04-13

- Изменение логики итерации по строкам раздела и обработке пустых строк.
- Методы `items` и `get_rows` секции больше не принимают словарь специфик. Фильтрация будет происходить снаружи.
- Метод `get_rows` возвращает список с одной пустой строкой "заглушкой" если эта строка отсутствует в отчёте.
- Метод проверки строки на соответствие спицификам перенес из `Section` в `Row` и переименовал в `filter`.
- Из класса `ElemList` удалены методы заполнения строки элементами заглушками если вернулся пустой список, так как get_rows теперь всегда вернёт хотя бы одну строку. Колонками её заполнит метод, который обрабатывает не пустой список.
- Исправил ошибку проверки формата отчёта по схемам в которых отсутствуют параметры, определяющие формат проверок для специфики указанной секции и строки.
- Сделал хэлпер ограниченно имитирующий `MultiDict` из одноименной библиотеки. Помогает в работе с мультистроками.
- Отрефакторил метод `_zip`. Вынес каждый шаг в отдельный метод для читабельности.
- Перед логической проверкой теперь происходит проверка, что ни один элемент не пустой. Иначе будет возбуждено исключение `NoElemToCompareError`.
- Методы проверки контролей условий и правил обёрнуты декораторами для отлова исключения `NoElemToCompareError`. Декоратор возвращает пустой список если поймает его.
- Поправил проверку специфики на вхождение в пересечение справочников. Ранее проверялось лишь вхождение в справочинк приложение.


### [1.1.1] - 2021-04-07

- Исправил ошибку разбора контролей с пробелами в операторах сравнения.
Expand Down
25 changes: 24 additions & 1 deletion rosstat/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ def get_spec_params(self, sec_code, row_code, spec_idx):
определяющий формат проверок для специфики с указанным кодом
'''
spec_code = self._get_spec_code(sec_code, str(spec_idx))
return self[sec_code][row_code][spec_code]
try:
return self[sec_code][row_code][spec_code]
except KeyError:
return {}


class NestedDefaultdict(dict):
Expand All @@ -29,3 +32,23 @@ def __getitem__(self, key):
except KeyError:
self[key] = defaultdict(self.default_factory)
return self[key]


class MultiDict:
def __init__(self):
self.keys = []
self.values = []

def __iter__(self):
return iter(sorted(set(self.keys), key=int))

def add(self, key, value):
self.keys.append(key)
self.values.append(value)

def getall(self, key):
values = []
for k, v in zip(self.keys, self.values):
if k == key:
values.append(v)
return values
53 changes: 24 additions & 29 deletions rosstat/report.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from math import gcd
from typing import List, Dict, Tuple, Optional
from typing import Dict, Tuple, Optional
from collections import defaultdict as defdict
from dataclasses import dataclass, InitVar, field as f
from lxml.etree import _ElementTree
from .helpers import MultiDict
from .schema import str_int


Expand All @@ -22,14 +23,17 @@ class Row:
cols: Dict[str, str] = f(default_factory=dict)
_blank: bool = True

_ignore_specs: Tuple[set] = f(default=({None}, {'*'}, {'0'}), repr=False)
_ignore_report_specs: Tuple[str] = f(default=('XX',), repr=False)

@property
def blank(self):
'''Флаг указывающий, что строка пустая'''
return self._blank

def items(self, codes=None):
'''Итерация по элементам строки'''
codes = codes or self.cols.keys()
codes = codes or iter(self.cols)
for col_code in codes:
yield col_code, self.get_col(col_code)

Expand All @@ -46,44 +50,35 @@ def get_spec(self, idx):
'''Возвращает специфику по её "индексу"'''
return getattr(self, f's{idx}')

def filter(self, specs):
'''Проверка, входит ли строка в список переданных специфик'''
for i in range(1, 4):
row_spec = self.get_spec(i)
if row_spec in self._ignore_report_specs:
return True
if specs[i] not in self._ignore_specs and row_spec not in specs[i]:
return False
return True


@dataclass
class Section:
code: str
rows: List[Row] = f(default_factory=list)
row_codes: List[str] = f(default_factory=list)
rows: MultiDict = f(default_factory=MultiDict)

_ignore_specs: Tuple[set] = f(default=({None}, {'*'}, {'0'}), repr=False)
_ignore_report_specs: Tuple[str] = f(default=('XX',), repr=False)

def items(self, codes=None, specs=None):
def items(self, codes=None):
'''Итерация по элементам раздела'''
codes = codes or sorted(set(self.row_codes), key=int)
codes = codes or iter(self.rows)
for row_code in codes:
yield row_code, list(self.get_rows(row_code, specs=specs))
yield row_code, self.get_rows(row_code)

def get_rows(self, code, specs=None):
'''Возвращает строки с указанным кодом и спецификами'''
for row_code, row in zip(self.row_codes, self.rows):
if row_code == code and self._check_specs(row, specs):
yield row

def _check_specs(self, row, specs):
'''Проверка, входит ли строка в список переданных специфик'''
if specs is None:
return True
for i in range(1, 4):
row_spec = getattr(row, f's{i}')
if row_spec in self._ignore_report_specs:
return True
if specs[i] not in self._ignore_specs and row_spec not in specs[i]:
return False
return True
def get_rows(self, code):
'''Возвращает строки с указанным кодом или пустую "заглушку"'''
return self.rows.getall(code) or [Row(code, None, None, None)]

def add_row(self, row_code, row):
'''Добавление строки в раздел'''
self.row_codes.append(row_code)
self.rows.append(row)
self.rows.add(row_code, row)


@dataclass
Expand Down
4 changes: 4 additions & 0 deletions rosstat/validators/control/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class RuleExprError(ControlError):
'''Ошибка разбора правила контроля'''


class NoElemToCompareError(ControlError):
'''Нет элемента для сравнения'''


class PrevPeriodNotImpl(ControlError):
def __init__(self, id):
self.id = id
Expand Down
14 changes: 13 additions & 1 deletion rosstat/validators/control/inspectors/formula.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from itertools import chain
from collections import namedtuple
from ..parser import parser
from ..exceptions import ConditionExprError, RuleExprError, PrevPeriodNotImpl
from ..exceptions import (ConditionExprError, RuleExprError, PrevPeriodNotImpl,
NoElemToCompareError)


ControlParams = namedtuple('ControlParams', ('is_rule',
Expand All @@ -12,6 +13,15 @@
'fault'))


def wrap_exc(f):
def wrapper(*args, **kwargs):
try:
return f(*args, **kwargs)
except NoElemToCompareError:
return []
return wrapper


class FormulaInspector:
def __init__(self, control, *, formats, catalogs, dimension, skip_warns):
self._skip_warns = skip_warns
Expand All @@ -38,13 +48,15 @@ def check(self, report):
return self._check_rule(report)
return []

@wrap_exc
def _check_condition(self, report):
'''Проверка условия для выполнения контроля'''
if self.condition and not self._is_previous_period(self.condition):
evaluator = self.__parse(self.condition, ConditionExprError)
return not list(self.__check(report, evaluator, self.__params()))
return True

@wrap_exc
def _check_rule(self, report):
'''Проверка правила контроля'''
if self.rule and not self._is_previous_period(self.rule):
Expand Down
79 changes: 43 additions & 36 deletions rosstat/validators/control/parser/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from copy import deepcopy
from itertools import chain
from functools import reduce
from ..exceptions import NoElemToCompareError

operator_map = {
'<': operator.lt,
Expand Down Expand Up @@ -191,30 +192,27 @@ def _read_data(self, report, dimension):
'''Чтение отчёта и конвертация его в массивы элементов'''
raw_sec = report.get_section(self.section)
for row_code, raw_rows in self._read_rows(raw_sec):
if not raw_rows:
self.elems.append(self._proc_row_empty(row_code, dimension))
continue
for raw_row in raw_rows:
for raw_row in self._filter_rows(raw_rows):
self.elems.append(self._proc_row(raw_row, row_code, dimension))

def _read_rows(self, raw_sec):
'''Читаем строки'''
if self.rows == {'*'}:
return raw_sec.items(specs=self.specs)
return raw_sec.items(codes=self.rows, specs=self.specs)
return raw_sec.items()
return raw_sec.items(codes=self.rows)

def _filter_rows(self, raw_rows):
'''Фильтруем строки на соответствие спецификам'''
for row in raw_rows:
if row.filter(self.specs):
yield row

def _read_columns(self, raw_row, dimension):
'''Читаем графы если строка не пустая'''
'''Читаем графы'''
if self.columns == {'*'}:
return raw_row.items(codes=dimension[self.section])
return raw_row.items(codes=self.columns)

def _read_columns_empty(self, dimension):
'''Читаем графы если строка пустая'''
if self.columns == {'*'}:
return iter(dimension[self.section])
return iter(self.columns)

def _proc_row(self, raw_row, row_code, dimension):
'''Заполняем строку элементами со значениями из отчета, отсутствующие
значения замещаем заглушкой
Expand All @@ -224,13 +222,6 @@ def _proc_row(self, raw_row, row_code, dimension):
row.append(Elem(value or 0, self.section, [row_code], [col_code]))
return row

def _proc_row_empty(self, row_code, dimension):
'''Заполняем строку элементами заглушками'''
row = []
for col_code in self._read_columns_empty(dimension):
row.append(Elem(0, self.section, [row_code], [col_code]))
return row

def _apply_funcs(self, report, params, ctx_elem):
'''Выполнение функций на эелементах массива'''
for func, args in self.funcs:
Expand Down Expand Up @@ -272,26 +263,38 @@ def _apply_math(self, report, params, func, elem):
left_operand = self._flatten_elems()
right_operand = elem.check(report, params, self)

operand_pairs = self._zip(left_operand, right_operand)
self.elems.clear()
for l_elem, r_elem in operand_pairs:
for l_elem, r_elem in zip(left_operand, right_operand):
self.elems.append([getattr(operator, func)(l_elem, r_elem)])

def _zip(self, *lists):
'''Сбираем массивы в список кортежей. Если длина массивов различается
и самый короткий длиной в 1 элемент, заменяем его на массив равной
длины с полными копиями этого элемента
(Предполагается, что только 1 массив может быть короче других и
его длина всегда равна 1, но проверку на всякий случай оставил)
[1, 2], [3], [4, 5] > [1, 2], [3, 3], [4, 5] > [1, 3, 4], [2, 3, 5]
def _zip(self, l_list, r_list):
'''Сбираем списки в список кортежей. Если короткий список пустой,
создаём "нулевой" элемент. Добиваем длину короткого списка
до длины длинного если они различаются.
[1, 2, 3], [4] > [1, 2, 3], [4, 4, 4] > [1, 4], [2, 4], [3, 4]
'''
smallest, *_, biggest = sorted(lists, key=len)
if len(smallest) != len(biggest) and len(smallest) == 1:
lists = list(lists)
s_idx, s_elem = lists.index(smallest), smallest[0]
lists[s_idx] = [deepcopy(s_elem) for _ in range(len(biggest))]
lists = [l_list, r_list]
if len(l_list) != len(r_list):
short_list, long_list = self.__order_lists(l_list, r_list)
short_list_index = self.__get_short_list_index(lists, short_list)
lists[short_list_index] = self.__generate_short_list(short_list,
long_list)
return zip(*lists)

def __order_lists(self, l_list, r_list):
'''Упорядочивание двух списков от меньшего к большему'''
if len(l_list) < len(r_list):
return l_list, r_list
return r_list, l_list

def __get_short_list_index(self, lists, short_list):
'''Возвращаем индекс короткого списка'''
return lists.index(short_list)

def __generate_short_list(self, short_list, long_list):
'''Генерация списка, который заёмет место короткого'''
return [deepcopy(short_list[0]) for _ in range(len(long_list))]

def _flatten_elems(self):
'''Возвращаем плоский массив элементов'''
return list(chain(*self.elems))
Expand Down Expand Up @@ -326,9 +329,13 @@ def _control(self, report):
'''Подготовка элементов, слияние, передача в метод контроля'''
l_elems = self.l_elem.check(report, self.params, self.r_elem)
r_elems = self.r_elem.check(report, self.params, self.l_elem)
elems_pairs = self._zip(l_elems, r_elems)

self.__control(elems_pairs)
self.__check_elems(l_elems, r_elems)
self.__control(self._zip(l_elems, r_elems))

def __check_elems(self, *elems):
if not all(elems):
raise NoElemToCompareError()

def __control(self, elems_pairs):
'''Определение аттрибута контроля. Итерация по парам элементов,
Expand Down
6 changes: 4 additions & 2 deletions rosstat/validators/format/inspectors/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ def _check(self, row, spec_idx, specs_map):
self.__check_value_catalog_coord(row, spec_idx, specs_map)

def __check_value_catalog_add(self, row, spec_idx):
'''Проверка на вхождение в справочник'''
if row.get_spec(spec_idx) not in self._catalogs[self.vld_param]['ids']:
'''Проверка на вхождение в пересечение справочников'''
main_catalog = set(self._catalogs[self.catalog]['ids'])
additional_catalog = set(self._catalogs[self.vld_param]['ids'])
if row.get_spec(spec_idx) not in additional_catalog & main_catalog:
raise SpecNotInDictError()

def __check_value_catalog_coord(self, row, spec_idx, specs_map):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name='rosstat-flc',
version='1.1.1',
version='1.2.0',
packages=find_packages(),
description='Tool for format-logistic control of reports sent to RosStat',
long_description=open('README.md', 'r').read(),
Expand Down

0 comments on commit 16b4dda

Please sign in to comment.