Spec-Zone.ru › RethinkDB python

Поваренная книга для Python

Не нашли нужный рецепт? Запросите или добавьте рецепт, открыв запрос на GitHub.

  • Основные команды
  • Фильтрация
  • Работа с документами
  • Пейджинг
  • Преобразования
  • Разное

Основные команды

Создание базы данных

Вы можете использовать метод db_create следующим образом:

r.db_create("blog").run()

Другой способ создания базы данных — через веб-интерфейс. Вы можете перейти к веб-интерфейсу по адресу http://HOST:8080. Нажмите на вкладку Таблицы вверху, а затем используйте кнопку Добавить базу данных.

Переименование базы данных

Самый простой способ переименовать базу данных — использовать команду config для доступа к db_config системной таблице, а затем просто используйте команду update.

r.db("old_db_name").config().update({"name": "new_db_name"}).run()

Создание таблицы

Вы можете выбрать базу данных, в которой вы хотите создать таблицу, с помощью команды db и использовать команду table_create следующим образом:

r.db("blog").table_create("posts").run()

Обратите внимание, что вы можете опустить команду db, если вы создаёте таблицу в базе данных по умолчанию в вашем подключении (установлено в test, если не указано в connect).

Другой способ создания новой таблицы — использовать веб-интерфейс. Вы можете перейти к веб-интерфейсу по адресу http://HOST:8080. Нажмите на вкладку Таблицы в верхней части страницы, а затем используйте кнопку Добавить таблицу.

Вставка документов

Вы можете вставлять документы, вызвав команду insert в соответствующей таблице:

r.table("user").insert({
    "name": "Michel",
    "age": 26
}).run()

Вы можете вставлять несколько документов сразу, передав массив документов в insert следующим образом:

r.table("user").insert([
    {
        "name": "Michel",
        "age": 26
    },
    {
        "name": "Slava",
        "age": 30
    }
]).run()

Удаление документов

Чтобы удалить документы, выберите документы, которые вы хотите удалить, и используйте команду delete. Например, давайте удалим все записи с автором «Michel»:

r.table("posts").filter(r.row["author"] == "Michel").delete().run()

Или, давайте попробуем удалить одного пользователя:

r.table("posts").get("7644aaf2-9928-4231-aa68-4e65e31bf219").delete().run()

Вот как мы бы удалили все документы в таблице:

r.table("posts").delete().run()

Получение документов

Чтобы получить все документы в таблице, просто используйте команду table:

r.table("posts").run()

Команда table возвращает курсор; используйте команду next или for для итерации по результатам, или list, чтобы получить набор в виде массива.

Чтобы получить конкретный документ по ID, используйте get:

r.table("posts").get(1).run()

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

r.table("posts").filter({"author": "Michel"}).run()

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

r.table("posts").get_all("review", index="category").run()

(Для более сложных рецептов фильтрации, читайте дальше.)

Фильтрация

Фильтрация по нескольким полям

Предположим, вы хотите выбрать все сообщения, где имя автора «Michel», а категория «Geek». Вы можете сделать это следующим образом:

r.table("posts").filter({
    "author": "Michel",
    "category": "Geek",
}).run()

В качестве альтернативы, вы можете использовать перегруженный оператор & для создания предиката и передачи его в filter:

r.table("posts").filter(
        (r.row["author"] == "Michel") & (r.row["category"] == "Geek")
    ).run()

Примечание: RethinkDB перегружает &, потому что Python не позволяет перегружать соответствующий оператор and. Поскольку у & высокий приоритет, убедитесь, что обернули условия вокруг него в скобки.

Вы также можете использовать команду r.and_, если предпочитаете не использовать перегруженный &:

r.table("posts").filter(r.and_(r.row["author"] == "Michel",
                             r.row["category"] == "Geek")).run()

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

Фильтрация по наличию значения в массиве

Предположим, у нас есть таблица users с документами следующего формата:

{
    name: "William Adama"
    emails: ["bill@bsg.com", "william@bsg.com"]
    ship: "Galactica"
}

Если мы хотим получить всех пользователей, у которых адрес электронной почты user@email.com, мы можем написать:

r.table("user").filter(r.row["emails"].contains("user@email.com")).run()

Если мы хотим получить всех пользователей на Galactica и Pegasus, мы можем написать:

r.table("user").filter(
    lambda user: r.expr(["Galactica", "Pegasus"]).contains(user["ship"])
).run()

Фильтрация по вложенным полям

В Python вы можете использовать оператор [] для получения значения поля. Этот оператор может быть связан для получения значений из вложенных полей.

Предположим, у нас есть таблица users с документами следующего формата:

{
    name: "William Adama"
    contact: {
        phone: "555-5555"
        email: "bill@bsg.com"
    }
}

Давайте отфильтруем по вложенному полю email:

r.table("user").filter(
    r.row["contact"]["email"] == "user@email.com"
).run()

Для многих команд ReQL, вы также можете использовать синтаксис вложенного JSON, который позволяет значительно большую гибкость. Читайте «Доступ к вложенным полям» для получения дополнительной информации.

Эффективное получение нескольких документов по первичному ключу

Если вы хотите получить все сообщения с первичными ключами 1, 2 или 3, вы можете использовать команду get_all:

r.table("posts").get_all(1, 2, 3).run()

Эффективное получение нескольких документов по вторичному индексу

Предположим, у нас есть таблица posts, которая связывает сообщения с авторами через поле author_id. Если мы создали вторичный индекс по author_id и хотим получить все сообщения, где author_id равно 1, 2 или 3, мы можем использовать команду get_all, чтобы сделать это следующим образом:

r.table("posts").get_all(1, 2, 3, index='author_id').run()

Прочитайте о создании вторичных индексов в RethinkDB.

Получение всех объектов в потоке (курсоре) как массива

Если вы используете команду, которая возвращает поток, и хотите получить все её результаты сразу в массиве, а не итерируя по ним с объектом курсора, вы можете привести его к массиву, используя list.

posts = list(r.table('posts').run(conn))

См. документацию по типам данных для получения дополнительной информации о потоках.

Возвращение определенных полей документа

Если вам нужно получить только несколько определенных полей из ваших документов, вы можете использовать команду pluck. Например, вот как вы вернёте только поля name и age из каждой строки в таблице users:

r.table("users").pluck("name", "age").run()

Это эквивалентно вызову SELECT name, age FROM users в SQL.

Команда pluck также поддерживает выбор вложенных полей в документе. Например, предположим, что мы хотим выбрать поля phone и email из следующего документа:

{
    name: "William Adama"
    contact: {
        phone: "555-5555"
        email: "bill@bsg.com"
    }
}

Мы можем использовать следующий синтаксис:

r.table("users").pluck({"contact": {"phone": True, "email": True}}).run()

Фильтрация по диапазону дат

Предположим, вы хотите получить все сообщения, дата которых находится между 1 января 2012 года (включительно) и 1 января 2013 года (исключительно), вы можете сделать это так:

r.table("posts").filter( lambda post:
    post.during(r.time(2012, 1, 1, 'Z'), r.time(2013, 1, 1, 'Z'))
).run(conn)

Вы также можете сравнивать даты вручную:

r.table("posts").filter( lambda post:
    (post["date"] >= r.time(2012, 1, 1, 'Z')) &
    (post["date"] < r.time(2013, 1, 1, 'Z'))
).run(conn)

Фильтрация с помощью регулярных выражений

Если вы хотите получить всех пользователей, чья фамилия начинается с «Ma», вы можете использовать r.match таким образом:

# Will return Martin, Martinez, Marshall etc.
r.table("users").filter( lambda user:
    user["lastName"].match("^Ma")
).run(conn)

Если вы хотите получить всех пользователей, чья фамилия оканчивается на «s», вы можете использовать r.match таким образом:

# Will return Williams, Jones, Davis etc.
r.table("users").filter( lambda user:
    user["lastName"].match("s$")
).run(conn)

Если вы хотите получить всех пользователей, чья фамилия содержит «ll», вы можете использовать r.match таким образом:

# Will return Williams, Miller, Allen etc.
r.table("users").filter( lambda user:
    user["lastName"].match("ll")
).run(conn)

Регистронезависимая фильтрация

Получить всех пользователей, имя которых «William» (регистронезависимый поиск).

# Will return william, William, WILLIAM, wiLLiam etc.
r.table("users").filter( lambda user:
    user["lastName"].match("(?i)^william$")
).run(conn)

Выполнение нескольких агрегаций одновременно

Если вам нужно выполнить запрос, возвращающий агрегации по различным полям вместе, это типичный случай применения map-reduce.

Предположим набор данных, в котором перечислены лучшие фильмы, ранжированные по голосам пользователей. Вы хотите получить общее количество голосов и средний возраст 25 лучших фильмов: avg() столбца year и sum() столбца votes, отсортированных по столбцу rank для получения диапазона 1–25.

Для этого, используйте map для отображения первых 25 фильмов в новый результат, добавив столбец count, а затем reduce каждую строку отображённого набора результатов в общую сумму для каждого поля (votes, year и column). Затем используйте do для возврата набора результатов с общим количеством голосов и средним годом, вычисленным путём деления суммы лет на их количество.

r.table('movies').order_by('rank').limit(25).map(lambda doc:
    { 'total_votes': doc['votes'], 'total_year': doc['year'], 'count': 1 }
).reduce(lambda left, right: {
    'total_votes': (left['total_votes'] + right['total_votes']),
    'total_year': (left['total_year'] + right['total_year']),
    'count': (left['count'] + right['count'])
}).do(lambda res: {
    'total_votes': res['total_votes'],
    'average_year': (res['total_year'] / res['count'])
}).run(conn)

Мы работаем над более простым синтаксисом для выполнения нескольких агрегаций после команд group. Следите за прогрессом по проблеме 1725.

Обработка документов

Добавление/перезапись поля в документе

Для добавления или перезаписи поля можно использовать команду update. Например, если вы хотите добавить поле author со значением «Michel» для всех документов в таблице posts, используйте:

r.table("posts").update({ "author": "Michel" }).run()

Удаление поля из документа

Команда update позволяет переопределять поля, но не удалять их. Если вы хотите удалить поле, используйте команду replace. Команда replace заменяет весь ваш документ новым документом, который вы передаёте в качестве аргумента. Например, если вы хотите удалить поле author записи блога с идентификатором 1, используйте:

r.table("posts").get("1").replace(r.row.without('author')).run()

Атомарное обновление документа на основе условия

Все изменения, выполняемые с помощью команд update и replace, всегда атомные относительно одного документа. Например, предположим, что мы хотим атомарно обновить количество просмотров страницы, если поле countable установлено в значение true и получить старые и новые результаты в одном запросе. Мы можем выполнить эту операцию следующим образом:

r.table("pages").update(lambda page:
    r.branch(page["countable"] == True,          # if the page is countable
             { "views": page["views"] + 1 },     # increment the view count
             {}                                  # else do nothing
    )), {"return_changes": True}).run()

Выполнение условной вставки или замены

Используя похожую технику, что и в предыдущем рецепте, мы можем использовать branch и replace для поддержания полей updated_at и created_at документа путём либо вставки нового документа, либо обновления существующего в зависимости от того, существует ли документ с указанным ID.

def update_with_date(id, user_object):
    r.table('users').get(id).replace(
        lambda doc: r.branch(
            (doc == None),
            r.expr(user_object).merge({'id': id, 'created_at': r.now()}),
            doc.merge(user_object).merge({'updated_at': r.now()}))).run()

Хранение временных меток и строк JSON-дат в качестве типов данных Time

Вы можете использовать команды epoch_time и iso8601 для преобразования временных меток Unix (в секундах) и строк JSON-времени (которые имеют формат ISO 8601) в тип времени ReQL. Драйвер ReQL также преобразует объекты Python datetime в время ReQL.

import time
from datetime import datetime
timezone = time.strftime("%z")
reql_tz = r.make_timezone(timezone[:3] + ":" + timezone[3:])
the_date = datetime.now(reql_tz)
timestamp = time.mktime(the_date.timetuple())
json_date = the_date.isoformat()
r.table("dates").insert({
    'from_object': the_date,
    'from_epoch': r.epoch_time(timestamp),
    'from_iso': r.iso8601(json_date)
}).run(conn)

Используйте команды toEpochTime и toISO8601 для обратного преобразования.

Инкремент значения поля

Возможен инкремент значения поля в документе (например, счётчика) за один шаг на сервере.

r.table('aggregated').get(id).update(
    { 'count': (r.row['count'].default(0)+1) }
).run(conn)

Используйте default, чтобы убедиться, что если поле count ещё не существует в документе, оно добавляется корректно, а не add генерировало ошибку.

Пагинация

Ограничение количества возвращаемых документов

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

r.table("posts").order_by("date").limit(10).run()

Реализация пагинации

Существует несколько способов пагинации результатов в RethinkDB. Наиболее простой способ — использование skip и limit (аналогично работе OFFSET и LIMIT в SQL), но это также наименее эффективный способ. Более эффективно использовать slice, и ещё более эффективно использовать between с вторичным индексом.

Команда slice возвращает диапазон от заданного начального значения до, но не включая, заданное конечное значение. Это делает её удобной в качестве замены skip/limit: начальное значение — это первый элемент для получения, а конечное значение — это первый элемент плюс ограничение. Для получения постов с 11-го по 20-й используя slice:

r.table("posts").order_by("date").slice(11,21).run(conn)

Если у вас есть вторичный индекс, вы можете использовать команду between в сочетании с order_by и limit. Это наиболее эффективный способ пагинации, но требует поиска значений в поле вторичного индекса, чтобы найти первую запись каждой страницы.

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

r.table("users").order_by(index="name").limit(25).run(conn)

Для каждой последующей страницы начинайте с последнего имени на предыдущей странице.

r.table("users").between(last_name, r.maxval, left_bound="open",
    index="name").order_by(index="name").limit(25).run(conn)

Мы передаём last_name, сохранённый из предыдущего набора, в between в качестве начального индекса. В качестве конечного индекса мы передаём None, чтобы вернуть документы от начального индекса до конца таблицы. Параметр left_bound сообщает between не включать первую запись, так как она уже была возвращена в рамках предыдущей страницы.

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

Подсчёт количества документов в таблице

Вы можете подсчитать количество документов с помощью команды count:

r.table("posts").count().run()

Вычисление среднего значения поля

Вы можете вычислить среднее значение поля с помощью команды avg.

r.table("posts").avg("num_comments").run()

Использование подзапросов для возврата дополнительных полей

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

r.table("posts").merge(lambda post:
    {
        "comments": r.table("comments").filter(lambda comment:
            comment["id_post"] == post["id"]).coerce_to("ARRAY")
    }
).run()

Выполнение операции сворачивания

Предположим, что таблица marks хранит оценки каждого студента по каждому курсу:

[
    {
        "name": "William Adama",
        "mark": 90,
        "id": 1,
        "course": "English"
    },
    {
        "name": "William Adama",
        "mark": 70,
        "id": 2,
        "course": "Mathematics"
    },
    {
        "name": "Laura Roslin",
        "mark": 80,
        "id": 3,
        "course": "English"
    },
    {
        "name": "Laura Roslin",
        "mark": 80,
        "id": 4,
        "course": "Mathematics"
    }
]

Возможно, вас заинтересует получение результатов в таком формате:

[
    {
        "name": "Laura Roslin",
        "Mathematics": 80,
        "English": 80
    },
    {
        "name": "William Adama",
        "Mathematics": 70,
        "English": 90
    }
]

В этом случае вы можете выполнить операцию сворачивания с помощью команд group и coerce_to:

r.db('test').table('marks').group('name').map(
    lambda row: [row['course'], row['mark']]
).ungroup().map(
    lambda res: r.expr({'name': res['group']}).merge(
        res['reduction'].coerce_to('object'))
).run(conn)

Примечание: В будущем будет добавлен более удобный синтаксис. Следите за прогрессом по вопросу 838 на GitHub.

Выполнение операции разворота

Выполнение операции разворота для «отмены» сворачивания можно выполнить с помощью команд concat_map, map и keys:

r.table("pivoted_marks").concat_map(lambda doc:
    doc.without("id", "name").keys().map(lambda course:
        {
            "name": doc["name"],
            "course": course,
            "mark": doc[course]
        }
    )
)

Примечание: В будущем будет добавлен более удобный синтаксис. Следите за прогрессом по вопросу 838 на GitHub.

Переименование поля при получении документов

Предположим, что мы хотим переименовать поле id в id_user при получении документов из таблицы users. Мы можем сделать это так:

r.table("users").map(
    # Add the field id_user that is equal to the id one
    r.row.merge(
        lambda doc: { "id_user": doc["id"] }
    ).without("id") # Remove the field id
)

Группировка результатов запроса по периодам дат/времени

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

r.table('invoices').group(
    [r.row['date'].year(), r.row['date'].month()]
).ungroup().merge(
    {'invoices': r.row['reduction'], 'month': r.row['group']}
).without('reduction', 'group').order_by('month').run(conn)

(Мы также используем технику переименования поля, описанную выше, чтобы придать именам «reduction» и «group» более полезные имена «invoices» и «month».) Вы можете использовать любое сочетание команд ReQL для интервалов дат/времени в группировке или работать с датой/временем как с родным объектом.

В настоящее время ReQL имеет ограничение по умолчанию в 100 000 элементов в массиве, и реализация group требует, чтобы общее количество сгруппированных документов соответствовало этому ограничению, поэтому вы ограничены 100 000 счетами-фактурами. Однако это можно изменить, передав опцию array_limit в run. (Также обратите внимание, что ungroup всегда возвращает массив, хотя это может измениться в будущей версии. Следите за прогрессом по проблеме #2719.)

Вы также можете использовать этот подход с составным индексом по интервалам, которые вы хотите сгруппировать:

r.table('invoices').index_create(
    'by_day', [r.row['date'].year(), r.row['date'].month(), r.row['date'].day()]
).run(conn)

Затем вы можете использовать этот индекс в функции group. Этот запрос вернёт счёт-фактуру с наибольшим значением для каждого дня.

r.table('invoices').group(index='by_day').max('price').run(conn)

Разное

Генерация монотонно возрастающих значений первичных ключей

Эффективная генерация монотонно возрастающих идентификаторов в распределённой системе — это удивительно сложная задача. Если вставленный документ не содержит первичного ключа, RethinkDB в настоящее время генерирует случайный UUID. В будущем мы будем поддерживать дополнительные схемы автоматической генерации (см. https://github.com/rethinkdb/rethinkdb/issues/117), но пока вы можете использовать одну из доступных библиотек с открытым исходным кодом для генерации распределённых идентификаторов (например, twitter snowflake).

Парсинг ответа RethinkDB на запрос записи

При выполнении запроса записи (insert, delete, update или replace) RethinkDB возвращает объект сводки, который выглядит так:

{"deleted":0, "replaced":0, "unchanged":0, "errors":0, "skipped":0, "inserted":1}

Самым важным полем этого объекта является errors. Как правило, если не возникли исключения и errors равно 0, то запись выполнена успешно. (RethinkDB генерирует исключение, когда даже не может получить доступ к таблице; он устанавливает поле errors, если может получить доступ к таблице, но при записи возникает ошибка. Эта конвенция используется для того, чтобы пакетные записи не прерывались на половине пути при обнаружении ошибки.)

Следующие поля всегда присутствуют в этом объекте:

  • inserted – Количество новых документов, добавленных в базу данных.
  • deleted – Количество документов, удаленных из базы данных.
  • replaced – Количество документов, которые были изменены.
  • unchanged – Количество документов, которые должны были быть изменены, но новое значение было таким же, как и старое.
  • skipped – Количество документов, которые остались неизменными при выполнении операции записи, потому что документ недоступен для удаления или обновления. Документ мог быть удалён другой операцией, выполняемой одновременно, или в случае операции get ключ может отсутствовать.
  • errors – Количество документов, которые остались неизменными из-за ошибки.

Кроме того, следующие два поля устанавливаются в зависимости от обстоятельств:

  • generated_keys – Если вы выполняете запрос вставки, где некоторые или все строки не имеют первичных ключей, сервер сгенерирует первичные ключи для вас и вернёт массив этих ключей в этом поле. (Порядок этого массива будет соответствовать порядку строк в вашем запросе вставки.)
  • first_error – Если errors положительно, текст первого сообщения об ошибке, обнаруженного на сервере, будет находиться в этом поле. Это очень полезный инструмент отладки. (Мы не возвращаем все ошибки, потому что одна опечатка может привести к миллионам ошибок при работе с большой базой данных.)

Использование динамических ключей в командах ReQL

Иногда вам может потребоваться записать документ ReQL с динамическим ключом — имя поля хранится в переменной. Вы можете сделать это с помощью команды object, которая принимает список ключей и значений ((key, value, key, value ...)) и возвращает объект из них.

r.table('users').get(1).update(r.object(property_name, value)).run(conn)

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

r.table('users').for_each(
    lambda doc: r.table('users').get(doc['id']).update(
        r.object(doc['field'], new_value))
).run(conn)

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

[
    {
        "name": "John",
        "mark": 70,
        "id": 1,
        "course": "Mathematics"
    },
    {
        "name": "John",
        "mark": 90,
        "id": 2,
        "course": "English"
    }
]

Но вы хотели бы получить документ, более похожий на «ведомость оценок»:

{
    "Mathematics": 70,
    "English": 90
}

Вы можете добиться этого с помощью object и сводки.

r.table('marks').filter({'student': 'John'}).map(
    lambda mark: r.object(mark['course'], mark['mark'])
).reduce(
    lambda left, right: left.merge(right)
)

Возвращение запроса ReQL в виде строки

Для тестирования или ведения журнала вам может потребоваться захватить созданный запрос ReQL в виде строки. (Вы можете увидеть пример этого в сообщениях об ошибках ReQL.) Хотя нет команды ReQL для этого, вы можете просто передать цепочку запросов (без run()) в качестве аргумента команде str():

str(r.table('users').filter(lambda user: user['groups'].contains('operators')))

Создание запросов ReQL на нескольких строках

В некоторых интерфейсах запросов распространён шаблон «построения» запросов программно путём создания объекта запроса, многократного вызова его для добавления команд запроса, а затем вызова команды выполнения. Это позволяет динамически изменять запрос на основе условий во время выполнения. Вы можете ожидать, что это будет выполнено в ReQL так:

query = r.table('posts')
if request.filter:
    query.filter(request.filter)
query.order_by('date')
query.run(conn)

Однако это не сработает! Причина в том, что объект запроса не сохраняет состояние. Каждая команда после первой просто выполняется над исходным значением query (в данном случае таблица posts). Вы можете решить эту проблему, явно присваивая результат каждой новой команды переменной query:

query = r.table('posts')
if request.filter:
    query = query.filter(request.filter)
query = query.order_by('date')
query = query.run(conn)

Объединение нескольких потоков изменений в один

Возможно, вы захотите создать «объединённый» поток изменений для отслеживания нескольких таблиц или запросов в одном потоке. Поскольку команда union работает с changes, ReQL делает это довольно простым. Чтобы отслеживать две таблицы одновременно:

r.table('table1').union(r.table('table2')).changes().run(conn)

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

r.table('table1').merge({"table": "table1"}).union(
    r.table('table2').merge({"table": 'table2'}).changes().run(conn)

Также можно использовать changes с каждым запросом, а не после всего.

r.table("table1").filter({"flag": "blue"}).changes().union(
    r.table("table2").filter({"flag": "red"}).changes()
).run(conn)

© RethinkDB contributors
Licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License.
https://rethinkdb.com/docs/cookbook/python/

Spec-Zone.ru

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