feat: add parser source dashboard and scheduling
All checks were successful
CI/CD Pipeline / Code Quality Checks (pull_request) Successful in 1m55s
CI/CD Pipeline / Run Tests (pull_request) Successful in 3m6s
CI/CD Pipeline / Run API Inventory E2E Tests (pull_request) Successful in 2m48s
CI/CD Pipeline / Telegram Notify Success (pull_request) Successful in 19s
All checks were successful
CI/CD Pipeline / Code Quality Checks (pull_request) Successful in 1m55s
CI/CD Pipeline / Run Tests (pull_request) Successful in 3m6s
CI/CD Pipeline / Run API Inventory E2E Tests (pull_request) Successful in 2m48s
CI/CD Pipeline / Telegram Notify Success (pull_request) Successful in 19s
This commit is contained in:
@@ -23,6 +23,15 @@ class ParserLoadLog(TimestampMixin, models.Model):
|
||||
INSPECTIONS = "inspections", _("Единый реестр проверок")
|
||||
PROCUREMENTS = "procurements", _("Единая информационная система закупок")
|
||||
FNS_REPORTS = "fns_reports", _("Бухгалтерская отчетность ФНС")
|
||||
PROCUREMENTS_44FZ = "procurements_44fz", _("Закупки 44-ФЗ")
|
||||
PROCUREMENTS_223FZ = "procurements_223fz", _("Закупки 223-ФЗ")
|
||||
CONTRACTS = "contracts", _("Контракты ЕИС")
|
||||
UNFAIR_SUPPLIERS = "unfair_suppliers", _("Недобросовестные поставщики")
|
||||
FAS_GOZ = "fas_goz", _("Уклонение от ГОЗ")
|
||||
ARBITRATION = "arbitration", _("Арбитражные дела")
|
||||
FEDRESURS_BANKRUPTCY = "fedresurs_bankruptcy", _("Банкротства Федресурс")
|
||||
FSTEC = "fstec", _("Реестры ФСТЭК")
|
||||
TRUDVSEM = "trudvsem", _("Вакансии Работа России")
|
||||
|
||||
class Status(models.TextChoices):
|
||||
SUCCESS = "success", _("Успешно")
|
||||
@@ -80,6 +89,32 @@ class ParserLoadLog(TimestampMixin, models.Model):
|
||||
return f"Load #{self.batch_id} ({self.source}) - {self.records_count} records"
|
||||
|
||||
|
||||
class ParserBatchSequence(TimestampMixin, models.Model):
|
||||
"""Атомарный счётчик batch_id по источнику парсера."""
|
||||
|
||||
source = models.CharField(
|
||||
_("источник"),
|
||||
max_length=50,
|
||||
choices=ParserLoadLog.Source.choices,
|
||||
unique=True,
|
||||
help_text=_("Источник данных"),
|
||||
)
|
||||
next_batch_id = models.PositiveIntegerField(
|
||||
_("следующий ID пакета"),
|
||||
default=1,
|
||||
help_text=_("Следующий batch_id для источника"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "parsers_batch_sequence"
|
||||
verbose_name = _("счётчик пакетов парсера")
|
||||
verbose_name_plural = _("счётчики пакетов парсеров")
|
||||
ordering = ["source"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.source}: next batch {self.next_batch_id}"
|
||||
|
||||
|
||||
class IndustrialCertificateRecord(TimestampMixin, models.Model):
|
||||
"""
|
||||
Сертификат промышленного производства РФ.
|
||||
@@ -323,6 +358,116 @@ class IndustrialProductRecord(TimestampMixin, models.Model):
|
||||
return f"{self.registry_number} - {self.product_name[:50]}"
|
||||
|
||||
|
||||
class GenericParserRecord(TimestampMixin, models.Model):
|
||||
"""Универсальная запись для разнородных официальных источников."""
|
||||
|
||||
load_batch = models.PositiveIntegerField(
|
||||
_("ID пакета загрузки"),
|
||||
db_index=True,
|
||||
help_text=_("Идентификатор пакета загрузки"),
|
||||
)
|
||||
source = models.CharField(
|
||||
_("источник"),
|
||||
max_length=50,
|
||||
choices=ParserLoadLog.Source.choices,
|
||||
db_index=True,
|
||||
help_text=_("Источник данных"),
|
||||
)
|
||||
external_id = models.CharField(
|
||||
_("внешний ID"),
|
||||
max_length=255,
|
||||
db_index=True,
|
||||
help_text=_("Стабильный идентификатор записи во внешнем источнике"),
|
||||
)
|
||||
inn = models.CharField(
|
||||
_("ИНН"),
|
||||
max_length=20,
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text=_("ИНН организации, если есть в источнике"),
|
||||
)
|
||||
ogrn = models.CharField(
|
||||
_("ОГРН"),
|
||||
max_length=20,
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text=_("ОГРН организации, если есть в источнике"),
|
||||
)
|
||||
organisation_name = models.TextField(
|
||||
_("наименование организации"),
|
||||
blank=True,
|
||||
help_text=_("Наименование организации из источника"),
|
||||
)
|
||||
title = models.TextField(
|
||||
_("заголовок"),
|
||||
blank=True,
|
||||
help_text=_("Краткое описание записи"),
|
||||
)
|
||||
record_date = models.CharField(
|
||||
_("дата записи"),
|
||||
max_length=30,
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text=_("Дата записи в формате источника"),
|
||||
)
|
||||
amount = models.DecimalField(
|
||||
_("сумма"),
|
||||
max_digits=20,
|
||||
decimal_places=2,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Сумма, если источник её содержит"),
|
||||
)
|
||||
status = models.CharField(
|
||||
_("статус"),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text=_("Статус записи во внешнем источнике"),
|
||||
)
|
||||
url = models.TextField(
|
||||
_("URL"),
|
||||
blank=True,
|
||||
help_text=_("Ссылка на карточку/документ во внешнем источнике"),
|
||||
)
|
||||
payload = models.JSONField(
|
||||
_("исходные данные"),
|
||||
default=dict,
|
||||
blank=True,
|
||||
help_text=_("Нормализованный исходный документ"),
|
||||
)
|
||||
registry_organization = models.ForeignKey(
|
||||
"registers.Organization",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="generic_parser_records",
|
||||
verbose_name=_("организация из реестров"),
|
||||
help_text=_("Связь с организацией из приложения реестров"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "parsers_generic_record"
|
||||
verbose_name = _("запись внешнего источника")
|
||||
verbose_name_plural = _("записи внешних источников")
|
||||
ordering = ["-created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=["source", "load_batch"]),
|
||||
models.Index(fields=["source", "inn"]),
|
||||
models.Index(fields=["source", "ogrn"]),
|
||||
models.Index(fields=["source", "record_date"]),
|
||||
]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["source", "external_id"],
|
||||
name="unique_generic_source_external_id",
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
label = self.organisation_name or self.title or self.external_id
|
||||
return f"{self.source}: {label[:80]}"
|
||||
|
||||
|
||||
class Proxy(TimestampMixin, models.Model):
|
||||
"""
|
||||
Прокси-сервер для парсеров.
|
||||
|
||||
Reference in New Issue
Block a user