Skip to content

Commit

Permalink
Added the ability to update existing records via sql
Browse files Browse the repository at this point in the history
  • Loading branch information
Friskes committed Feb 4, 2025
1 parent d0f6296 commit ab6a290
Show file tree
Hide file tree
Showing 10 changed files with 560 additions and 190 deletions.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
2. Создание файла переменных среды
.env файл с содержимым:
```bash
# Опционально, позволяет запускать команду из вложенных директорий проекта.
ABS_ROOT_DIR=C:/path/to/your/project/dir
PATH_TO_LOADNSI_CONFIG=path/to/your/config/file.py
# Требуется только если вы собираетесь использовать официальный API НСИ (Запуск команды с флагом `--use_official_api`)
# Опционально, требуется только если вы собираетесь использовать официальный API НСИ (Запуск команды с флагом `--use_official_api`).
NSI_API_USER_KEY=some-key
```

Expand All @@ -34,8 +36,9 @@
NSI_PASSPORTS = {
'file': 'Локальное название файла с паспортами справочников',
'model': 'Локальное название модели с паспортами справочников',
# Опциональные параметры (fields):
'fields': <Iterable объект состоящий из полей паспорта (str) которые необходимо оставить в объекте паспорта>,
# Опциональные параметры (include, exclude):
'include': <Iterable объект состоящий из полей паспорта (str) которые необходимо оставить в объекте паспорта>,
'exclude': <Iterable объект состоящий из полей паспорта (str) которые необходимо исключить из объекта паспорта>,
}
DICT_INTERNAL_PK = 'your pk field name *not alias*'
PASSPORTS_REL = 'your fieldname for ForeignKey to PARENT_DICT_CLS'
Expand All @@ -44,9 +47,10 @@
'Локальное название файла справочника 1': {
'model': 'Приложение.МодельСправочника1',
'oid': 'OID Справочника 1',
# Опциональные параметры (filter и fields):
# Опциональные параметры (filter, include, exclude и create_sql):
'filter': <Callable объект принимает справочник (dict) должен вернуть (bool) оставлять ли этот объект в списке>,
'fields': <Iterable объект состоящий из полей справочника (str) которые необходимо оставить в объекте справочника>,
'include': <Iterable объект состоящий из полей справочника (str) которые необходимо оставить в объекте справочника>,
'exclude': <Iterable объект состоящий из полей справочника (str) которые необходимо исключить из объекта справочника>,
'create_sql': <Boolean объект (bool), если True будет создан дублирующий файл справочника в SQL формате>,
},
'Локальное название файла справочника 2': {
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies = [
"tenacity==9.0.0",
"rich==13.9.4",
"python-dotenv==1.0.1",
# "pypika==0.48.9",
]


Expand Down
2 changes: 2 additions & 0 deletions src/loadnsi/console_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Progress,
SpinnerColumn,
TextColumn,
TimeRemainingColumn,
TransferSpeedColumn,
)
from rich.text import Text
Expand Down Expand Up @@ -46,6 +47,7 @@ def update_console_size() -> None:
TextColumn('[progress.percentage]{task.percentage:>3.0f}%'),
DownloadColumn(),
TransferSpeedColumn(),
TimeRemainingColumn(),
)
timeout_progress_bar = Progress(
SpinnerColumn(),
Expand Down
396 changes: 233 additions & 163 deletions src/loadnsi/core.py

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions src/loadnsi/dtos.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@ class DictState:
passport_name: str = ''
passport_pk: str = field(default_factory=lambda: str(uuid.uuid4()))
dict_model_data: dict[str, dict] = field(default_factory=dict)
filter_func: Callable[[dict], bool] | None = None
fields: Iterable[str] | None = None
filter: Callable[[dict], bool] | None = None
include: Iterable[str] | None = None
exclude: Iterable[str] | None = None
version_changed: bool = False
create_sql: bool = False
dict_changed: bool = False
forced_update: bool = False

def __post_init__(self):
if self.filter_func is not None and not callable(self.filter_func):
if self.filter is not None and not callable(self.filter):
raise TypeError('Объект filter должен быть вызываемым - Callable')
if self.include and self.exclude:
raise ValueError('Одновременное использование параметров include и exclude запрещено.')

def __repr__(self) -> str:
return f'<DictState(dict_filename={self.dict_filename})>'
26 changes: 23 additions & 3 deletions src/loadnsi/loadnsi.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .file_handlers import NsiFileHandler
from .logger import log, switch_logger
from .model_examplers import ModelExampler
from .sql_builders import SqlBuilder
from .web_handlers import OfficialApiNsiWebCrawler, PirateApiNsiWebCrawler

load_dotenv(find_dotenv())
Expand All @@ -26,9 +27,10 @@ class LoadnsiConfig:
NSI_DICTIONARIES: dict


def load_config(config_path: str) -> dict:
def load_config(root_dir: str | None, config_path: str) -> dict:
"""Загружает файл конфигурации как модуль."""
full_path = os.path.join(os.getcwd(), config_path)
start_dir = root_dir if root_dir else os.getcwd()
full_path = os.path.join(start_dir, config_path)
config_globals = {}
with open(full_path, encoding='utf-8') as file:
file_data = file.read()
Expand All @@ -37,7 +39,7 @@ def load_config(config_path: str) -> dict:
return LoadnsiConfig(**filtered_keys)


config = load_config(os.environ['PATH_TO_LOADNSI_CONFIG'])
config = load_config(os.environ.get('ABS_ROOT_DIR'), os.environ['PATH_TO_LOADNSI_CONFIG'])

fixtures_path = Path(config.NSI_FIXTURES_FOLDER)
if not fixtures_path.exists():
Expand Down Expand Up @@ -131,6 +133,15 @@ def load_config(config_path: str) -> dict:
'для принудительно обновления, даже если версия справочника не изменилась. '
'-fu filename1 -fu filename2',
)
@click.option(
'--lowercase_fields',
'-lf',
show_default=True,
default=True,
# is_flag=True, # Не запрашивать значение для флага если он передан при запуске
help='Приводить вообще все поля к нижнему регистру, а именно: '
'справочников | паспортов в json, sql, примерах моделей.',
)
def loadnsi(**options):
"""
#### Логика взаимодействия с командой:
Expand Down Expand Up @@ -229,6 +240,11 @@ def run_loadnsi(**options):
parent_dict_cls=config.PARENT_DICT_CLS,
)

builder = SqlBuilder(
dict_internal_pk_field=config.DICT_INTERNAL_PK,
passports_rel_field=config.PASSPORTS_REL,
)

if options['use_official_api']:
web = OfficialApiNsiWebCrawler(
nsi_base_url=nsi_base_url,
Expand All @@ -241,10 +257,12 @@ def run_loadnsi(**options):
web,
file,
exampler,
builder,
nsi_passports=nsi_passports,
nsi_dicts=nsi_dicts,
do_not_use_nested_data=options['do_not_use_nested_data'],
forced_update=options['forced_update'],
lowercase_fields=options['lowercase_fields'],
dict_internal_pk_field=config.DICT_INTERNAL_PK,
passports_rel_field=config.PASSPORTS_REL,
)
Expand All @@ -259,10 +277,12 @@ def run_loadnsi(**options):
web,
file,
exampler,
builder,
nsi_passports=nsi_passports,
nsi_dicts=nsi_dicts,
do_not_use_nested_data=options['do_not_use_nested_data'],
forced_update=options['forced_update'],
lowercase_fields=options['lowercase_fields'],
dict_internal_pk_field=config.DICT_INTERNAL_PK,
passports_rel_field=config.PASSPORTS_REL,
)
Expand Down
19 changes: 10 additions & 9 deletions src/loadnsi/model_examplers.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class ModelExampler(Exampler):
dict: 'JSONField',
list: 'JSONField',
}
DICT_PK_NAMES: set[str]
DICT_PK_NAMES: list[str]

def __init__(
self,
Expand Down Expand Up @@ -143,11 +143,14 @@ def _construct_dict_model_cls(self, dict_state: DictState) -> str:
for field_name, field_params in dict_state.dict_model_data.items():
params = []

alias = field_params.get('alias')
if issubclass(field_params['field_type'], (dict, list)):
params.append(f'verbose_name={field_params.get("alias", "")!r}')
if alias:
params.append(f'verbose_name={alias!r}')
params.append(f'default={field_params["field_type"].__name__}')
else:
params.append(f'{field_params["alias"]!r}')
if alias:
params.append(f'{alias!r}')

max_length = field_params.get('field_length')
if max_length is not None:
Expand Down Expand Up @@ -206,12 +209,9 @@ def upd_dict_model_data_with_passport_fields(
return
log.debug('Обновление dict_model_data')

if not dict_state.passport_name:
dict_state.passport_name = remote_passport['shortName']

for field_data in remote_passport['fields']:
key = field_data['field']
if key == 'extId':
key: str = field_data['field']
if key.lower() == 'extid':
for pk_name in self.DICT_PK_NAMES:
if pk_name in dict_state.dict_model_data:
key = pk_name
Expand All @@ -227,8 +227,9 @@ def upd_dict_model_data_with_passport_fields(
'alias',
# 'dataType',
):
field_val = field_data[k]
try:
dict_state.dict_model_data[key][k] = field_data[k]
dict_state.dict_model_data[key][k] = field_val
except KeyError as exc:
log.warning(
'Ключ: %s есть в fields паспорта справочника, '
Expand Down
Loading

0 comments on commit ab6a290

Please sign in to comment.