pip install rels
Rels позволяет создавать сложные перечисляемые типы, со следующими свойствами:
- каждый элемент перечисления — отдельный объект со всеми необходимыми методами;
- наследование перечислений;
- любое количество дополнительных данных, привязанных к элементу перечисления;
- автоматическое создание индексов по данным;
- дополнительные проверки данных;
- формирование обратных ссылок в связанных друг с другом перечислениях (и других объектах);
Общая идея заключается в использовании реляционной модели данных. При описании нового типа описываются поля отношения (столбцы таблицы) и непосредственно сами данные (в виде таблицы).
За счёт наследования можно заранее объявить необходимые общие (абстрактные) типы, а конкретные перечисления уже наследовать от них.
Благодаря динамической природе Python, при создании новых перечислений данные можно загружать из внешних источников, например, электронных таблиц.
Пример использования:
from rels import Column, Relation
class Enum(Relation): # объявляем абстраткное перечисление
name = Column(primary=True) # имя
value = Column(external=True) # значение
class EnumWithText(Enum): # добавляем дополнительный столбец для описания значений
text = Column() # например, для использования в пользовательском интерфейсе
class SOME_CONSTANTS(Enum): # объявляем конкретное перечисление
records = ( ('NAME_1', 1), # и указываем данные для него
('NAME_2', 2))
# Варианты работы с элементами перечислений
SOME_CONSTANTS.NAME_1.name == 'NAME_1' # True
SOME_CONSTANTS.NAME_1.value == 1 # True
SOME_CONSTANTS(1) == SOME_CONSTANTS.NAME_1 # True
SOME_CONSTANTS.NAME_2 == SOME_CONSTANTS.NAME_2 # True
SOME_CONSTANTS.NAME_2 != SOME_CONSTANTS.NAME_1 # True
SOME_CONSTANTS.NAME_2.is_NAME_2 # True
class SOME_CONSTANTS_WITH_TEXT(EnumWithText):
records = ( ('NAME_1', 1, 'constant 1'),
('NAME_2', 2, 'constant 2'))
SOME_CONSTANTS.NAME_2 != SOME_CONSTANTS_WITH_TEXT.NAME_2 # True
SOME_CONSTANTS.NAME_1 != SOME_CONSTANTS_WITH_TEXT.NAME_1 # True
# добавление новых элементов перечисления
class EXTENDED_CONSTANTS(SOME_CONSTANTS_WITH_TEXT):
records = ( ('NAME_3', 3, 'constant 3'), ) # добавляем ещё одно значение
При описании столбцов таблицы, можно указать их свойства:
- name —
string
— имя столбца (по умолчанию равно имени, которому присваивается объект столбца); - unique —
boolean
— значения в столбце должны быть уникальными, иначе будет брошено исключение (по умолчаниюTrue
); - primary —
boolean
— элементы перечисления будут доступны как атрибуты перечисления с именами, указанными в этом столбце (например,ENUM.COLUMN_VALUE
) (по умолчаниюFalse
); - external —
boolean
— элемент перечисления можно будет получить по значению этого столбца, передав его в конструктор перечисления (например,ENUM(some_value)
) (по умолчаниюFalse
); - single_type —
boolean
— все значения в столбце должны быть одного типа (по умолчаниюTrue
); - index_name —
string
— имя, по которому будет доступен индекс с ключам из этого столбца (по умолчанию равенindex_<имя столбца>
), см. подробнее в описании индексов; - related_name —
string
|None
— имя, по которому элемент перечисления будет доступен в объектах, на которые ссылается этот столбец (по умолчаниюNone
), см. подробнее в описании связывания.
При создании перечисления:
- Каждая строка данных в таблице превратится в элемент перечисления.
- В классе перечисления будут установлен атрибут, ссылающиеся на соответствующий элемент перечисления. Имя атрибута будет установлено в значение в столбце, отмеченном как
primary
. - Если
primary
столбцов больше 1, то будут установлены атрибуты для каждого.
Атрибуты перечисления:
.<имя из primary столбца>
— ссылка на элемент перечисления (создаётся для всех значений из primary столбцов);.records
— список всех элементов перечисления в порядке их объявления в «сырых» данных;.<имя индекса>
— индексы всех столбцов (по умолчаниюindex_<имя столбца>
);.__call__
— принимает значение из столбца с external установленным вTrue
, возвращает элемент перечисления, которому оно соответствует;.select(*<список имён столбцов>)
— возвращает таблицу с выборкой данных по указанным столбцам;.get_from_name(<полное имя элемента перечисления>)
— принимает строку с именем конкретного элемента перечисления (например,"ENUM.NAME"
) и возвращает соответствующий элемент перечисления или бросает исключение.
Атрибуты элемента перечисления:
.<имя столбца>
— получение данных для соответствующего столбца;.is_<имя из primary столбца>
— возвращаетTrue
, если
Для каждого столбца таблицы формируются индексы с ключами, равными данным в этом столбце, и значениями, равными элементам перечисления (если значения в столбце помечены как уникальные) или спискам элементов перечисления (если значения в столбце не уникальны).
По умолчанию индекс доступен как атрибут перечисления с именем index_<имя столбца>
, но оно может быть изменено при описании столбца.
Пример:
from rels import Column, Relation
class ENUM(Relation):
name = Column(primary=True)
value = Column(external=True)
text = Column(unique=False, index_name='by_key')
records = ( ('NAME_1', 0, 'key_1'),
('NAME_2', 1, 'key_2'),
('NAME_3', 2, 'key_2'), )
ENUM.index_name # {'NAME_1': ENUM.NAME_1, 'NAME_2': ENUM.NAME_2, 'NAME_3': ENUM.NAME_3}
ENUM.by_key # {'key_1': [ENUM.NAME_1], 'key_2': [ENUM.NAME_2, ENUM.NAME_3]}
Наследование позволяет расширять как количество столбцов, так и добавлять новые элементы перечисления (дополняя таблицу данных).
Добавлять столбцы можно только если в родительском классе не была указана таблица данных.
Пример наследования можно видеть в самом первом листинге.
Если для одного из столбцов указано related_name
, то во время создания перечисления, для каждого объекта из этого столбца будет вызван метод set_related_name(<имя атрибута>, <ссылка на соответствующий элемент перечисления>)
, который должен установить объекту атрибут с соответствующим значением.
В первую очередь, этот механизм предназначен для связи отношений друг с другом, но может использоваться и в других объектах.
Пример:
from rels import Column, Relation
class DESTINATION_ENUM(Relation):
name = Column(primary=True)
val_1 = Column()
records = ( ('STATE_1', 'value_1'),
('STATE_2', 'value_2') )
class SOURCE_ENUM(Relation):
name = Column(primary=True)
val_1 = Column()
rel = Column(related_name='rel_source')
records = ( ('STATE_1', 'value_1', DESTINATION_ENUM.STATE_1),
('STATE_2', 'value_2', DESTINATION_ENUM.STATE_2) )
DESTINATION_ENUM.STATE_1.rel_source == SOURCE_ENUM.STATE_1 # True
DESTINATION_ENUM.STATE_2 == SOURCE_ENUM.STATE_2.rel # True
Для взаимодействия с кодом, использующим другую реализацию перечислений, можно использовать значения из столбца с установленным в True параметром external
. Для восстановления объекта элемента перечисления, достаточно передать это значение в конструктор перечисления.
Пример использования можно найти в самом первом листинге (SOME_CONSTANTS(1) == SOME_CONSTANTS.NAME_1
)
## Использование библиотеки
Все необходимые объекты вынесены в корень модуля:
import rels
# Базовые классы
rels.Column # класс столбца
rels.Record # класс элемента перечисления (обычно использовать нет необходимости)
rels.Relation # базовый клас перечисления
# Простые перечисления
rels.Enum # простое перечисление со столбцами name и value
rels.EnumWithText # простое перечисление с дополнительным столбцом text
# Прочее
rels.NullObject # объект заглушка для отсутствующих значений в external столбцах
rels.exceptions # модуль с исключениями
Название классов перечислений и имена элементов перечисления в primary столбцах желательно писать заглавными буквами так как у них семантика констант (также, это решает проблему пересечения имён стандартных методов с именами элементов перечисления ).
В модуле rels.django
реализован небольшой функционал для взаимодействия с Django.
Перечисление, унаследованное от EnumWithText, дополнительные методы:
- choices — возвращает список
[<элемент перечисления, текс>, …]
Наследник models.IntegerField
, автоматически конвертирует друг в друга целочисленные значения из базы и элементы перечисления.
Конструктор принимает следующие параметры (кроме стандартных для IntegerField
):
- relation — объект отношения
- relation_column — имя столбца, который сохраняется в базу (по умолчанию, равен
"value"
)
Django нормально воспринимает RelationIntegerField
, но в случае указания параметра default
, понадобится править код миграции, так как django ничего о rels не знает.
Все «фичи», за исключением связанных с Django, покрыты тестами.