Spec-Zone.ru › Django 6.0

Специальные поля моделей PostgreSQL

Все эти поля доступны из модуля django.contrib.postgres.fields.

Индексация этих полей

Index и Field.db_index создают B-tree-индекс, который не особенно полезен при запросах к сложным типам данных. Лучше подходят такие индексы, как GinIndex и GistIndex, однако выбор индекса зависит от используемых запросов. Как правило, GiST может быть хорошим выбором для полей диапазонов и HStoreField, а GIN может быть полезен для ArrayField.

ArrayField

class ArrayField(base_field, size=None, **options)

Поле для хранения списков данных. Можно использовать большинство типов полей; в качестве base_field передается экземпляр другого поля. Также можно указать size. Поля ArrayField можно вкладывать друг в друга для хранения многомерных массивов.

Если вы задаете полю default, убедитесь, что это вызываемый объект, например list (для пустого значения по умолчанию) или объект, возвращающий список (например, функция). Неправильное использование default=[] создает изменяемое значение по умолчанию, общее для всех экземпляров ArrayField.

base_field

Это обязательный аргумент.

Задает базовый тип данных и поведение массива. Это должен быть экземпляр подкласса Field. Например, это может быть IntegerField или CharField. Допускается большинство типов полей, за исключением полей для работы с реляционными данными (ForeignKey, OneToOneField и ManyToManyField) и файловыми полями (FileField и ImageField).

Можно вкладывать поля массивов друг в друга — для этого укажите экземпляр ArrayField в качестве base_field. Например:

from django.contrib.postgres.fields import ArrayField
from django.db import models


class ChessBoard(models.Model):
    board = ArrayField(
        ArrayField(
            models.CharField(max_length=10, blank=True),
            size=8,
        ),
        size=8,
    )

Преобразование значений между базой данных и моделью, проверка данных и конфигурации, а также сериализация делегируются базовому полю.

size

Это необязательный аргумент.

Если он задан, для массива будет установлено указанное максимальное количество элементов. Это значение будет передано базе данных, однако в настоящее время PostgreSQL не обеспечивает соблюдение этого ограничения.

Примечание

При вложении ArrayField, независимо от того, используете ли вы параметр size, PostgreSQL требует, чтобы массивы были прямоугольными:

from django.contrib.postgres.fields import ArrayField
from django.db import models


class Board(models.Model):
    pieces = ArrayField(ArrayField(models.IntegerField()))


# Valid
Board(
    pieces=[
        [2, 3],
        [2, 1],
    ]
)

# Not valid
Board(
    pieces=[
        [2, 3],
        [2],
    ]
)

Если нужны массивы неправильной формы, базовое поле должно допускать значение NULL, а значения следует дополнить элементами None.

Запросы к ArrayField

Для ArrayField доступны различные специальные поисковые выражения и преобразования. Воспользуемся следующей примерной моделью:

from django.contrib.postgres.fields import ArrayField
from django.db import models


class Post(models.Model):
    name = models.CharField(max_length=200)
    tags = ArrayField(models.CharField(max_length=200), blank=True)

    def __str__(self):
        return self.name

contains

Поисковое выражение contains переопределено для ArrayField. Возвращаются объекты, данные которых содержат переданные значения. Используется оператор SQL @>. Например:

>>> Post.objects.create(name="First post", tags=["thoughts", "django"])
>>> Post.objects.create(name="Second post", tags=["thoughts"])
>>> Post.objects.create(name="Third post", tags=["tutorial", "django"])

>>> Post.objects.filter(tags__contains=["thoughts"])
<QuerySet [<Post: First post>, <Post: Second post>]>

>>> Post.objects.filter(tags__contains=["django"])
<QuerySet [<Post: First post>, <Post: Third post>]>

>>> Post.objects.filter(tags__contains=["django", "thoughts"])
<QuerySet [<Post: First post>]>

contained_by

Это обратное поисковое выражение для contains: возвращаются объекты, данные которых являются подмножеством переданных значений. Используется оператор SQL <@. Например:

>>> Post.objects.create(name="First post", tags=["thoughts", "django"])
>>> Post.objects.create(name="Second post", tags=["thoughts"])
>>> Post.objects.create(name="Third post", tags=["tutorial", "django"])

>>> Post.objects.filter(tags__contained_by=["thoughts", "django"])
<QuerySet [<Post: First post>, <Post: Second post>]>

>>> Post.objects.filter(tags__contained_by=["thoughts", "django", "tutorial"])
<QuerySet [<Post: First post>, <Post: Second post>, <Post: Third post>]>

overlap

Возвращает объекты, данные которых содержат хотя бы одно из переданных значений. Используется оператор SQL &&. Например:

>>> Post.objects.create(name="First post", tags=["thoughts", "django"])
>>> Post.objects.create(name="Second post", tags=["thoughts", "tutorial"])
>>> Post.objects.create(name="Third post", tags=["tutorial", "django"])

>>> Post.objects.filter(tags__overlap=["thoughts"])
<QuerySet [<Post: First post>, <Post: Second post>]>

>>> Post.objects.filter(tags__overlap=["thoughts", "tutorial"])
<QuerySet [<Post: First post>, <Post: Second post>, <Post: Third post>]>

>>> Post.objects.filter(tags__overlap=Post.objects.values_list("tags"))
<QuerySet [<Post: First post>, <Post: Second post>, <Post: Third post>]>

len

Возвращает длину массива. После этого доступны те же поисковые выражения, что и для IntegerField. Например:

>>> Post.objects.create(name="First post", tags=["thoughts", "django"])
>>> Post.objects.create(name="Second post", tags=["thoughts"])

>>> Post.objects.filter(tags__len=1)
<QuerySet [<Post: Second post>]>

Преобразования по индексу

Преобразования по индексу позволяют обратиться к элементу массива. Можно использовать любое неотрицательное целое число. Если оно превышает size массива, ошибки не возникает. После преобразования доступны те же поисковые выражения, что и для base_field. Например:

>>> Post.objects.create(name="First post", tags=["thoughts", "django"])
>>> Post.objects.create(name="Second post", tags=["thoughts"])

>>> Post.objects.filter(tags__0="thoughts")
<QuerySet [<Post: First post>, <Post: Second post>]>

>>> Post.objects.filter(tags__1__iexact="Django")
<QuerySet [<Post: First post>]>

>>> Post.objects.filter(tags__276="javascript")
<QuerySet []>

Примечание

При записи обычного SQL для полей-массивов PostgreSQL использует индексацию с 1. Однако в этих индексах и в slices используется индексация с 0, чтобы она соответствовала Python.

Преобразования срезов

Преобразования срезов извлекают срез массива. Можно использовать любые два неотрицательных целых числа, разделенные одним символом подчеркивания. Доступные после преобразования поисковые выражения не меняются. Например:

>>> Post.objects.create(name="First post", tags=["thoughts", "django"])
>>> Post.objects.create(name="Second post", tags=["thoughts"])
>>> Post.objects.create(name="Third post", tags=["django", "python", "thoughts"])

>>> Post.objects.filter(tags__0_1=["thoughts"])
<QuerySet [<Post: First post>, <Post: Second post>]>

>>> Post.objects.filter(tags__0_2__contains=["thoughts"])
<QuerySet [<Post: First post>, <Post: Second post>]>

Примечание

При записи обычного SQL для полей-массивов PostgreSQL использует индексацию с 1. Однако в этих срезах и в indexes используется индексация с 0, чтобы она соответствовала Python.

Многомерные массивы с индексами и срезами

При использовании индексов и срезов для многомерных массивов PostgreSQL ведет себя довольно необычно. Индексы всегда позволяют добраться до конечных базовых данных, однако большинство других срезов на уровне базы данных работают странно, и Django не может поддерживать их логично и единообразно.

HStoreField

class HStoreField(**options)

Поле для хранения пар «ключ — значение». Используемый тип данных Python — dict. Ключи должны быть строками, а значения могут быть строками или null (None в Python).

Чтобы использовать это поле, необходимо:

  1. Добавить 'django.contrib.postgres' в INSTALLED_APPS.
  2. Настроить расширение hstore в PostgreSQL.

Если пропустить первый шаг, вы увидите ошибку вроде can't adapt type 'dict', а если второй — type "hstore" does not exist.

Примечание

Иногда может быть полезно задать обязательные или допустимые ключи для определенного поля. Это можно сделать с помощью KeysValidator.

Запросы к HStoreField

Помимо возможности выполнять запросы по ключу, для HStoreField доступен ряд специальных поисковых выражений.

Воспользуемся следующей примерной моделью:

from django.contrib.postgres.fields import HStoreField
from django.db import models


class Dog(models.Model):
    name = models.CharField(max_length=200)
    data = HStoreField()

    def __str__(self):
        return self.name

Поиск по ключу

Чтобы выполнить запрос по заданному ключу, используйте этот ключ как имя поискового выражения:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador"})
>>> Dog.objects.create(name="Meg", data={"breed": "collie"})

>>> Dog.objects.filter(data__breed="collie")
<QuerySet [<Dog: Meg>]>

После поиска по ключу можно добавить другие поисковые выражения:

>>> Dog.objects.filter(data__breed__contains="l")
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

или использовать выражения F() для добавления значения ключа в аннотацию. Например:

>>> from django.db.models import F
>>> rufus = Dog.objects.annotate(breed=F("data__breed"))[0]
>>> rufus.breed
'labrador'

Если ключ, по которому нужно выполнить запрос, совпадает с именем другого поискового выражения, вместо него необходимо использовать поисковое выражение hstorefield.contains.

Примечание

Преобразования ключей также можно комбинировать со следующими поисковыми выражениями: contains, icontains, endswith, iendswith, iexact, regex, iregex, startswith и istartswith.

Предупреждение

Поскольку ключом в значении hstore может быть любая строка, любое поисковое выражение, кроме перечисленных ниже, будет интерпретировано как поиск по ключу. Ошибки не возникает. Будьте особенно внимательны к опечаткам и всегда проверяйте, что запросы работают так, как задумано.

contains

Поисковое выражение contains переопределено для HStoreField. Возвращаются объекты, поле которых содержит все переданные dict пар «ключ — значение». Используется оператор SQL @>. Например:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador", "owner": "Bob"})
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})
>>> Dog.objects.create(name="Fred", data={})

>>> Dog.objects.filter(data__contains={"owner": "Bob"})
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

>>> Dog.objects.filter(data__contains={"breed": "collie"})
<QuerySet [<Dog: Meg>]>

contained_by

Это обратное поисковое выражение для contains: возвращаются объекты, пары «ключ — значение» которых являются подмножеством пар в переданном значении. Используется оператор SQL <@. Например:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador", "owner": "Bob"})
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})
>>> Dog.objects.create(name="Fred", data={})

>>> Dog.objects.filter(data__contained_by={"breed": "collie", "owner": "Bob"})
<QuerySet [<Dog: Meg>, <Dog: Fred>]>

>>> Dog.objects.filter(data__contained_by={"breed": "collie"})
<QuerySet [<Dog: Fred>]>

has_key

Возвращает объекты, данные которых содержат заданный ключ. Используется оператор SQL ?. Например:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador"})
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})

>>> Dog.objects.filter(data__has_key="owner")
<QuerySet [<Dog: Meg>]>

has_any_keys

Возвращает объекты, данные которых содержат хотя бы один из заданных ключей. Используется оператор SQL ?|. Например:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador"})
>>> Dog.objects.create(name="Meg", data={"owner": "Bob"})
>>> Dog.objects.create(name="Fred", data={})

>>> Dog.objects.filter(data__has_any_keys=["owner", "breed"])
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

has_keys

Возвращает объекты, данные которых содержат все заданные ключи. Используется оператор SQL ?&. Например:

>>> Dog.objects.create(name="Rufus", data={})
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})

>>> Dog.objects.filter(data__has_keys=["breed", "owner"])
<QuerySet [<Dog: Meg>]>

keys

Возвращает объекты, массив ключей которых совпадает с заданным значением. Порядок элементов не гарантируется, поэтому это преобразование в основном полезно в сочетании с поисковыми выражениями для ArrayField. Используется функция SQL akeys(). Например:

>>> Dog.objects.create(name="Rufus", data={"toy": "bone"})
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})

>>> Dog.objects.filter(data__keys__overlap=["breed", "toy"])
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

values

Возвращает объекты, массив значений которых совпадает с заданным значением. Порядок элементов не гарантируется, поэтому это преобразование в основном полезно в сочетании с поисковыми выражениями для ArrayField. Используется функция SQL avals(). Например:

>>> Dog.objects.create(name="Rufus", data={"breed": "labrador"})
>>> Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})

>>> Dog.objects.filter(data__values__contains=["collie"])
<QuerySet [<Dog: Meg>]>

Поля диапазонов

Существует пять типов полей диапазонов, соответствующих встроенным типам диапазонов PostgreSQL. Эти поля используются для хранения диапазона значений, например времени начала и окончания события или диапазона возрастов, для которых подходит какое-либо занятие.

Все поля диапазонов преобразуются в Python в объекты Range psycopg, но также принимают кортежи в качестве входных данных, если сведения о границах не нужны. По умолчанию нижняя граница включена, а верхняя исключена, то есть [) (подробности см. в документации PostgreSQL о различных границах). Границы по умолчанию можно изменить для недискретных полей диапазонов (DateTimeRangeField и DecimalRangeField) с помощью аргумента default_bounds.

PostgreSQL приводит диапазон без точек к пустому диапазону

Диапазон с одинаковыми значениями включённой нижней и исключённой верхней границ, например Range(datetime.date(2005, 6, 21), datetime.date(2005, 6, 21)) или [4, 4), не содержит точек. При сохранении в базе данных PostgreSQL преобразует такое значение в пустой диапазон, и исходные значения границ будут потеряны. Подробности см. в документации PostgreSQL.

IntegerRangeField

class IntegerRangeField(**options)

Хранит диапазон целых чисел. Основано на IntegerField. В базе данных представлено как int4range, а в Python — как django.db.backends.postgresql.psycopg_any.NumericRange.

Независимо от границ, заданных при сохранении данных, PostgreSQL всегда возвращает диапазон в канонической форме, включающей нижнюю границу и исключающей верхнюю, то есть [).

BigIntegerRangeField

class BigIntegerRangeField(**options)

Хранит диапазон больших целых чисел. Основано на BigIntegerField. В базе данных представлено как int8range, а в Python — как django.db.backends.postgresql.psycopg_any.NumericRange.

Независимо от границ, заданных при сохранении данных, PostgreSQL всегда возвращает диапазон в канонической форме, включающей нижнюю границу и исключающей верхнюю, то есть [).

DecimalRangeField

class DecimalRangeField(default_bounds='[)', **options)

Хранит диапазон чисел с плавающей точкой. Основано на DecimalField. В базе данных представлено как numrange, а в Python — как django.db.backends.postgresql.psycopg_any.NumericRange.

default_bounds

Необязательный атрибут. Значение bounds для входных списков и кортежей. По умолчанию нижняя граница включена, а верхняя исключена, то есть [) (подробности см. в документации PostgreSQL о различных границах). default_bounds не используется для входных данных django.db.backends.postgresql.psycopg_any.NumericRange.

DateTimeRangeField

class DateTimeRangeField(default_bounds='[)', **options)

Хранит диапазон временных меток. Основано на DateTimeField. В базе данных представлено как tstzrange, а в Python — как django.db.backends.postgresql.psycopg_any.DateTimeTZRange.

default_bounds

Необязательный атрибут. Значение bounds для входных списков и кортежей. По умолчанию нижняя граница включена, а верхняя исключена, то есть [) (подробности см. в документации PostgreSQL о различных границах). default_bounds не используется для входных данных django.db.backends.postgresql.psycopg_any.DateTimeTZRange.

DateRangeField

class DateRangeField(**options)

Хранит диапазон дат. Основано на DateField. В базе данных представлено как daterange, а в Python — как django.db.backends.postgresql.psycopg_any.DateRange.

Независимо от границ, заданных при сохранении данных, PostgreSQL всегда возвращает диапазон в канонической форме, включающей нижнюю границу и исключающей верхнюю, то есть [).

Запросы к полям диапазонов

Для полей диапазонов доступны различные пользовательские операции поиска и преобразования. Они доступны для всех перечисленных выше полей. В примерах ниже используется следующая модель:

from django.contrib.postgres.fields import IntegerRangeField
from django.db import models


class Event(models.Model):
    name = models.CharField(max_length=200)
    ages = IntegerRangeField()
    start = models.DateTimeField()

    def __str__(self):
        return self.name

Также воспользуемся следующими объектами-примерами:

>>> import datetime
>>> from django.utils import timezone
>>> now = timezone.now()
>>> Event.objects.create(name="Soft play", ages=(0, 10), start=now)
>>> Event.objects.create(
...     name="Pub trip", ages=(21, None), start=now - datetime.timedelta(days=1)
... )

и NumericRange:

>>> from django.db.backends.postgresql.psycopg_any import NumericRange

Функции проверки вхождения

Как и для других полей PostgreSQL, доступны три стандартных оператора проверки вхождения: contains, contained_by и overlap, которым соответствуют операторы SQL @>, <@ и &&.

contains
>>> Event.objects.filter(ages__contains=NumericRange(4, 5))
<QuerySet [<Event: Soft play>]>
contained_by
>>> Event.objects.filter(ages__contained_by=NumericRange(0, 15))
<QuerySet [<Event: Soft play>]>

Операция поиска contained_by также доступна для типов полей, не являющихся полями диапазонов: SmallAutoField, AutoField, BigAutoField, SmallIntegerField, IntegerField, BigIntegerField, DecimalField, FloatField, DateField и DateTimeField. Например:

>>> from django.db.backends.postgresql.psycopg_any import DateTimeTZRange
>>> Event.objects.filter(
...     start__contained_by=DateTimeTZRange(
...         timezone.now() - datetime.timedelta(hours=1),
...         timezone.now() + datetime.timedelta(hours=1),
...     ),
... )
<QuerySet [<Event: Soft play>]>
overlap
>>> Event.objects.filter(ages__overlap=NumericRange(8, 12))
<QuerySet [<Event: Soft play>]>

Функции сравнения

Для полей диапазонов доступны стандартные операции поиска: lt, gt, lte и gte. Они не особенно полезны: сначала сравниваются нижние границы, а затем, только если необходимо, верхние. Этот же принцип используется при сортировке по полю диапазона. Лучше использовать специальные операторы сравнения диапазонов.

fully_lt

Возвращаемые диапазоны строго меньше переданного диапазона. Другими словами, все точки в возвращаемом диапазоне меньше всех точек в переданном диапазоне.

>>> Event.objects.filter(ages__fully_lt=NumericRange(11, 15))
<QuerySet [<Event: Soft play>]>
fully_gt

Возвращаемые диапазоны строго больше переданного диапазона. Другими словами, все точки в возвращаемом диапазоне больше всех точек в переданном диапазоне.

>>> Event.objects.filter(ages__fully_gt=NumericRange(11, 15))
<QuerySet [<Event: Pub trip>]>
not_lt

Возвращаемые диапазоны не содержат точек, меньших точек переданного диапазона; то есть нижняя граница возвращаемого диапазона не меньше нижней границы переданного диапазона.

>>> Event.objects.filter(ages__not_lt=NumericRange(0, 15))
<QuerySet [<Event: Soft play>, <Event: Pub trip>]>
not_gt

Возвращаемые диапазоны не содержат точек, больших точек переданного диапазона; то есть верхняя граница возвращаемого диапазона не больше верхней границы переданного диапазона.

>>> Event.objects.filter(ages__not_gt=NumericRange(3, 10))
<QuerySet [<Event: Soft play>]>
adjacent_to

Возвращаемые диапазоны имеют общую границу с переданным диапазоном.

>>> Event.objects.filter(ages__adjacent_to=NumericRange(10, 21))
<QuerySet [<Event: Soft play>, <Event: Pub trip>]>

Запросы с использованием границ

Для полей диапазонов доступны несколько дополнительных операций поиска.

startswith

Возвращаемые объекты имеют указанную нижнюю границу. Можно объединять с допустимыми операциями поиска для базового поля.

>>> Event.objects.filter(ages__startswith=21)
<QuerySet [<Event: Pub trip>]>
endswith

Возвращаемые объекты имеют указанную верхнюю границу. Можно объединять с допустимыми операциями поиска для базового поля.

>>> Event.objects.filter(ages__endswith=10)
<QuerySet [<Event: Soft play>]>
isempty

Возвращаемые объекты представляют собой пустые диапазоны. Можно объединять с допустимыми операциями поиска для BooleanField.

>>> Event.objects.filter(ages__isempty=True)
<QuerySet []>
lower_inc

Возвращает объекты с включёнными или исключёнными нижними границами в зависимости от переданного логического значения. Можно объединять с допустимыми операциями поиска для BooleanField.

>>> Event.objects.filter(ages__lower_inc=True)
<QuerySet [<Event: Soft play>, <Event: Pub trip>]>
lower_inf

Возвращает объекты с неограниченной (бесконечной) или ограниченной нижней границей в зависимости от переданного логического значения. Можно объединять с допустимыми операциями поиска для BooleanField.

>>> Event.objects.filter(ages__lower_inf=True)
<QuerySet []>
upper_inc

Возвращает объекты с включёнными или исключёнными верхними границами в зависимости от переданного логического значения. Можно объединять с допустимыми операциями поиска для BooleanField.

>>> Event.objects.filter(ages__upper_inc=True)
<QuerySet []>
upper_inf

Возвращает объекты с неограниченной (бесконечной) или ограниченной верхней границей в зависимости от переданного логического значения. Можно объединять с допустимыми операциями поиска для BooleanField.

>>> Event.objects.filter(ages__upper_inf=True)
<QuerySet [<Event: Pub trip>]>

Определение собственных типов диапазонов

PostgreSQL позволяет определять пользовательские типы диапазонов. Реализации полей моделей и форм Django используют приведённые ниже базовые классы, а psycopg предоставляет register_range(), позволяющий использовать пользовательские типы диапазонов.

class RangeField(**options)

Базовый класс полей диапазонов модели.

base_field

Класс поля модели, который следует использовать.

range_type

Тип диапазона, который следует использовать.

form_field

Класс поля формы, который следует использовать. Должен быть подклассом django.contrib.postgres.forms.BaseRangeField.

class django.contrib.postgres.forms.BaseRangeField

Базовый класс полей диапазонов формы.

base_field

Поле формы, которое следует использовать.

range_type

Тип диапазона, который следует использовать.

Операторы диапазонов

class RangeOperators

PostgreSQL предоставляет набор операторов SQL, которые можно использовать с типами данных диапазонов (полное описание операторов диапазонов см. в документации PostgreSQL). Этот класс предназначен для удобства и помогает избежать опечаток. Имена операторов совпадают с именами соответствующих операций поиска.

class RangeOperators:
    EQUAL = "="
    NOT_EQUAL = "<>"
    CONTAINS = "@>"
    CONTAINED_BY = "<@"
    OVERLAPS = "&&"
    FULLY_LT = "<<"
    FULLY_GT = ">>"
    NOT_LT = "&>"
    NOT_GT = "&<"
    ADJACENT_TO = "-|-"

Выражения RangeBoundary()

class RangeBoundary(inclusive_lower=True, inclusive_upper=False)
inclusive_lower

Если True (по умолчанию), нижняя граница включена '[', в противном случае она исключена '('.

inclusive_upper

Если False (по умолчанию), верхняя граница исключена ')', в противном случае она включена ']'.

Выражение RangeBoundary() задаёт границы диапазона. Его можно использовать с пользовательскими функциями диапазонов, принимающими границы, например для определения ExclusionConstraint. Полное описание см. в документации PostgreSQL.

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/6.0/ref/contrib/postgres/fields/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API