Курс объемом 140 учебных часов рассчитан на 6-ой семестр. Состоит из 70 часов лекционных занятий, 70 часов практической работы. В качестве самостоятельной работы предусмотрены домашние задания и курсовая работа. По окончанию обучения студенты сдают экзамен. Допуском к экзамену является выполнение всех домашних работ и сдача курсовой работы.
Операционная система в данном курсе не имеет значения, подойдет любая распространенная ОС с графическим интерфейсом. Например Linux, MacOS или Windows. Но в примерах будет использоваться ОС Linux.
См.также
За работой в текстовом редакторе Веб-программист проводит 90% времени, поэтому нужно ответственно подойти к этому выбору. Можно использовать любой понятный вам и удобный в использовании текстовый редактор.
Критериями должны стать:
Всем этим критериям удовлетворяют такие редакторы как Vim и Emacs. Также программисты используют менее функциональные Bred3, Notepad++, SublimeText и другие. Если нет времени на изучение редактора, отличным выбором будет Visual Studio Code, в котором из коробки можно отлаживать Python, управлять git и писать код с автодополнением и проверкой синтаксиса.
Можно выбрать один из самых популярных браузеров (на сегодняшний день это Mozilla Firefox или Google Chrome) или любой другой, соответствующий Веб-стандартам.
Примечание
Git - самая популярная система контроля версий, по сути это уже стандарт в отрасли.
В данном курсе для выполнения самостоятельных работ потребуются знания системы контроля версий git и учетная запись в сервисе GitHub.
Системы контроля версий:
Социальные сети для разработчиков:
3 курс
Студенты должны:
Хронология событий по годам. [1]
Примечание
Интернет — это глобальная компьютерная сеть, объединяющая сотни миллионов компьютеров в общее информационное пространство. Интернет представляет свою инфраструктуру для прикладных сервисов различного назначения, самым популярным из которых является Всемирная Паутина – World Wide Web (www). [2]
World Wide Web (www, web, рус.: веб, Всемирная Паутина) — распределенная информационная система, предоставляющая доступ к гипертекстовым документам по протоколу HTTP.
WWW — сетевая технология прикладного уровня стека TCP/IP, построенная на клиент-серверной архитектуре и использующая инфраструктуру Интернет для взаимодействия между сервером и клиентом (www
).
Серверы www (веб-серверы) — это хранилища гипертекстовой (в общем случае) информации, управляемые специальным программным обеспечением.
Документы, представленные в виде гипертекста, называются веб-страницами. Несколько веб-страниц, объединенных общей тематикой, оформлением, связанных гипертекстовыми ссылками и обычно находящихся на одном и том же веб-сервере, называются веб-сайтом.
Для загрузки и просмотра информации с веб-сайтов используются специальные программы — браузеры, способные обрабатывать гипертектовую разметку и отображать содержимое веб-страниц.
В основе www — взаимодействие между веб-сервером и браузерами по протоколу HTTP (HyperText Transfer Protocol). Веб-сервер — это программа, запущенная на сетевом компьютере и ожидающая клиентские запросы по протоколу HTTP. Браузер может обратиться к веб-серверу по доменному имени или по ip-адресу, передавая в запросе идентификатор требуемого ресурса. Получив запрос от клиента, сервер находит соответствующий ресурс на локальном устройстве хранения и отправляет его как ответ. Браузер принимает ответ и обрабатывает его соответствующим образом, в зависимости от типа ресурса (отображает гипертекст, показывает изображения, сохраняет полученные файлы и т.п.).
Основной тип ресурсов Всемирной паутины — гипертекстовые страницы. Гипертекст — это обычный текст, размеченный специальными управляющими конструкциями — тегами. Браузер считывает теги и интерпретирует их как команды форматирования при выводе информации. Теги описывают структуру документа, а специальные теги, якоря и гиперссылки, позволяют установить связи между веб-страницами и перемещаться как внутри веб-сайта, так и между сайтами.
Примечание
Т. Дж. Бернерс-Ли — «отец» Всемирной паутины
Сэр Тимоти Джон Бернерс-Ли — британский учёный-физик, изобретатель Всемирной паутины (совместно с Робертом Кайо), автор URI, HTTP и HTML. Действующий глава Консорциума Всемирной паутины (W3C). Автор концепции семантической паутины и множества других разработок в области информационных технологий. 16 июля 2004 года Королева Великобритании Елизавета II произвела Тима Бернерса-Ли в Рыцари-Командоры за «службу во благо глобального развития Интернета».
Функционирование сервиса обеспечивается четырьмя составляющими:
Адресация веб-ресурсов. URL, URN, URI
Для доступа к любым сетевым ресурсам необходимо знать, где они размещены, и как к ним можно обратиться. Во Всемирной паутине для обращения к веб-документам изначально используется стандартизированная схема адресации и идентификации, учитывающая опыт адресации и идентификации таких сетевых сервисов, как e-mail, telnet, ftp и т.п. — URL, Uniform Resource Locator.
URL (RFC 1738) — унифицированный локатор (указатель) ресурсов, стандартизированный способ записи адреса ресурса в www и сети Интернет. Адрес URL имеет гибкую и расширяемую структуру для максимально естественного указания местонахождения ресурсов в сети. Для записи адреса используется ограниченный набор символов ASCII. Общий вид адреса можно представить так:
<схема>://<логин>:<пароль>@<хост>:<порт>/<полный-путь-к-ресурсу>
Где:
Примеры URL:
В августе 2002 года RFC 3305 анонсировал устаревание URL в пользу URI (Uniform Resource Identifier), еще более гибкого способа адресации, вобравшего возможности как URL, так и URN (Uniform Resource Name, унифицированное имя ресурса). URI позволяет не только указывать местонахождение ресурса (как URL), но и идентифицировать его в заданном пространстве имен (как URN). Если в URI не указывать местонахождение, то с его помощью можно описывать ресурсы, которые не могут быть получены непосредственно из Интернета (автомобили, персоны и т.п.). Текущая структура и синтаксис URI регулируется стандартом RFC 3986, вышедшим в январе 2005 года.
HTML (HyperText Markup Language <https://ru.wikipedia.org/wiki/HTML>) — стандартный язык разметки документов во Всемирной паутине. Большинство веб-страниц созданы при помощи языка HTML. Язык HTML интерпретируется браузером и отображается в виде документа в удобной для человека форме. HTML является приложением SGML (стандартного обобщённого языка разметки) и соответствует международному стандарту ISO 8879.
HTML создавался как язык для обмена научной и технической документацией, пригодный для использования людьми, не являющимися специалистами в области вёрстки. Для этого он представляет небольшой (сравнительно) набор структурных и семантических элементов — тегов. С помощью HTML можно легко создать относительно простой, но красиво оформленный документ. Изначально язык HTML был задуман и создан как средство структурирования и форматирования документов без их привязки к средствам воспроизведения (отображения). В идеале, текст с разметкой HTML должен единообразно воспроизводиться на различном оборудовании (монитор ПК, экран планшета, ограниченный по размерам экран мобильного телефона, медиа-проектор). Однако современное применение HTML очень далеко от его изначальной задачи. Со временем основная идея платформонезависимости языка HTML стала жертвой коммерциализации www и потребностей в мультимедийном и графическом оформлении.
HTTP (HyperText Transfer Protocol) — протокол передачи гипертекста, текущая версия HTTP/1.1 (RFC 2616). Этот протокол изначально был предназначен для обмена гипертекстовыми документами, но сейчас его возможности существенно расширены в сторону передачи двоичной информации.
HTTP — типичный клиент-серверный протокол, обмен сообщениями идёт по схеме «запрос-ответ» в виде ASCII-команд. Особенностью протокола HTTP является возможность указать в запросе и ответе способ представления одного и того же ресурса по различным параметрам: формату, кодировке, языку и т. д. Именно благодаря возможности указания способа кодирования сообщения клиент и сервер могут обмениваться двоичными данными, хотя данный протокол является символьно-ориентированным.
HTTP — протокол прикладного уровня, но используется также в качестве «транспорта» для других прикладных протоколов, в первую очередь, основанных на языке XML (SOAP, XML-RPC, SiteMap, RSS и проч.).
CGI (Common Gateway Interface) — механизм доступа к программам на стороне веб-сервера. Спецификация CGI была разработана для расширения возможностей сервиса www за счет подключения различного внешнего программного обеспечения. При использовании CGI веб-сервер представляет браузеру доступ к исполнимым программам, запускаемым на его (серверной) стороне через стандартные потоки ввода и вывода.
Интерфейс CGI применяется для создания динамических веб-сайтов, например, когда веб-страницы формируются из результатов запроса к базе данных. Сейчас популярность CGI снизилась, т.к. появились более совершенные альтернативные решения (например, модульные расширения веб-серверов).
Веб-серверы
Веб-сервер — это сетевое приложение, обслуживающее HTTP-запросы от клиентов, обычно веб-браузеров. Веб-сервер принимает запросы и возвращает ответы, обычно вместе с HTML-страницей, изображением, файлом, медиа-потоком или другими данными. Веб-серверы — основа Всемирной паутины. С расширением спектра сетевых сервисов веб-серверы все чаще используются в качестве шлюзов для серверов приложений или сами представляют такие функции (например, Apache Tomcat).
Созданием программного обеспечения веб-серверов занимаются многие разработчики, но наибольшую популярность (по статистике http://netcraft.com) имеют такие программные продукты, как Apache (Apache Software Foundation), IIS (Microsoft), Google Web Server (GWS, Google Inc.) и nginx.
Apache — свободное программное обеспечение, распространяется под совместимой с GPL лицензией. Apache уже многие годы является лидером по распространенности во Всемирной паутине в силу своей надежности, гибкости, масштабируемости и безопасности.
IIS (Internet Information Services) — проприетарный набор серверов для нескольких служб Интернета, разработанный Майкрософт и распространяемый с серверными операционными системами семейства Windows. Основным компонентом IIS является веб-сервер, также поддерживаются протоколы FTP, POP3, SMTP, NNTP.
Google Web Server (GWS) — разработка компании Google на основе веб-сервера Apache. GWS оптимизирован для выполнения приложений сервиса Google Applications.
nginx [engine x] — это HTTP-сервер, совмещенный с кэширующим прокси-сервером. Разработан И. Сысоевым для компании Рамблер. Осенью 2004 года вышел первый публично доступный релиз, сейчас nginx используется на 9-12% веб-серверов.
Браузеры
Браузер, веб-обозреватель (web-browser) — клиентское приложение для доступа к веб-серверам по протоколу HTTP и просмотра веб-страниц. Как правило браузеры дополнительно поддерживают и ряд других протоколов (например ftp, file, mms, pop3).
Первые HTTP-клиенты были консольными и работали в текстовом режиме, позволяя читать гипертекст и перемещаться по ссылкам. Сейчас консольные браузеры (такие, как lynx, w3m или links) практически не используются рядовыми посетителями веб-сайтов. Тем не менее такие браузеры весьма полезны для веб-разработчиков, так как позволяют «увидеть» веб-страницу «глазами» поискового робота.
Исторически первым браузером в современном понимании (т.е. с графическим интерфейсом и т.д.) была программа NCSA Mosaic, разработанная Марком Андерисеном и Эриком Бина. Mosaic имел довольно ограниченные возможности, но его открытый исходный код стал основой для многих последующих разработок.
Существует большое число программ-браузеров, но наибольшей популярностью пользуются следующие [4] [3]:
Internet Explorer (IE) — браузер, разработанный компанией Майкрософт и тесно интегрированный c ОС Windows. Платформозависим (поддержка сторонних ОС прекращена начиная с версии 5). Единственный браузер, напрямую поддерживающий технологию ActiveX. Не полностью совместим со стандартами W3C, в связи с чем требует дополнительных затрат от веб-разработчиков.
Firefox — свободный кроссплатформенный браузер, разрабатываемый Mozilla Foundation и распространяемый под тройной лицензией GPL/LGPL/MPL. В основе браузера — движок Gekko, который изначально создавался для Netscape Communicator. Однако, вместо того, чтобы предоставить все возможности движка в стандартной поставке, Firefox реализует лишь основную его функциональность, предоставляя пользователям возможность модифицировать браузер в соответствии с их требованиями через поддержку расширений (add-ons), тем оформления и плагинов.
Safari — проприетарный браузер, разработанный корпорацией Apple и входящий в состав операционной системы Mac OS X. Бесплатно распространяется для операционных систем семейства Microsoft Windows. В браузере используется уникальный по производительности интерпретатор JavaScript и еще ряд интересных для пользователя решений, которые отсутствуют или не развиты в других браузерах.
Chrome — кроссплатформенный браузер с открытым исходным кодом, разрабатываемый компанией Google. Первая стабильная версия вышла 11 декабря 2008 года. В отличие от многих других браузеров, в Chrome каждая вкладка является отдельным процессом. В случае если процесс обработки содержимого вкладки зависнет, его можно будет завершить без риска потери данных других вкладок. Еще одна особенность — интеллектуальная адресная строка (Omnibox). К возможности автозаполнения она добавляет поисковые функции с учетом популярности сайта, релевантности и пользовательских предпочтений (истории переходов).
Opera — кроссплатформенный многофункциональный веб-браузер, впервые представленный в 1994 году группой исследователей из норвежской компании Telenor. Дальнейшая разработка ведется Opera Software ASA. Этот браузер обладает высокой скоростью работы и совместим с основными стандартами. Отличительными особенностями Opera долгое время являлись многостраничный интерфейс и возможность масштабирования веб-страниц целиком. На разных этапах развития в Opera были интегрированы возможности почтового/новостного клиента, адресной книги, клиента сети BitTorrent, агрегатора RSS, клиента IRC, менеджера закачек, WAP-браузера, а также поддержка виджетов — графических модулей, работающих вне окна браузера.
Роботы-«пауки»
Наряду с браузерами, ориентированными на пользователя, существуют и специализированные клиенты-роботы («пауки», «боты»), подключающиеся к веб-серверам и выполняющие различные задачи автоматической обработки гипертекстовой информации. Сюда относятся, в первую очередь, роботы поисковых систем, таких как google.com, yandex.ru, yahoo.com и т.п., выполняющие обход веб-сайтов для последующего построения поискового индекса.
Web 1.0 - до .com bubble. Статичное содержание страниц, аскетичный дизайн, чаты, форумы, гостевые книги.
Web 2.0 - новое поколение сайтов (после 2001) User-generated content. Предоставление и потребление API. RSS. Обновление страниц «на лету» (ajax).
Web 3.0 - ??? Community-generated content. Семантическая паутина. Уникальные идентификаторы и микроформаты.
[1] | https://ru.wikipedia.org/wiki/Интернет |
[2] | http://www.4stud.info/web-programming/lecture1.html |
[3] | http://evolutionofweb.appspot.com/ |
[4] | http://www.w3schools.com/browsers/default.asp |
См.также
См.также
Понятие Веб-сервер может относиться как к железу, так и к программному обеспечению (ПО).
Простыми словами, когда браузеру нужен файл, размещенный на веб-сервере, браузер запрашивает его через HTTP. Когда запрос достигает нужного веб-сервера (железо), сервер HTTP (ПО) передает запрашиваемый документ обратно, также через HTTP.
См.также
Чтобы опубликовать веб-сайт, нужен либо статический, либо динамический веб-сервер.
Статический веб-сервер или стек состоит из компьютера (железо) с сервером HTTP (ПО). Мы называем это «статикой», потому что сервер посылает размещенные на нем файлы в браузер не изменяя их.
Динамических веб-сервер состоит из статического веб-сервера плюс дополнительного программного обеспечения, наиболее часто сервером приложений и базы данных. Мы называем его «динамический», потому что сервер приложений изменяет исходные файлы перед отправкой в ваш браузер по HTTP.
Например, для получения итоговой страницы, которую вы видите в браузере, сервер приложений может заполнить HTML шаблон данными из базы данных. Такие сайты, как MDN (Mozilla Developer Network) или Википедия состоят из тысяч веб-страниц, но они не являются реальными HTML документами, лишь несколько HTML шаблонов и гигантские базы данных. Эта структура упрощает и ускоряет сопровождение веб-приложений и доставку контента.
Чтобы загрузить веб-страницу, как мы уже говорили, браузер отправляет запрос к веб-серверу, который приступает к поиску запрашиваемого файла в своем собственном пространстве памяти. Найдя файл, сервер считывает его, обрабатывает так, как ему это необходимо, и направляет его в браузер. Давайте рассмотрим эти шаги более подробно.
Во-первых, веб-сервер хранит файлы веб-сайта, а именно все HTML документы и связанные с ними ресурсы, включая изображения, CSS стили, JavaScript файлы, шрифты и видео.
Технически, вы можете разместить все эти файлы на своем компьютере, но гораздо удобнее хранить их на выделенном веб-сервере, который:
Таким образом, выбор хорошего хостинг-провайдера является важной частью создания сайта. Рассмотрите различные предложения компаний и выберите то, что соответствует вашим потребностям и бюджету (предложения варьируются от бесплатных до тысяч долларов в месяц).
Во-вторых, веб-сервер обеспечивает поддержку HTTP (hypertext transfer protocol). Как следует из названия, HTTP указывает, как передавать гипертекст (т.е. связанные веб-документы) между двумя компьютерами.
Протокол представляет собой набор правил для связи между двумя компьютерами. HTTP является текстовым протоколом без сохранения состояния.
Текстовый
Все команды это человеко-читаемый текст.
Не сохраняет состояние
Ни клиент, ни сервер, не помнят о предыдущих соединениях. Например, опираясь только на HTTP, сервер не сможет вспомнить введенный вами пароль, или на каком шаге транзакции вы находитесь. Для таких задач вам потребуется сервер приложений.
HTTP задает строгие правила, как клиент и сервер должны общаться. Более подробно смотри http-protocol. Вот некоторые из них:
На веб-сервере, HTTP сервер отвечает за обработку входящих запросов и ответ на них.
Грубо говоря, сервер может отдавать статическое или динамическое содержимое.
«Статическое» означает «отдается как есть». Статические веб-сайты проще всего установить, поэтому мы предлагаем вам сделать свой первый сайт статическим.
«Динамическое» означает, что сервер обрабатывает данные или даже генерирует их на лету из базы данных. Это обеспечивает больше гибкости, но технически сложнее в обслуживании, что делает его более сложным для создания веб-сайта.
Возьмем к примеру страницу What is web server, перевод которой вы читаете. На веб-сервере, где это хостится, есть сервер приложений, который извлекает содержимое статьи из базы данных, форматирует его, добавляет в HTML шаблоны и отправляет вам результат. В нашем случае, сервер приложений называется Kuma, написан он на языке программирования Python (используя фреймворк Django). Команда Mozilla создали Kuma для конкретных нужд MDN, но есть много подобных приложений, построенных на многих других технологий.
Существует много серверов приложений для разных запросов, поэтому довольно трудно выбрать какой-то один универсальный. Некоторые серверы приложений удовлетворяют определенной категории веб-сайтов, такие как блоги, вики или интернет-магазины; другие, называемые CMS (системы управления контентом), являются более общими. Если вы создаете динамический сайт, потратьте немного времени на выбор инструмента, который соответствует вашим потребностям. Если вы не хотите изучать веб-программирование (хотя это захватывающая область сама по себе!), то вам не нужно создавать свой собственный сервер приложений. Это будет очередной велосипед.
См.также
CGI (от англ. Common Gateway Interface — «общий интерфейс шлюза») — стандарт интерфейса, используемого для связи внешней программы с веб-сервером. Программу, которая работает по такому интерфейсу совместно с веб-сервером, принято называть шлюзом, хотя многие предпочитают названия «скрипт» (сценарий) или «CGI-программа».
Поскольку гипертекст статичен по своей природе, веб-страница не может непосредственно взаимодействовать с пользователем. До появления JavaScript, не было иной возможности отреагировать на действия пользователя, кроме как передать введенные им данные на веб-сервер для дальнейшей обработки. В случае CGI эта обработка осуществляется с помощью внешних программ и скриптов, обращение к которым выполняется через стандартизованный (см. RFC 3875: CGI Version 1.1) интерфейс — общий шлюз.
Упрощенная модель, иллюстрирующая работу CGI:
Сам интерфейс разработан таким образом, чтобы можно было использовать любой язык программирования, который может работать со стандартными устройствами ввода-вывода. Такими возможностями обладают даже скрипты для встроенных командных интерпретаторов операционных систем, поэтому в простых случаях могут использоваться даже командные скрипты.
Обобщенный алгоритм работы через CGI можно представить в следующем виде:
Наиболее частая задача, для решения которой применяется CGI — создание интерактивных страниц, содержание которых зависит от действий пользователя. Типичными примерами таких веб-страниц является форма регистрации на сайте или форма для отправки комментария. Другая область применения CGI, остающаяся за кулисами взаимодействия с пользователем, связана со сбором и обработкой информации о клиенте: установка и чтение «печенюшек»-cookies; получение данных о браузере и операционной системе; подсчет количества посещений веб-страницы; мониторинг веб-трафика и т.п.
Это обеспечивается возможностью подключения CGI-скрипта к базе данных, а также возможностью обращаться к файловой системе сервера. Таким образом CGI-скрипт может сохранять информацию в таблицах БД или файлах и получать ее оттуда по запросу, чего нельзя сделать средствами HTML.
Предупреждение
CGI — это не язык программирования! Это простой протокол, позволяющий веб-серверу передавать данные через stdin и читать их из stdout. Поэтому в качестве CGI-обработчика может использоваться любая серверная программа, способная работать со стандартными потоками ввода-вывода.
Пример на Python:
#!/usr/bin/python
print("""Content-Type: text/plain
Hello, world!""")
В этом коде строка #!/usr/bin/python
указывает полный путь к интерпретатору Python.
Пример на Си:
#include <stdio.h>
int main(void) {
printf("Content-Type: text/plain\n\n");
printf("Hello, world!\n\n");
return 0;
}
Строка Content-type: text/html\n\n
— http-заголовок, задающий тип содержимого (mime-type). Удвоенный символ разрыва строки (\n\n) — обязателен, он отделяет заголовки от тела сообщения.
Все скрипты, как правило, помещают в каталог cgi (или cgi-bin) сервера, но это необязательно: скрипт может располагаться где угодно, но при этом большинство веб-серверов требуют специальной настройки. В веб-сервере Apache, например, такая настройка может производиться при помощи общего файла настроек httpd.conf или с помощью файла .htaccess в том каталоге, где содержится этот скрипт. Также скрипты должны иметь права на исполнение (chmod +x hello.py
).
Все CGI-приложения имеют доступ к переменным окружения, устанавливаемым веб-сервером. Эти переменные играют важную роль при написании CGI-программ. В таблице перечислены некоторые из переменных, доступных CGI.
Пример вывода переменных окружения CGI-скрипта:
1 2 3 4 5 6 7 8 | #!/usr/bin/python
import os
print("Content-type: text/html\r\n\r\n")
print("<font size=+10>Environment</font><br>")
for param in os.environ.keys():
print("<b>%20s</b>: %s<br>" % (param, os.environ[param])) |
Самым большим недостатком этой технологии являются повышенные требования к производительности веб-сервера. Дело в том, что каждое обращение к CGI-приложению вызывает порождение нового процесса, со всеми вытекающими отсюда накладными расходами. Если же приложение написано с ошибками, то возможна ситуация, когда оно, например, зациклится. Браузер прервет соединение по истечении тайм-аута, но на серверной стороне процесс будет продолжаться, пока администратор не снимет его принудительно.
См.также
Для запуска CGI сервера необходимо перейти в директорию sourcecode
и выполнить команду:
python -m CGIHTTPServer 8000
или
python3 -m http.server --cgi 8000
или cgiserver.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2014 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
"""
Demo CGI Server
"""
try:
import BaseHTTPServer
import CGIHTTPServer
except ImportError:
import http.server as BaseHTTPServer
import http.server as CGIHTTPServer
import cgitb
cgitb.enable() # This line enables CGI error reporting
server = BaseHTTPServer.HTTPServer
handler = CGIHTTPServer.CGIHTTPRequestHandler
server_address = ("", 8000)
handler.cgi_directories = ["/cgi-bin", "/wsgi"]
httpd = server(server_address, handler)
httpd.serve_forever() |
python cgiserver.py
Теперь CGI-скрипты доступны на 8000
порту,
например по адресу http://localhost:8000/cgi-bin/1.hello.py
Примечание
Для компиляции кода на C++ необходимо установить библиотеку cgicc:
sudo apt-get install libcgicc5-dev
Пример компиляции:
g++ -o 3.get.post.cgi 3.get.post.cpp -lcgicc
Примечание
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2014 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
#
# 1.hello.py
# http://www.tutorialspoint.com/python/python_cgi_programming.htm
print("Content-type:text/html\r\n\r\n")
print('<html>')
print('<head>')
print('<title>Hello Word - First CGI Program</title>')
print('</head>')
print('<body>')
print('<h2>Hello Word! This is my first CGI program</h2>')
print('</body>')
print('</html>') |
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #!/usr/bin/env ruby
#
# 1.hello.rb
# Copyright (C) 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
puts "Content-type:text/html\r\n\r\n"
puts '<html>'
puts '<head>'
puts '<title>Hello Word - First CGI Program</title>'
puts '</head>'
puts '<body>'
puts '<h2>Hello Word! This is my first CGI program</h2>'
puts '</body>'
puts '</html>' |
C++
Для компиляции: make 1_hello
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /*
* 1.hello.cpp
* Copyright (C) 2015 uralbash <root@uralbash.ru>
*
* Distributed under terms of the MIT license.
*/
#include <iostream>
using namespace std;
int main()
{
cout << "Content-type:text/html\r\n\r\n";
cout << "<html>\n";
cout << "<head>\n";
cout << "<title>Hello World - First CGI Program</title>\n";
cout << "</head>\n";
cout << "<body>\n";
cout << "<h2>Hello World! This is my first CGI program</h2>\n";
cout << "</body>\n";
cout << "</html>\n";
return 0;
} |
Go
Для компиляции: make 1_hello_go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | package main
import "fmt"
func main() {
html :=
`Content-type:text/html
<html>
<head>
<title>Hello Word - First CGI Program</title>
</head>
<body>
<h2>Hello Word! This is my first CGI program</h2>
</body>
</html>`
fmt.Println(html)
} |
Примечание
Python
1 2 3 4 5 6 7 8 | #!/usr/bin/python
import os
print("Content-type: text/html\r\n\r\n")
print("<font size=+10>Environment</font><br>")
for param in os.environ.keys():
print("<b>%20s</b>: %s<br>" % (param, os.environ[param])) |
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #!/usr/bin/env ruby
#
# 2.environment.rb
# Copyright (C) 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
#
print "Content-type: text/html\r\n\r\n"
print "<font size=+10>Environment</font><br>"
for param in ENV
print "<b>%20s</b>: %s<br>" % param
end |
C++
Для компиляции: make 2_environment
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | /*
* 2.environment.cpp
* Copyright (C) 2015 uralbash <root@uralbash.ru>
*
* Distributed under terms of the MIT license.
*/
#include <iostream>
using namespace std;
int main(int argc, char **argv, char** env)
{
cout << "Content-type:text/html\r\n\r\n";
cout << "<html>\n";
cout << "<head>\n";
cout << "<title>CGI Envrionment Variables</title>\n";
cout << "</head>\n";
cout << "<body>\n";
while (*env)
cout << *env++ << "<br/>";
cout << "</body>\n";
cout << "</html>\n";
return 0;
} |
Примечание
GET (action=»http://localhost:8000/cgi-bin/3.get.post.cgi» method=»get»)
POST (action=»http://localhost:8000/cgi-bin/3.get.post.cgi» method=»post»)
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | #! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2014 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
#
# CGI get method
# http://www.tutorialspoint.com/python/python_cgi_programming.htm
import cgi
# Create instance of FieldStorage
form = cgi.FieldStorage()
# Get data from fields
first_name = form.getvalue('first_name')
last_name = form.getvalue('last_name')
print("Content-type:text/html\r\n\r\n")
print("<html>")
print("<head>")
print("<title>Hello - Second CGI Program</title>")
print("</head>")
print("<body>")
print("<h2>Hello %s %s</h2>" % (first_name, last_name))
print("</body>")
print("</html>") |
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #!/usr/bin/env ruby
#
# 3.get.rb
# Copyright (C) 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
#
require 'cgi'
cgi = CGI.new
# Get data from fields
first_name = cgi['first_name']
last_name = cgi['last_name']
print "Content-type:text/html\r\n\r\n"
print "<html>"
print "<head>"
print "<title>Hello - Second CGI Program</title>"
print "</head>"
print "<body>"
print "<h2>Hello %s %s</h2>" % [first_name, last_name]
print "</body>"
print "</html>" |
C++
Для компиляции: make 3_get_post
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | /*
* 3.get.cpp
* Copyright (C) 2015 uralbash <root@uralbash.ru>
*
* Distributed under terms of the MIT license.
*/
#include <iostream>
#include <cgicc/Cgicc.h>
using namespace std;
using namespace cgicc;
int main()
{
Cgicc formData;
cout << "Content-type:text/html\r\n\r\n";
cout << "<html>\n";
cout << "<head>\n";
cout << "<title>Using GET and POST Methods</title>\n";
cout << "</head>\n";
cout << "<body>\n";
form_iterator fi = formData.getElement("first_name");
if(fi != (*formData).end()) {
cout << "First name: " << **fi << endl;
}else{
cout << "No text entered for first name" << endl;
}
cout << "<br/>\n";
fi = formData.getElement("last_name");
if(fi != (*formData).end()) {
cout << "Last name: " << **fi << endl;
}else{
cout << "No text entered for last name" << endl;
}
cout << "<br/>\n";
cout << "</body>\n";
cout << "</html>\n";
return 0;
} |
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | #! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2014 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
#
# CGI checkbox
# http://www.tutorialspoint.com/python/python_cgi_programming.htm
import cgi
# Create instance of FieldStorage
form = cgi.FieldStorage()
# Get data from fields
maths = form.getvalue('maths')
physics = form.getvalue('physics')
print("Content-type:text/html\r\n\r\n")
print("<html>")
print("<head>")
print("<title>Checkbox - Third CGI Program</title>")
print("</head>")
print("<body>")
if maths:
print("Maths Flag: ON")
else:
print("Maths Flag: OFF")
print("<br>")
if physics:
print("Physics Flag: ON")
else:
print("Physics Flag: OFF")
print("</body>")
print("</html>") |
C++
Для компиляции: make 4_checkbox
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | /*
* 4.checkbox.cpp
* Copyright (C) 2015 uralbash <root@uralbash.ru>
*
* Distributed under terms of the MIT license.
*/
#include <iostream>
#include <cgicc/Cgicc.h>
using namespace std;
using namespace cgicc;
int main()
{
Cgicc formData;
bool maths_flag, physics_flag;
cout << "Content-type:text/html\r\n\r\n";
cout << "<html>\n";
cout << "<head>\n";
cout << "<title>Checkbox Data to CGI</title>\n";
cout << "</head>\n";
cout << "<body>\n";
maths_flag = formData.queryCheckbox("maths");
if( maths_flag ) {
cout << "Maths Flag: ON " << endl;
}else{
cout << "Maths Flag: OFF " << endl;
}
cout << "<br/>\n";
physics_flag = formData.queryCheckbox("physics");
if( physics_flag ) {
cout << "Physics Flag: ON " << endl;
}else{
cout << "Physics Flag: OFF " << endl;
}
cout << "<br/>\n";
cout << "</body>\n";
cout << "</html>\n";
return 0;
} |
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2014 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
#
# CGI radio
# http://www.tutorialspoint.com/python/python_cgi_programming.htm
import cgi
# Create instance of FieldStorage
form = cgi.FieldStorage()
# Get data from fields
if form.getvalue('subject'):
subject = form.getvalue('subject')
else:
subject = "Not set"
print("Content-type:text/html\r\n\r\n")
print("<html>")
print("<head>")
print("<title>Radio - Fourth CGI Program</title>")
print("</head>")
print("<body>")
print("<h2> Selected Subject is %s</h2>" % subject)
print("</body>")
print("</html>") |
C++
Для компиляции: make 5_radio
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | /*
* 5.radio.cpp
* Copyright (C) 2015 uralbash <root@uralbash.ru>
*
* Distributed under terms of the MIT license.
*/
#include <iostream>
#include <cgicc/Cgicc.h>
using namespace std;
using namespace cgicc;
int main()
{
Cgicc formData;
cout << "Content-type:text/html\r\n\r\n";
cout << "<html>\n";
cout << "<head>\n";
cout << "<title>Radio Button Data to CGI</title>\n";
cout << "</head>\n";
cout << "<body>\n";
form_iterator fi = formData.getElement("subject");
if( !fi->isEmpty() && fi != (*formData).end()) {
cout << "Radio box selected: " << **fi << endl;
}
cout << "<br/>\n";
cout << "</body>\n";
cout << "</html>\n";
return 0;
} |
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
import cgi
# Create instance of FieldStorage
form = cgi.FieldStorage()
# Get data from fields
if form.getvalue('textcontent'):
text_content = form.getvalue('textcontent')
else:
text_content = "Not entered"
print("Content-type:text/html\r\n\r\n")
print("<html>")
print("<head>")
print("<title>Text Area - Fifth CGI Program</title>")
print("</head>")
print("<body>")
print("<h2> Entered Text Content is %s</h2>" % text_content)
print("</body>") |
C++
Для компиляции: make 6_textarea
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | /*
* 6.textarea.cpp
* Copyright (C) 2015 uralbash <root@uralbash.ru>
*
* Distributed under terms of the MIT license.
*/
#include <iostream>
#include <cgicc/Cgicc.h>
using namespace std;
using namespace cgicc;
int main()
{
Cgicc formData;
cout << "Content-type:text/html\r\n\r\n";
cout << "<html>\n";
cout << "<head>\n";
cout << "<title>Text Area Data to CGI</title>\n";
cout << "</head>\n";
cout << "<body>\n";
form_iterator fi = formData.getElement("textcontent");
if( !fi->isEmpty() && fi != (*formData).end()) {
cout << "Text Content: " << **fi << endl;
}else{
cout << "No text entered" << endl;
}
cout << "<br/>\n";
cout << "</body>\n";
cout << "</html>\n";
return 0;
} |
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
import cgi
# Create instance of FieldStorage
form = cgi.FieldStorage()
# Get data from fields
if form.getvalue('dropdown'):
subject = form.getvalue('dropdown')
else:
subject = "Not entered"
print("Content-type:text/html\r\n\r\n")
print("<html>")
print("<head>")
print("<title>Dropdown Box - Sixth CGI Program</title>")
print("</head>")
print("<body>")
print("<h2> Selected Subject is %s</h2>" % subject)
print("</body>")
print("</html>") |
C++
Для компиляции: make 7_dropdown
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | /*
* 7.dropdown.cpp
* Copyright (C) 2015 uralbash <root@uralbash.ru>
*
* Distributed under terms of the MIT license.
*/
#include <iostream>
#include <cgicc/Cgicc.h>
using namespace std;
using namespace cgicc;
int main()
{
Cgicc formData;
cout << "Content-type:text/html\r\n\r\n";
cout << "<html>\n";
cout << "<head>\n";
cout << "<title>Drop Down Box Data to CGI</title>\n";
cout << "</head>\n";
cout << "<body>\n";
form_iterator fi = formData.getElement("dropdown");
if( !fi->isEmpty() && fi != (*formData).end()) {
cout << "Value Selected: " << **fi << endl;
}
cout << "<br/>\n";
cout << "</body>\n";
cout << "</html>\n";
return 0;
} |
Примечание
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2014 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
# Import modules for CGI handling
from os import environ
print("Content-type:text/html\r\n\r\n")
print("<html>")
print("<head>")
print("<title>Get Cookie</title>")
print("</head>")
print("<body>")
if 'HTTP_COOKIE' in environ:
for cookie in environ['HTTP_COOKIE'].split(';'):
(key, value) = cookie.split('=')
print("%s: %s" % (key, value))
print("<br/>")
print("</body>")
print("</html>") |
C++
Для компиляции: make 8_getcookie
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | /*
* 8.getcookie.cpp
* Copyright (C) 2015 uralbash <root@uralbash.ru>
*
* Distributed under terms of the MIT license.
*/
#include <iostream>
#include <cgicc/Cgicc.h>
using namespace std;
using namespace cgicc;
int main()
{
Cgicc cgi;
const_cookie_iterator cci;
cout << "Content-type:text/html\r\n\r\n";
cout << "<html>\n";
cout << "<head>\n";
cout << "<title>Cookies in CGI</title>\n";
cout << "</head>\n";
cout << "<body>\n";
cout << "<table border = \"0\" cellspacing = \"2\">";
// get environment variables
const CgiEnvironment& env = cgi.getEnvironment();
for( cci = env.getCookieList().begin();
cci != env.getCookieList().end();
++cci )
{
cout << "<tr><td>" << cci->getName() << "</td><td>";
cout << cci->getValue();
cout << "</td></tr>\n";
}
cout << "</table>\n";
cout << "<br/>\n";
cout << "</body>\n";
cout << "</html>\n";
return 0;
} |
Примечание
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2014 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
print("Set-Cookie:UserID=XYZ;")
print("Set-Cookie:Password=XYZ123;")
print("Set-Cookie:Expires=Tuesday, 31-Dec-2007 23:12:40 GMT;")
print("Set-Cookie:Domain=www.tutorialspoint.com;")
print("Set-Cookie:Path=/perl;")
print("Content-type:text/html\r\n")
print("<html>")
print("<head>")
print("<title>Cookies in CGI</title>")
print("</head>")
print("<body>")
print("Setting cookies")
print("<br/>")
print("</body>")
print("</html>") |
C++
Для компиляции: make 9_setcookie
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | /*
* 9.setcookie.cpp
* Copyright (C) 2015 uralbash <root@uralbash.ru>
*
* Distributed under terms of the MIT license.
*/
#include <iostream>
using namespace std;
int main()
{
cout << "Set-Cookie:UserID=XYZ;\r\n";
cout << "Set-Cookie:Password=XYZ123;\r\n";
cout << "Set-Cookie:Domain=www.tutorialspoint.com;\r\n";
cout << "Set-Cookie:Path=/perl;\n";
cout << "Content-type:text/html\r\n\r\n";
cout << "<html>\n";
cout << "<head>\n";
cout << "<title>Cookies in CGI</title>\n";
cout << "</head>\n";
cout << "<body>\n";
cout << "Setting cookies" << endl;
cout << "<br/>\n";
cout << "</body>\n";
cout << "</html>\n";
return 0;
} |
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
import cgi
import os
form = cgi.FieldStorage()
# Get filename here.
fileitem = form['filename']
# Test if the file was uploaded
if fileitem.filename:
# strip leading path from file name to avoid
# directory traversal attacks
fn = os.path.basename(fileitem.filename)
open('/tmp/' + fn, 'wb').write(fileitem.file.read())
message = 'The file "' + fn + '" was uploaded successfully'
else:
message = 'No file was uploaded'
print("""\
Content-Type: text/html\n
<html>
<body>
<p>%s</p>
</body>
</html>""" % (message,)) |
C++
Для компиляции: make 10_fileupload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | /*
* 10.fileupload.cpp
* Copyright (C) 2015 uralbash <root@uralbash.ru>
*
* Distributed under terms of the MIT license.
*/
#include <iostream>
#include <cgicc/Cgicc.h>
#include <cgicc/HTTPHTMLHeader.h>
using namespace std;
using namespace cgicc;
int main()
{
Cgicc cgi;
// get list of files to be uploaded
const_file_iterator file = cgi.getFile("filename");
if(file != cgi.getFiles().end()) {
// send data type at cout.
cout << HTTPContentHeader(file->getDataType());
// write content at cout.
file->writeToStream(cout);
cout << "<br><br>";
cout << "File uploaded successfully!\n";
} else
cout << "No file :(";
return 0;
} |
Примечание
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | #! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
import cgitb
cgitb.enable()
def func1(arg1):
local_var = arg1 * 2
return func2(local_var)
def func2(arg2):
local_var = arg2 + 2
return func3(local_var)
def func3(arg3):
local_var = arg2 / 2 # noqa
return local_var
func1(1) |
См.также
В отличие от CGI, FastCGI использует постоянно запущенные процессы для обработки множества запросов.
CGI-программы взаимодействуют с сервером через STDIN и STDOUT запущенного процесса.
FastCGI-процессы используют для связи с сервером Unix Domain Sockets или TCP/IP . Это даёт следующее преимущество над обычными CGI-программами: FastCGI-программы могут быть запущены не только на этом же сервере, но и где угодно в сети. Также возможна обработка запросов несколькими FastCGI-процессами, работающими параллельно. Можно использовать несколько FastCGI-серверов, распределяя нагрузку между ними с помощью nginx или lighttpd.
После установления соединения FastCGI-процесса с web-сервером, между ними начинается обмен данными с использованием простого протокола, решающего две задачи: организация двунаправленного обмена в рамках одного соединения (для эмуляции STDIN, STDOUT, STDERR) и организация нескольких независимых FastCGI-сессий в рамках одного соединения.
Все передаваемые данные оборачиваются в FastCGI-записи — единицу данных протокола. FastCGI-записи служат для организации двунаправленного обмена и мультиплексирования нескольких сессий в рамках одного соединения.
FastCGI-запись состоит из заголовка фиксированной длины, следующего за ним содержимого и выравнивающих данных переменной длины. Каждая запись содержит 7 элементов.
Nginx
Примечание
Nginx доступен по адресу http://localhost:8080/
1 2 3 4 5 6 7 8 9 10 11 | # default.nginx
server {
listen 80 default_server;
root /usr/share/nginx/html;
index index.html index.htm;
include includes/fcgi.nginx;
include includes/static.nginx;
} |
1 2 3 4 5 6 7 | # fcgi.nginx
location /fastcgi_hello {
# host and port to fastcgi server
include fastcgi.conf;
fastcgi_pass 172.17.0.89:5000;
} |
fastcgi_param
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
С
Примечание
Компиляция gcc -o hello.fcgi hello.cpp -lfcgi
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /*
* hello.cpp
* Copyright (C) 2015 uralbash <root@uralbash.ru>
*
* Distributed under terms of the MIT license.
*/
#include "fcgi_stdio.h"
#include <stdlib.h>
int main(void)
{
while(FCGI_Accept() >= 0)
{
printf("Content-type: text/html\r\nStatus: 200 OK\r\n\r\nHello World!");
}
return 0;
} |
Запуск fcgi сервера на 5000 порту
spawn-fcgi -p 5000 -n hello.fcgi
Примечание
Пример доступен по адресу http://localhost:8080/fastcgi_hello
См.также
В некоторых языках, например в Go, уже существует встроенный Веб-сервер, который можно использовать в вашем приложении.
См.также
В этом случае не нужно запускать отдельно fcgi сервер,
например spawn-fcgi
.
Примечание
Компиляция go build -o hello.go.fcgi hello.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | package main
import (
"fmt"
"net"
"net/http"
"net/http/fcgi"
)
func handler(res http.ResponseWriter, req *http.Request) {
fmt.Fprint(res, "Hello World!")
}
func main() {
// For local machine
// l, _ := net.Listen("unix", "/var/run/go-fcgi.sock")
l, err := net.Listen("tcp", "0.0.0.0:5000") // TCP 5000 listen
if err != nil {
return
}
http.HandleFunc("/", handler)
fcgi.Serve(l, nil)
} |
Запуск go fcgi сервера на 5000 порту (Без компиляции).
go run hello.go
Или скомпилированный файл.
./hello.go.fcgi
Настройка Nginx
1 2 3 4 5 6 7 | # fcgi.nginx
location /fastcgi_hello {
# host and port to fastcgi server
include fastcgi.conf;
fastcgi_pass 172.17.0.89:5000;
} |
Client Request ----> Nginx (Reverse-Proxy) ----> App. FastCGI Server I. 127.0.0.1:5000
либо с балансировкой на несколько серверов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # Nginx
upstream myapp1 {
server 127.0.0.1:5000;
server 127.0.0.1:5001;
server 127.0.0.1:5002;
}
server {
listen 80;
location /some/path {
fastcgi_pass http://myapp1;
}
} |
Client Request ----> Nginx (Reverse-Proxy)
|
/|\
| | `-> App. FastCGI Server I. 127.0.0.1:5000
| `--> App. FastCGI Server II. 127.0.0.1:5001
`----> App. FastCGI Server III. 127.0.0.1:5002
Запуск напрямую без CGI и FastCGI.
Примечание
Компиляция go build -o hello.go.http hello.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello World!")
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8000", nil)
} |
Запуск go http сервера на 8000 порту (Без компиляции).
go run hello.go
Или скомпилированный файл.
./hello.go.http
На такой сервер можно зайти напрямую по адресу http://localhost:8000/, либо настроить обратный прокси сервер:
1 2 3 4 5 | # Nginx
location /some/path/ {
proxy_pass http://127.0.0.1:8000;
} |
Client Request ----> Nginx (Reverse-Proxy) ----> App. HTTP Server I. 127.0.0.1:8000
либо с балансировкой на несколько серверов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # Nginx
upstream myapp1 {
server 127.0.0.1:8000;
server 127.0.0.1:8001;
server 127.0.0.1:8002;
}
server {
listen 80;
location /some/path {
proxy_pass http://myapp1;
}
} |
Client Request ----> Nginx (Reverse-Proxy)
|
/|\
| | `-> App. HTTP Server I. 127.0.0.1:8000
| `--> App. HTTP Server II. 127.0.0.1:8001
`----> App. HTTP Server III. 127.0.0.1:8002
См.также
WSGI — стандарт взаимодействия между Python-программой, выполняющейся на стороне сервера, и самим веб-сервером, например Apache.
Идея:
В Python существует большое количество различного рода веб-фреймворков, тулкитов и библиотек. У каждого из них собственный метод установки и настройки, они не умеют взаимодействовать между собой. Это может стать затруднением для тех, кто только начинает изучать Python, так как, например, выбор определённого фреймворка может ограничить выбор веб-сервера и наоборот.
WSGI предоставляет простой и универсальный интерфейс между большинством веб-серверов и веб-приложениями или фреймворками.
По стандарту, WSGI-приложение должно удовлетворять следующим требованиям:
Простейшим примером WSGI-приложения может служить такая функция-генератор:
1 2 3 4 5 6 7 8 9 10 | def simple_app(environ, start_response):
"""
(dict, callable( status: str,
headers: list[(header_name: str, header_value: str)]))
-> body: iterable of strings
"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return 'Hello world!\n' |
или то же самое в виде класса:
1 2 3 4 5 6 7 8 9 10 | class AppClass(object):
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
def __iter__(self):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers)
yield "Hello world!\n" |
Чтобы запустить наше WSGI приложение, нужен WSGI сервер. Он запускает WSGI приложение один раз при каждом HTTP запросе от клиента.
Задачи WSGI сервера:
Пример WSGI-шлюза к CGI-серверу.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | import os
import sys
def run_with_cgi(application):
environ = dict(os.environ.items())
environ['wsgi.input'] = sys.stdin
environ['wsgi.errors'] = sys.stderr
environ['wsgi.version'] = (1, 0)
environ['wsgi.multithread'] = False
environ['wsgi.multiprocess'] = True
environ['wsgi.run_once'] = True
if environ.get('HTTPS', 'off') in ('on', '1'):
environ['wsgi.url_scheme'] = 'https'
else:
environ['wsgi.url_scheme'] = 'http'
headers_set = []
headers_sent = []
def write(data):
if not headers_set:
raise AssertionError("write() before start_response()")
elif not headers_sent:
# Before the first output, send the stored headers
status, response_headers = headers_sent[:] = headers_set
sys.stdout.write('Status: %s\r\n' % status)
for header in response_headers:
sys.stdout.write('%s: %s\r\n' % header)
sys.stdout.write('\r\n')
sys.stdout.write(data)
sys.stdout.flush()
def start_response(status, response_headers, exc_info=None):
if exc_info:
try:
if headers_sent:
# Re-raise original exception if headers sent
raise Exception(exc_info[0], exc_info[1], exc_info[2])
finally:
exc_info = None # avoid dangling circular ref
elif headers_set:
raise AssertionError("Headers already set!")
headers_set[:] = [status, response_headers]
return write
result = application(environ, start_response)
try:
for data in result:
if data: # don't send headers until body appears
write(data)
if not headers_sent:
write('') # send headers now if body was empty
finally:
if hasattr(result, 'close'):
result.close() |
Всегда словарь
environ
это обычно копия переменных окружения ОС os.environ
+ стандартные CGI переменные.
SCRIPT_NAME
- содержит имя вызванного скрипта. Например: myapp.py
PATH_INFO
- путь к файлу /cgi-bin/myapp.py
также включает в себя дополнительные WSGI-специфичные переменные, наиболее важные из них:
wsgi.input
- представляет тело (body) HTTP запроса.
wsgi.errors
- указывает поток куда нужно выводить ошибки.
wsgi.url_scheme
- это просто «http» или «https».
См.также
Функция start_response
принимает два обязательных аргумента:
status
- строка содержащая статус HTTP ответа, например 200 OK
.response_headers
- список кортежей, которые содержат заголовки ответа,
например [('Content-Type', 'text/html'), ('Content-Length', '15')
.1 2 3 4 5 6 7 8 9 10 | def simple_app(environ, start_response):
"""
(dict, callable( status: str,
headers: list[(header_name: str, header_value: str)]))
-> body: iterable of strings
"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return 'Hello world!\n' |
start_response
возвращает вызываемый объект, обычно «write».
write
выводит тело ответа в поток вывода, используется при необычных
обстоятельствах.
Предупреждение
Обратный вызов write
плохо поддерживается серверами и веб-фреймворками,
поэтому рекомендуется проектировать свои приложения без его вызова.
Обычно данные возвращаются таким образом:
def application(environ, start_response):
start_response(status, headers)
return ['content block 1',
'content block 2',
'content block 3']
Но можно делать и так:
def application(environ, start_response):
write = start_response(status, headers)
write('content block 1')
return ['content block 2',
'content block 3']
Запуск нашего приложения через WSGI-шлюз к CGI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | #! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
"""
Example from pep-333
"""
from cgi_gateway import run_with_cgi
def simple_app(environ, start_response):
"""
(dict, callable( status: str,
headers: list[(header_name: str, header_value: str)]))
-> body: iterable of strings
"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return 'Hello world!\n'
class AppClass(object):
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
def __iter__(self):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers)
yield "Hello world!\n"
if __name__ == '__main__':
run_with_cgi(AppClass) |
Результат выполнения:
$ python 1.cgi.app.py
Status: 200 OK
Content-type: text/plain
Hello world!
Примечание
В исходных кодах к лекциям cgiserver.py
делает этот пример доступным по адресу
http://localhost:8000/wsgi/1.cgi.app.py
Помимо приложений и серверов, стандарт дает определение middleware-компонентов, предоставляющих интерфейсы как приложению, так и серверу. То есть для сервера middleware является приложением, а для приложения сервером. Это позволяет составлять «цепочки» WSGI-совместимых middleware.
Middleware могут брать на себя следующие функции (но не ограничиваются этим):
Мы рассмотрим пример приложения, которое считает количество обращений и использует следующие middleware:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | def app(environ, start_response):
# Except error
if 'error' in environ['PATH_INFO'].lower():
raise Exception('Detect "error" in URL path')
# Session
session = environ.get('paste.session.factory', lambda: {})()
if 'count' in session:
count = session['count']
else:
count = 1
session['count'] = count + 1
# Generate response
start_response('200 OK', [('Content-Type', 'text/plain')])
return [b'You have been here %d times!\n' % count, ] |
Приложение выводит число 1 при первом обращении, записывает его в сессию и при каждом последующем обращении увеличивает число на 1.
Т.к. протокол HTTP не сохраняет предыдущего состояния, то при обновлении страницы число не увеличится. Чтобы это произошло, нужно реализовать механизм сессий.
1 2 | from paste.evalexception.middleware import EvalException
app = EvalException(app) |
EvalException
позволяет нам отлавливать ошибки и выводить их в браузере.
Если мы перейдем по адресу http://localhost:8000/Errors_500, наше приложение
найдет слово error в пути и искусственно вызовет исключение.
1 2 | from paste.session import SessionMiddleware
app = SessionMiddleware(app) |
SessionMiddleware
добавляет cookie клиенту с ключом _SID_ и номером
сессии.
Например _SID_=20150313142600-d18ec118fff970ad4fb3628fbf530bc4
Для каждой сессии на сервере в директории /tmp/
(по умолчанию) создается
файл с таким же именем.
$ tree /tmp/
/tmp/
|-- 20150313094744-5d2e448000e6312d7c0b8a02ed954d22
`-- 20150313142600-d18ec118fff970ad4fb3628fbf530bc4
1 directory, 2 files
В этот файл записывается значение count
для нашей сессии. При каждом
обращении клиента SessionMiddleware
находит файл с таким же именем как у
cookie _SID_
десереализует объекты в нем и присваивает переменной окружения
paste.session.factory
. Таким образом мы можем хранить состояние сессии и
при каждом обновлении будет отдаваться значение, увеличенное на 1.
1 2 | from paste.gzipper import middleware as GzipMiddleware
app = GzipMiddleware(app) |
GzipMiddleware
сжимает ответ методом gzip
1 2 | from paste.pony import PonyMiddleware
app = PonyMiddleware(app) |
Это самое важное расширение в WSGI. Доступно по адресу http://localhost:8000/pony.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | #! /usr/bin/env nix-shell
#! nix-shell -i python3 -p python3 python3Packages.paste
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
"""
3.http.middleware.py
"""
from paste.evalexception.middleware import EvalException
from paste.gzipper import middleware as GzipMiddleware
from paste.pony import PonyMiddleware
from paste.session import SessionMiddleware
def app(environ, start_response):
# Except error
if 'error' in environ['PATH_INFO'].lower():
raise Exception('Detect "error" in URL path')
# Session
session = environ.get('paste.session.factory', lambda: {})()
if 'count' in session:
count = session['count']
else:
count = 1
session['count'] = count + 1
# Generate response
start_response('200 OK', [('Content-Type', 'text/plain')])
return [b'You have been here %d times!\n' % count, ]
app = EvalException(app) # go to http://localhost:8000/Errors
app = SessionMiddleware(app)
app = GzipMiddleware(app)
app = PonyMiddleware(app) # go to http://localhost:8000/pony
if __name__ == '__main__':
from paste import reloader
from paste.httpserver import serve
reloader.install()
serve(app, host='0.0.0.0', port=8000) |
1 2 3 4 5 6 7 8 9 10 11 12 | class GoogleRefMiddleware(object):
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
environ['google'] = False
if 'HTTP_REFERER' in environ:
if environ['HTTP_REFERER'].startswith('http://google.com'):
environ['google'] = True
return self.app(environ, start_response)
app = GoogleRefMiddleware(app) |
GoogleRefMiddleware
добавляет переменную окружения google
, и если бы мы
перешли на наш сайт из поиска google.com
, тo это значение было бы True
.
В этом разделе мы напишем еще один блог, используя популярные инструменты языка программирования Python.
Python Paste, или просто Paste — набор программ для веб-разработки. Включает в себя множество различных middleware, WSGI-сервер и другое. Был разработан Яном Бикингом, чтобы показать всю красоту спецификации WSGI, которая на тот момент была еще в черновиках. Проект больше академический и до недавнего времени не имел даже поддержки Python 3, но несмотря на это, многие современные фреймворки взяли за основу примеры из Paste (TurboGears, Zope, Pylons, Pyramid)
В нем есть готовая поддержка самых разных способов аутентификации (Basic, Digest, form, signed cookie, auth_tkt), поддержка корректной и удобной генерации ответов и заголовков (к примеру редиректы, Cache-control, Expires, gzipper и прочие). Различные базовые средства комбинации приложений (URLMap, Cascade, Recursive), статических данных (с учетом Etag, If-Modified итп).
Некоторые возможности paste
мы рассмотрели в разделе WSGI (pep-333).
Предупреждение
Примеры работают только в Python3
Встроенный WSGI сервер wsgiref
появился в Python начиная с версии
2.5,
частично вобрав наработки из модуля paste.httpserver
. На данный момент
целесообразно использовать встроенный в Python модуль wsgiref
или
сторонние более производительные реализации waitress и gunicorn.
1 2 3 4 5 6 7 8 9 10 11 12 | def blog(environ, start_response):
start_response(
'200 OK',
[('Content-Type', 'text/plain')]
)
return [b'Simple Blog', ]
if __name__ == '__main__':
from wsgiref.simple_server import make_server
httpd = make_server('0.0.0.0', 8000, blog)
httpd.serve_forever() |
В нашем случае подойдет любая реализация сервера отвечающего стандарту WSGI,
поэтому в примерах используется paste.httpserver
. С его помощью запустим
простое WSGI-приложение:
1 2 3 4 5 6 7 8 9 10 11 | def blog(environ, start_response):
start_response(
'200 OK',
[('Content-Type', 'text/plain')]
)
return [b'Simple Blog', ]
if __name__ == '__main__':
from paste.httpserver import serve
serve(blog, host='0.0.0.0', port=8000) |
Теперь приложение доступно по адресу http://localhost:8000/.
Примечание
Стоит отметить, что приложение будет доступно по любому пути этого адреса, например:
Доступ к WSGI приложению обычно осуществляется по конкретным URL адресам (ресурсам). В нашем примере приложение blog должно быть доступно только по корневому URL адресу, иные адреса должны выдавать страницу с ошибкой 404.
Для разделения путей напишем WSGI-middleware URLDispatch
.
1 2 3 4 5 6 7 8 9 10 11 12 13 | class URLDispatch(object):
def __init__(self, app_list):
self.app_list = app_list
def __call__(self, environ, start_response):
path_info = environ.get('PATH_INFO', '')
for prefix, app in self.app_list:
if path_info == prefix or path_info == prefix+'/':
return app(environ, start_response)
start_response('404 Not Found',
[('content-type', 'text/plain')])
return [b'not found'] |
Добавим настройки в наше приложение:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | from middlewares.urldispatch import URLDispatch
def blog(environ, start_response):
start_response(
'200 OK',
[('Content-Type', 'text/plain')]
)
return [b'Simple Blog', ]
# URL dispatching middleware
app_list = [
('/', blog),
]
dispatch = URLDispatch(app_list)
if __name__ == '__main__':
from paste.httpserver import serve
serve(dispatch, host='0.0.0.0', port=8000) |
Любой путь, отличающийся от корневого (http://localhost:8000/), по которому
доступно приложение blog
, будет инициализировать код ошибки 404.
Такой механизм в Веб-разработке называется URL маршрутизация или диспетчеризация, более подробно об этом будет говориться в разделе Маршруты.
Структура нашего блога будет состоять из следующих страниц:
Название | URL | Описание |
---|---|---|
Главная | / | Показывает все записи в блоге, отсортированные по дате |
(CREATE) Добавление | /article/add | Форма добавления новой статьи |
(READ) Просмотр | /article/{id} | Показывает конкретную статью, соответствующую {id} |
(UPDATE) Редактирование | /article/{id}/edit | Редактирование статьи по {id} |
(DELETE) Удаление | /article/{id}/delete | Удаление статьи по {id} |
По сути блог является стандартным CRUD (CREATE-READ-UPDATE-DELETE) интерфейсом, каждую часть которого будет реализовывать свое отдельное WSGI приложение, связанное со своим URL адресом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | from middlewares.urldispatch import URLDispatch
class BaseBlog(object):
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
class BlogIndex(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/plain')])
yield b'Simple Blog'
class BlogCreate(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/plain')])
yield b'Simple Blog -> CREATE'
class BlogRead(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/plain')])
yield b'Simple Blog -> READ'
class BlogUpdate(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/plain')])
yield b'Simple Blog -> UPDATE'
class BlogDelete(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/plain')])
yield b'Simple Blog -> DELETE'
# URL dispatching middleware
app_list = [
('/', BlogIndex),
('/article/add', BlogCreate),
('/article/{id}', BlogRead),
('/article/{id}/edit', BlogUpdate),
('/article/{id}/delete', BlogDelete),
]
dispatch = URLDispatch(app_list)
if __name__ == '__main__':
from paste.httpserver import serve
serve(dispatch, host='0.0.0.0', port=8000) |
Примечание
Обратите внимание, что адреса доступны по следующим ссылкам:
Если вместо {id}
подставить цифру, то вернется 404 ошибка.
Пока наша реализация роутов ничего не знает про символы типа «{id}», поэтому мы
не можем заменять их числом. Чтобы это исправить научим URLDispatch
middleware понимать регулярные выражения.
Название | URL | Описание |
---|---|---|
Главная | / | Показывает все записи в блоге, отсортированные по дате |
(CREATE) Добавление | /article/add | Форма добавления новой статьи |
(READ) Просмотр | ^/article/(?P<id>d+)/$ | Показывает конкретную статью, соответствующую {id} |
(UPDATE) Редактирование | ^/article/(?P<id>d+)/edit$ | Редактирование статьи по {id} |
(DELETE) Удаление | ^/article/(?P<id>d+)/delete$ | Удаление статьи по {id} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class RegexDispatch(object):
def __init__(self, app_list):
self.app_list = app_list
def __call__(self, environ, start_response):
path_info = environ.get('PATH_INFO', '')
for prefix, app in self.app_list:
if path_info == prefix or path_info == prefix+'/':
return app(environ, start_response)
match = re.match(prefix, path_info) or\
re.match(prefix, path_info+'/')
if match and match.groupdict():
environ['url_params'] = match.groupdict()
return app(environ, start_response)
start_response('404 Not Found', [('content-type', 'text/plain')])
return [b'not found'] |
Подменим символы «{id}» в адресах на регулярные выражения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | from middlewares.urldispatch import RegexDispatch
class BaseBlog(object):
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
class BlogIndex(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/plain')])
yield b'Simple Blog'
class BlogCreate(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/plain')])
yield b'Simple Blog -> CREATE'
class BlogRead(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/plain')])
yield b'Simple Blog -> READ'
class BlogUpdate(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/plain')])
yield b'Simple Blog -> UPDATE'
class BlogDelete(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/plain')])
yield b'Simple Blog -> DELETE'
# URL dispatching middleware
app_list = [
('/', BlogIndex),
('/article/add', BlogCreate),
(r'^/article/(?P<id>\d+)/$', BlogRead),
(r'^/article/(?P<id>\d+)/edit/$', BlogUpdate),
(r'^/article/(?P<id>\d+)/delete/$', BlogDelete),
]
dispatch = RegexDispatch(app_list)
if __name__ == '__main__':
from paste.httpserver import serve
serve(dispatch, host='0.0.0.0', port=8000) |
Примечание
Теперь можно переходить по URL’ам с числами вместо {id}, например:
Приложения обычно представляют данные, которые собирают из разных мест (БД, память, файлы, и т.д.), для блога такими данными как раз являются статьи и возможно комментарии к ним.
Оформим данные в виде списка, каждая запись которого будет являться статьей со
следующими ключами id
, title
, content
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | ARTICLES = [
{'id': 1, 'title': 'Lorem ipsum dolor sid amet!',
'content': '''Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Curabitur vel tortor eleifend, sollicitudin nisl quis, lacinia augue.
Duis quam est, laoreet sit amet justo vitae, viverra egestas sem.
Maecenas pellentesque augue in nibh feugiat tincidunt. Nunc magna ante,
mollis vitae ultricies eu, consectetur id ante. In ut libero eleifend,
blandit ipsum a, ullamcorper nunc. Sed bibendum eget odio eget
pellentesque. Curabitur elit felis, pellentesque id feugiat et, tincidunt
ut mauris. Integer vitae vehicula nunc. Integer ullamcorper, nunc in
volutpat auctor, elit leo convallis nulla, vitae varius mi nisl ac lorem.
Sed a lacus mi. In hac habitasse platea dictumst. Cras in posuere velit,
id dignissim nisl. Interdum et malesuada fames ac ante ipsum primis in
faucibus. Nulla bibendum suscipit convallis.'''},
{'id': 2, 'title': 'Hello', 'content': 'Test2'},
{'id': 3, 'title': 'World', 'content': 'Test2'}, ] |
Главная страница формируется перебором статей в списке:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class BlogIndex(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1>Simple Blog</h1>'
for article in ARTICLES:
yield str.encode(
'''
{0} - <a href="/article/{0}">{1}</a>
(<a href="/article/{0}/delete">delete</a>)<br/>
'''.format(
article['id'],
article['title']
)
) |
WSGI-приложения BlogRead
, BlogUpdate
и BlogDelete
теперь
наследуются от специально класса BaseArticle
, он берет id
статьи
(переменная окружения, которую добавляет middlwware RegexDispatch
) и
находит ее среди списка данных.
1 2 3 4 5 6 7 8 9 | class BaseArticle(BaseBlog):
def __init__(self, *args):
super(BaseArticle, self).__init__(*args)
article_id = self.environ['url_params']['id']
(self.index,
self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
if art['id'] == int(article_id)),
(None, None)) |
Приложение BlogRead
, отвечающее за чтение статьи, выводит его содержимое или
отдает 404 ошибку:
1 2 3 4 5 6 7 8 9 10 11 12 | class BlogRead(BaseArticle):
def __iter__(self):
if not self.article:
self.start('404 Not Found', [('content-type', 'text/plain')])
yield b'not found'
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> READ</h1>'
yield str.encode('<h2>{}</h2>'.format(self.article['title']))
yield str.encode(self.article['content']) |
Приложение, удаляющее статью — BlogDelete
, удаляет объект из списка данных и
возвращает статус ответа 302 Fount с заголовком Location: /, указывающий
браузеру, что нужно перейти на главную страницу (перенаправление).
1 2 3 4 5 6 7 8 | class BlogDelete(BaseArticle):
def __iter__(self):
self.start('302 Found', # '301 Moved Permanently',
[('Content-Type', 'text/html'),
('Location', '/')])
ARTICLES.pop(self.index)
yield b'' |
Полный код с изменениями:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | from middlewares.urldispatch import RegexDispatch
ARTICLES = [
{'id': 1, 'title': 'Lorem ipsum dolor sid amet!',
'content': '''Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Curabitur vel tortor eleifend, sollicitudin nisl quis, lacinia augue.
Duis quam est, laoreet sit amet justo vitae, viverra egestas sem.
Maecenas pellentesque augue in nibh feugiat tincidunt. Nunc magna ante,
mollis vitae ultricies eu, consectetur id ante. In ut libero eleifend,
blandit ipsum a, ullamcorper nunc. Sed bibendum eget odio eget
pellentesque. Curabitur elit felis, pellentesque id feugiat et, tincidunt
ut mauris. Integer vitae vehicula nunc. Integer ullamcorper, nunc in
volutpat auctor, elit leo convallis nulla, vitae varius mi nisl ac lorem.
Sed a lacus mi. In hac habitasse platea dictumst. Cras in posuere velit,
id dignissim nisl. Interdum et malesuada fames ac ante ipsum primis in
faucibus. Nulla bibendum suscipit convallis.'''},
{'id': 2, 'title': 'Hello', 'content': 'Test2'},
{'id': 3, 'title': 'World', 'content': 'Test2'}, ]
class BaseBlog(object):
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
class BaseArticle(BaseBlog):
def __init__(self, *args):
super(BaseArticle, self).__init__(*args)
article_id = self.environ['url_params']['id']
(self.index,
self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
if art['id'] == int(article_id)),
(None, None))
class BlogIndex(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1>Simple Blog</h1>'
for article in ARTICLES:
yield str.encode(
'''
{0} - <a href="/article/{0}">{1}</a>
(<a href="/article/{0}/delete">delete</a>)<br/>
'''.format(
article['id'],
article['title']
)
)
class BlogCreate(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/plain')])
yield b'Simple Blog -> CREATE'
class BlogRead(BaseArticle):
def __iter__(self):
if not self.article:
self.start('404 Not Found', [('content-type', 'text/plain')])
yield b'not found'
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> READ</h1>'
yield str.encode('<h2>{}</h2>'.format(self.article['title']))
yield str.encode(self.article['content'])
class BlogUpdate(BaseArticle):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/plain')])
yield b'Simple Blog -> UPDATE'
class BlogDelete(BaseArticle):
def __iter__(self):
self.start('302 Found', # '301 Moved Permanently',
[('Content-Type', 'text/html'),
('Location', '/')])
ARTICLES.pop(self.index)
yield b''
# URL dispatching middleware
app_list = [
('/', BlogIndex),
('/article/add', BlogCreate),
(r'^/article/(?P<id>\d+)/$', BlogRead),
(r'^/article/(?P<id>\d+)/edit/$', BlogUpdate),
(r'^/article/(?P<id>\d+)/delete/$', BlogDelete),
]
dispatch = RegexDispatch(app_list)
if __name__ == '__main__':
from paste.httpserver import serve
serve(dispatch, host='0.0.0.0', port=8000) |
Для создания статьи требуется HTML форма, где указываются заголовок и
содержание. В WSGI приложении BlogCreate
, запрос с методом GET
возвращает HTML форму, а POST записывает данные в список ARTICLES
,
после чего перенаправляет на главную страницу.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class BlogCreate(BaseBlog):
def __iter__(self):
if self.environ['REQUEST_METHOD'].upper() == 'POST':
from urllib.parse import parse_qs
values = parse_qs(self.environ['wsgi.input'].read())
max_id = max([art['id'] for art in ARTICLES])
ARTICLES.append(
{'id': max_id+1,
'title': values[b'title'].pop().decode(),
'content': values[b'content'].pop().decode()
}
)
self.start('302 Found',
[('Content-Type', 'text/html'),
('Location', '/')])
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> CREATE</h1>'
yield b'''
<form action="" method="POST">
Title:<br>
<input type="text" name="title"><br>
Content:<br>
<textarea name="content"></textarea><br><br>
<input type="submit" value="Submit">
</form>''' |
Обновление статей происходит схожим образом, за исключением того, что в форму
подставляются уже существующие значения и вместо добавления нового объекта в
список ARTICLES
, обновляется уже существующий.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class BlogUpdate(BaseArticle):
def __iter__(self):
if self.environ['REQUEST_METHOD'].upper() == 'POST':
from urllib.parse import parse_qs
values = parse_qs(self.environ['wsgi.input'].read())
self.article['title'] = values[b'title'].pop().decode()
self.article['content'] = values[b'content'].pop().decode()
self.start('302 Found',
[('Content-Type', 'text/html'),
('Location', '/')])
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> UPDATE</h1>'
yield str.encode(
'''
<form action="" method="POST">
Title:<br>
<input type="text" name="title" value="{0}"><br>
Content:<br>
<textarea name="content">{1}</textarea><br><br>
<input type="submit" value="Submit">
</form>
'''.format(
self.article['title'],
self.article['content']
)
) |
Полный код с изменениями:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | # third-party
from middlewares.urldispatch import RegexDispatch
ARTICLES = [
{'id': 1, 'title': 'Lorem ipsum dolor sid amet!',
'content': '''Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Curabitur vel tortor eleifend, sollicitudin nisl quis, lacinia augue.
Duis quam est, laoreet sit amet justo vitae, viverra egestas sem.
Maecenas pellentesque augue in nibh feugiat tincidunt. Nunc magna ante,
mollis vitae ultricies eu, consectetur id ante. In ut libero eleifend,
blandit ipsum a, ullamcorper nunc. Sed bibendum eget odio eget
pellentesque. Curabitur elit felis, pellentesque id feugiat et, tincidunt
ut mauris. Integer vitae vehicula nunc. Integer ullamcorper, nunc in
volutpat auctor, elit leo convallis nulla, vitae varius mi nisl ac lorem.
Sed a lacus mi. In hac habitasse platea dictumst. Cras in posuere velit,
id dignissim nisl. Interdum et malesuada fames ac ante ipsum primis in
faucibus. Nulla bibendum suscipit convallis.'''},
{'id': 2, 'title': 'Hello', 'content': 'Test2'},
{'id': 3, 'title': 'World', 'content': 'Test2'}, ]
class BaseBlog(object):
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
class BaseArticle(BaseBlog):
def __init__(self, *args):
super(BaseArticle, self).__init__(*args)
article_id = self.environ['url_params']['id']
(self.index,
self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
if art['id'] == int(article_id)),
(None, None))
class BlogIndex(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1>Simple Blog</h1>'
yield b'<a href="/article/add">Add article</a>'
yield b'<br />'
yield b'<br />'
for article in ARTICLES:
yield str.encode(
'''
{0} - (<a href="/article/{0}/delete">delete</a> |
<a href="/article/{0}/edit">edit</a>)
<a href="/article/{0}">{1}</a><br />
'''.format(
article['id'],
article['title']
)
)
class BlogCreate(BaseBlog):
def __iter__(self):
if self.environ['REQUEST_METHOD'].upper() == 'POST':
from urllib.parse import parse_qs
values = parse_qs(self.environ['wsgi.input'].read())
max_id = max([art['id'] for art in ARTICLES])
ARTICLES.append(
{'id': max_id+1,
'title': values[b'title'].pop().decode(),
'content': values[b'content'].pop().decode()
}
)
self.start('302 Found',
[('Content-Type', 'text/html'),
('Location', '/')])
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> CREATE</h1>'
yield b'''
<form action="" method="POST">
Title:<br>
<input type="text" name="title"><br>
Content:<br>
<textarea name="content"></textarea><br><br>
<input type="submit" value="Submit">
</form>'''
class BlogRead(BaseArticle):
def __iter__(self):
if not self.article:
self.start('404 Not Found', [('content-type', 'text/plain')])
yield b'not found'
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> READ</h1>'
yield str.encode('<h2>{}</h2>'.format(self.article['title']))
yield str.encode(self.article['content'])
class BlogUpdate(BaseArticle):
def __iter__(self):
if self.environ['REQUEST_METHOD'].upper() == 'POST':
from urllib.parse import parse_qs
values = parse_qs(self.environ['wsgi.input'].read())
self.article['title'] = values[b'title'].pop().decode()
self.article['content'] = values[b'content'].pop().decode()
self.start('302 Found',
[('Content-Type', 'text/html'),
('Location', '/')])
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> UPDATE</h1>'
yield str.encode(
'''
<form action="" method="POST">
Title:<br>
<input type="text" name="title" value="{0}"><br>
Content:<br>
<textarea name="content">{1}</textarea><br><br>
<input type="submit" value="Submit">
</form>
'''.format(
self.article['title'],
self.article['content']
)
)
class BlogDelete(BaseArticle):
def __iter__(self):
self.start('302 Found', # '301 Moved Permanently',
[('Content-Type', 'text/html'),
('Location', '/')])
ARTICLES.pop(self.index)
yield b''
# URL dispatching middleware
app_list = [
('/', BlogIndex),
('/article/add', BlogCreate),
(r'^/article/(?P<id>\d+)/$', BlogRead),
(r'^/article/(?P<id>\d+)/edit/$', BlogUpdate),
(r'^/article/(?P<id>\d+)/delete/$', BlogDelete),
]
dispatch = RegexDispatch(app_list)
if __name__ == '__main__':
from paste.httpserver import serve
serve(dispatch, host='0.0.0.0', port=8000) |
Авторизация поможет защитить ресурсы от сторонних пользователей, в первую
очередь это касается операций, которые изменяют данные (BlogCreate
,
BlogUpdate
, BlogDelete
). В эти WSGI приложения необходимо будет
добавить проверку пользователя.
В нашем примере используется алгоритм BasicAuth
и WSGI-middleware
middlewares.basicauth.BasicAuth
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class BasicAuth(object):
def __init__(self, app, users, realm='www'):
self.app = app
self.users = users
self.realm = realm
def __call__(self, environ, start_response):
auth = environ.get('HTTP_AUTHORIZATION')
if not auth:
return self.auth_required(environ, start_response)
auth_type, enc_auth_info = auth.split(None, 1)
assert auth_type.lower() == 'basic'
auth_info = base64.b64decode(enc_auth_info)
username, password = auth_info.decode().split(':', 1)
if self.users.get(username) != password:
return self.auth_required(environ, start_response)
environ['REMOTE_USER'] = username
return self.app(environ, start_response)
def auth_required(self, environ, start_response):
status = '401 Authorization Required'
headers = [
('content-type', 'text/plain'),
('WWW-Authenticate', 'Basic realm="%s"' % self.realm)
]
start_response(status, headers)
return [b'authentication required'] |
Полный код с изменениями:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | from middlewares.urldispatch import RegexDispatch
from middlewares.basicauth import BasicAuth
ARTICLES = [
{'id': 1, 'title': 'Lorem ipsum dolor sid amet!',
'content': '''Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Curabitur vel tortor eleifend, sollicitudin nisl quis, lacinia augue.
Duis quam est, laoreet sit amet justo vitae, viverra egestas sem.
Maecenas pellentesque augue in nibh feugiat tincidunt. Nunc magna ante,
mollis vitae ultricies eu, consectetur id ante. In ut libero eleifend,
blandit ipsum a, ullamcorper nunc. Sed bibendum eget odio eget
pellentesque. Curabitur elit felis, pellentesque id feugiat et, tincidunt
ut mauris. Integer vitae vehicula nunc. Integer ullamcorper, nunc in
volutpat auctor, elit leo convallis nulla, vitae varius mi nisl ac lorem.
Sed a lacus mi. In hac habitasse platea dictumst. Cras in posuere velit,
id dignissim nisl. Interdum et malesuada fames ac ante ipsum primis in
faucibus. Nulla bibendum suscipit convallis.'''},
{'id': 2, 'title': 'Hello', 'content': 'Test2'},
{'id': 3, 'title': 'World', 'content': 'Test2'}, ]
class BaseBlog(object):
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
class BaseArticle(BaseBlog):
def __init__(self, *args):
super(BaseArticle, self).__init__(*args)
article_id = self.environ['url_params']['id']
(self.index,
self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
if art['id'] == int(article_id)),
(None, None))
class BlogIndex(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1>Simple Blog</h1>'
yield b'<a href="/article/add">Add article</a>'
yield b'<br />'
yield b'<br />'
for article in ARTICLES:
yield str.encode(
'''
{0} - (<a href="/article/{0}/delete">delete</a> |
<a href="/article/{0}/edit">edit</a>)
<a href="/article/{0}">{1}</a><br />
'''.format(
article['id'],
article['title']
)
)
class BlogCreate(BaseBlog):
def __iter__(self):
if self.environ['REQUEST_METHOD'].upper() == 'POST':
from urllib.parse import parse_qs
values = parse_qs(self.environ['wsgi.input'].read())
max_id = max([art['id'] for art in ARTICLES])
ARTICLES.append(
{'id': max_id+1,
'title': values[b'title'].pop().decode(),
'content': values[b'content'].pop().decode()
}
)
self.start('302 Found',
[('Content-Type', 'text/html'),
('Location', '/')])
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> CREATE</h1>'
yield b'''
<form action="" method="POST">
Title:<br>
<input type="text" name="title"><br>
Content:<br>
<textarea name="content"></textarea><br><br>
<input type="submit" value="Submit">
</form>
'''
class BlogRead(BaseArticle):
def __iter__(self):
if not self.article:
self.start('404 Not Found', [('content-type', 'text/plain')])
yield b'not found'
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> READ</h1>'
yield str.encode('<h2>{}</h2>'.format(self.article['title']))
yield str.encode(self.article['content'])
class BlogUpdate(BaseArticle):
def __iter__(self):
if self.environ['REQUEST_METHOD'].upper() == 'POST':
from urllib.parse import parse_qs
values = parse_qs(self.environ['wsgi.input'].read())
self.article['title'] = values[b'title'].pop().decode()
self.article['content'] = values[b'content'].pop().decode()
self.start('302 Found',
[('Content-Type', 'text/html'),
('Location', '/')])
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> UPDATE</h1>'
yield str.encode(
'''
<form action="" method="POST">
Title:<br>
<input type="text" name="title" value="{0}"><br>
Content:<br>
<textarea name="content">{1}</textarea><br><br>
<input type="submit" value="Submit">
</form>
'''.format(
self.article['title'],
self.article['content']
)
)
class BlogDelete(BaseArticle):
def __iter__(self):
self.start('302 Found', # '301 Moved Permanently',
[('Content-Type', 'text/html'),
('Location', '/')])
ARTICLES.pop(self.index)
yield b''
# BasicAuth applications
passwd = {'admin': '123'}
create = BasicAuth(BlogCreate, passwd)
update = BasicAuth(BlogUpdate, passwd)
delete = BasicAuth(BlogDelete, passwd)
# URL dispatching middleware
app_list = [
('/', BlogIndex),
('/article/add', create),
(r'^/article/(?P<id>\d+)/$', BlogRead),
(r'^/article/(?P<id>\d+)/edit/$', update),
(r'^/article/(?P<id>\d+)/delete/$', delete),
]
dispatch = RegexDispatch(app_list)
if __name__ == '__main__':
from paste.httpserver import serve
serve(dispatch, host='0.0.0.0', port=8000) |
В программном обеспечении принято разделять: программную логику, код, который относится к данным, настройки и шаблоны. Таким образом, код становится более структурированным, и его легче развивать, а также сопровождать в дальнейшем.
MVC (Model-View-Controller: модель-вид-контроллер) — шаблон архитектуры ПО, который подразумевает разделение программы на 3 слабосвязанных компонента, каждый из которых отвечает за свою сферу деятельности.
Бешеная популярность данной структуры в Веб-приложениях сложилась благодаря её включению в две среды разработки, которые стали очень востребованными: Struts и Ruby on Rails. Эти среды разработки наметили пути развития для сотен рабочих сред, созданных позже.
Классические MVC фреймворки:
Фреймворк Django ввел новую терминологию MTV.
В Django функции, отвечающие за обработку логики, соответствуют части Controller из MVC, но называются View, а отображение соответствует части View из MVC, но называется Template. Получилось, что:
Так появилась аббревиатура MTV.
TADA!! Django invented MTV
Вся логика при таком подходе вынесена во View, а то, как будут отображаться данные в Template. Из-за ограничений HTTP протокола, View в Django описывает, какие данные будут представленны по запросу на определенный URL. View, как и протокол HTTP, не хранит состояний и по факту является обычной функцией обратного вызова, которая запускается вновь при каждом запросе по URL. Шаблоны (Templates), в свою очередь, описывают, как данные представить пользователю.
MTV фреймворки:
В защиту своего дизайна авторы Pyramid написали довольно большой документ, который призван развеять мифы о фреймворке. Например, на критику модели MVC в Pyramid следует подробное объяснение, что MVC «притянут за уши» к веб-приложениям. Следующая цитата хорошо характеризует подход к терминологии в Pyramid:
«Мы считаем, что есть только две вещи: ресурсы (Resource) и виды (View). Дерево ресурсов представляет структуру сайта, а вид представляет ресурс.
«Шаблоны» (Template) в реальности лишь деталь реализации некоторого вида: строго говоря, они не обязательны, и вид может вернуть ответ (Response) и без них.
Нет никакого «контроллера» (Controller): его просто не существует.
«Модель» (Model) же либо представлена деревом ресурсов, либо «доменной моделью» (domain model) (например, моделью SQLAlchemy), которая вообще не является частью каркаса.
Нам кажется, что наша терминология более разумна при существующих ограничениях веб-технологий.»
Веб ограничен URL, который и представляет из себя дерево ресурсов или структуру сайта.
Также протокол HTTP не позволяет хранить состояние и отправлять/принимать оповещения клиенту от сервера, что ограничивает возможность отслеживания действий клиента для последующего уведомления модели на изменение состояния.
Поэтому данные часто используются на «frontend»-е (например в связке React/Redux), а на стороне сервера формируются только один раз во время ответа, либо загружаются отдельным запросом при помощи AJAX, или даже с помощью других протоколов, например WebSocket.
RV фреймворки:
Приведем структуру нашего блога к следующему виду:
.
├── __init__.py
├── models.py
└── views.py
0 directories, 3 files
Примечание
Исходный код доступен по адресу:
Где:
__init__.py
- входная точка программы, которая содержит основные
настройки и запуск Веб-сервераmodels.py
- код, который представляет данные, обычно называется моделиviews.py
- логика программы (в нашем случае WSGI-приложения)Предупреждение
Примеры работают только в Python3
models.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | ARTICLES = [
{
'id': 1,
'title': 'Lorem ipsum dolor sid amet!',
'content': '''
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Curabitur vel tortor eleifend, sollicitudin nisl quis, lacinia augue.
Duis quam est, laoreet sit amet justo vitae, viverra egestas sem.
Maecenas pellentesque augue in nibh feugiat tincidunt. Nunc magna ante,
mollis vitae ultricies eu, consectetur id ante. In ut libero eleifend,
blandit ipsum a, ullamcorper nunc. Sed bibendum eget odio eget
pellentesque. Curabitur elit felis, pellentesque id feugiat et, tincidunt
ut mauris. Integer vitae vehicula nunc. Integer ullamcorper, nunc in
volutpat auctor, elit leo convallis nulla, vitae varius mi nisl ac lorem.
Sed a lacus mi. In hac habitasse platea dictumst. Cras in posuere velit,
id dignissim nisl. Interdum et malesuada fames ac ante ipsum primis in
faucibus. Nulla bibendum suscipit convallis.
'''
},
{
'id': 2,
'title': 'Hello',
'content': 'Test2'
},
{
'id': 3,
'title': 'World',
'content': 'Test2'
},
] |
Мы использовали самописные WSGI-middleware, которые решают стандартные задачи. Заменим их на уже существующие:
Настройки авторизации __init__.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | from views import BlogRead, BlogIndex, BlogCreate, BlogDelete, BlogUpdate
from wsgi_basic_auth import BasicAuth
# third-party
import selector
def make_wsgi_app():
passwd = {
'admin': '123'
}
# BasicAuth applications
create = BasicAuth(BlogCreate, 'www', passwd)
update = BasicAuth(BlogUpdate, 'www', passwd)
delete = BasicAuth(BlogDelete, 'www', passwd)
# URL dispatching middleware
dispatch = selector.Selector()
dispatch.add('/', GET=BlogIndex)
dispatch.prefix = '/article'
dispatch.add('/add', GET=create, POST=create)
dispatch.add('/{id:digits}', GET=BlogRead)
dispatch.add('/{id:digits}/edit', GET=update, POST=update)
dispatch.add('/{id:digits}/delete', GET=delete)
return dispatch
if __name__ == '__main__':
from paste.httpserver import serve
app = make_wsgi_app()
serve(app, host='0.0.0.0', port=8000) |
Настройки URL-диспетчеризации __init__.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | from views import BlogRead, BlogIndex, BlogCreate, BlogDelete, BlogUpdate
from wsgi_basic_auth import BasicAuth
# third-party
import selector
def make_wsgi_app():
passwd = {
'admin': '123'
}
# BasicAuth applications
create = BasicAuth(BlogCreate, 'www', passwd)
update = BasicAuth(BlogUpdate, 'www', passwd)
delete = BasicAuth(BlogDelete, 'www', passwd)
# URL dispatching middleware
dispatch = selector.Selector()
dispatch.add('/', GET=BlogIndex)
dispatch.prefix = '/article'
dispatch.add('/add', GET=create, POST=create)
dispatch.add('/{id:digits}', GET=BlogRead)
dispatch.add('/{id:digits}/edit', GET=update, POST=update)
dispatch.add('/{id:digits}/delete', GET=delete)
return dispatch
if __name__ == '__main__':
from paste.httpserver import serve
app = make_wsgi_app()
serve(app, host='0.0.0.0', port=8000) |
WSGI-приложение можно указывать как объект (BlogRead
) или как строку
импорта ("views.BlogIndex"
).
views.py
:
1 2 3 4 5 6 7 8 9 | class BaseArticle(BaseBlog):
def __init__(self, *args):
super(BaseArticle, self).__init__(*args)
article_id = self.environ['wsgiorg.routing_args'][1]['id']
(self.index,
self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
if art['id'] == int(article_id)),
(None, None)) |
urlrelay добавляет результат поиска в переменную с названием wsgiorg.routing_args
.
Практически не изменились.
views.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | from models import ARTICLES
class BaseBlog(object):
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
class BaseArticle(BaseBlog):
def __init__(self, *args):
super(BaseArticle, self).__init__(*args)
article_id = self.environ['wsgiorg.routing_args'][1]['id']
(self.index,
self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
if art['id'] == int(article_id)),
(None, None))
class BlogIndex(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1>Simple Blog</h1>'
yield b'<a href="/article/add">Add article</a>'
yield b'<br />'
yield b'<br />'
for article in ARTICLES:
yield str.encode(
'''
{0} - (<a href="/article/{0}/delete">delete</a> |
<a href="/article/{0}/edit">edit</a>)
<a href="/article/{0}">{1}</a><br />
'''.format(
article['id'],
article['title']
)
)
class BlogCreate(BaseBlog):
def __iter__(self):
if self.environ['REQUEST_METHOD'].upper() == 'POST':
from urllib.parse import parse_qs
values = parse_qs(self.environ['wsgi.input'].read())
max_id = max([art['id'] for art in ARTICLES])
ARTICLES.append(
{'id': max_id+1,
'title': values[b'title'].pop().decode(),
'content': values[b'content'].pop().decode()
}
)
self.start('302 Found',
[('Content-Type', 'text/html'),
('Location', '/')])
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> CREATE</h1>'
yield b'''
<form action="" method="POST">
Title:<br>
<input type="text" name="title"><br>
Content:<br>
<textarea name="content"></textarea><br><br>
<input type="submit" value="Submit">
</form>
'''
class BlogRead(BaseArticle):
def __iter__(self):
if not self.article:
self.start('404 Not Found', [('content-type', 'text/plain')])
yield b'not found'
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> READ</h1>'
yield str.encode(
'<h2>{}</h2>'.format(self.article['title'])
)
yield str.encode(self.article['content'])
class BlogUpdate(BaseArticle):
def __iter__(self):
if self.environ['REQUEST_METHOD'].upper() == 'POST':
from urllib.parse import parse_qs
values = parse_qs(self.environ['wsgi.input'].read())
self.article['title'] = values[b'title'].pop().decode()
self.article['content'] = values[b'content'].pop().decode()
self.start('302 Found',
[('Content-Type', 'text/html'),
('Location', '/')])
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> UPDATE</h1>'
yield str.encode(
'''
<form action="" method="POST">
Title:<br>
<input type="text" name="title" value="{0}"><br>
Content:<br>
<textarea name="content">{1}</textarea><br><br>
<input type="submit" value="Submit">
</form>
'''.format(
self.article['title'],
self.article['content']
)
)
class BlogDelete(BaseArticle):
def __iter__(self):
self.start('302 Found', # '301 Moved Permanently',
[('Content-Type', 'text/html'),
('Location', '/')])
ARTICLES.pop(self.index)
yield b'' |
См.также
Идея маршрута (англ. - route) впервые появилась в Ruby on Rails и быстро обрела популярность в других веб-фреймворках. Также концептуально очень близкой системой является URLConf в Django. В последующих разработках наиболее мощной реализацией данной идеи, вероятно, является http://routes.groovie.org/, используемая в Pylons.
Маршруты отвечают за ключевую проблему веб-разработки: сопоставление кода и
URL. Например, какой код должен отвечать за обработку запросов по адресу
«/2008/01/08» или «/login»? Во многих фреймворках используется
фиксированная система диспетчеризации, например «/A/B/C» означает прочитать
файл «C» в каталоге «B» (например /auth/login.php
или
/cgi-bin/hello.cgi
), или вызвать метод «С» класса «B» в модуле «A».
Это работает прекрасно до тех пор, пока не возникает необходимости в реорганизации кода, и выясняется, что закладки пользователей стали недействительны. Кроме того, если вы хотите переделать адреса (например, создать раздел в подразделе), то нужно изменить уже отлаженную логику по генерации ссылок внутри сайта.
Маршруты предлагают иной подход. Вы определяете шаблоны URL и связываете их со своим кодом. Если вы измените свое решение по поводу конкретного URL, то просто поменяйте шаблон URL - код по-прежнему будет работать отлично, и не понадобится менять какую-либо логику.
Регулярные выражения дают огромные возможности для обработки URL-путей, но из-за ограничений, описанных в стандарте RFC 1738, большинство из них не нужны, при этом использование регулярных выражений затрудняет читабельность кода. Более современный подход придуманный Ruby on Rails это использовать технологию сопоставление с образом. Рассмотрим отличия на примере нашего блога:
Примечание
Исходный код доступен по адресу:
Предупреждение
Примеры работают только в Python3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | from views import BlogRead, BlogIndex, BlogCreate, BlogDelete, BlogUpdate
from wsgi_basic_auth import BasicAuth
# third-party
import selector
def make_wsgi_app():
passwd = {
'admin': '123'
}
# BasicAuth applications
create = BasicAuth(BlogCreate, 'www', passwd)
update = BasicAuth(BlogUpdate, 'www', passwd)
delete = BasicAuth(BlogDelete, 'www', passwd)
# URL dispatching middleware
dispatch = selector.Selector()
dispatch.add('/', GET=BlogIndex)
dispatch.prefix = '/article'
dispatch.add('/add', GET=create, POST=create)
dispatch.add('/{id:digits}', GET=BlogRead)
dispatch.add('/{id:digits}/edit', GET=update, POST=update)
dispatch.add('/{id:digits}/delete', GET=delete)
return dispatch
if __name__ == '__main__':
from paste.httpserver import serve
app = make_wsgi_app()
serve(app, host='0.0.0.0', port=8000) |
Регулярные выражения | Сопоставление с образом |
---|---|
/ | / |
/article/add | /article/add |
^/article/(?P<id>d+)/$ | /article/{id:digits} |
^/article/(?P<id>d+)/edit$ | /article/{id:digits}/edit |
^/article/(?P<id>d+)/delete$ | /article/{id:digits}/delete |
Шаблонизатор (в web) — это программное обеспечение, позволяющее использовать html-шаблоны для генерации конечных html-страниц.
Основная цель использования шаблонизаторов — это отделение представления данных от исполняемого кода. Часто это необходимо для обеспечения возможности параллельной работы программиста и дизайнера-верстальщика. Такой подход значительно ускоряет время разработки и прототипирования приложения, дизайнеру не нужно вникать в программирование, а программисту беспокоиться об интерфейсе.
Использование шаблонизаторов улучшает читаемость кода и упрощает внесение изменений во внешний вид, когда проект целиком выполняет один человек.
Свою популярность шаблоны обрели с приходом фреймворка Ruby On Rails и стали популярны не только в Вебе, современные десктопные приложения тоже идут по пути отделения логики программы от интерфейса, например библиотека Electron позволяет создавать GUI приложения с интерфейсом, написанном на HTML + JavaScript и логикой на NodeJS, по сути встраивая движок Chromium в ваш исполняемый файл.
Другим примером является фреймворк Qt, в котором интерфейс может быть
написан на Qml + JavaScript и запускаться независимо от основного
приложения при помощи утилиты qmlscene
. Компания Microsoft также
продвигает эту идею в .Net, предоставляя технологию WPF. Как мы
видим, некоторые принципы, ранее встречающиеся преимущественно в Вебе,
перенимаются другими областями программирования. Тем самым с развитием
Интернет Веб-технологии будут все больше влиять на программирование в целом.
Шаблоны имеют очень простое определение — в статические файлы вставляются куски кода, а при прогоне таких файлов через специальный транслятор (препроцессор), код заменяется результатом его выполнения. Например, при компиляции шаблоны в C++ заменяются определенными значениями:
template< typename T >
T min( T a, T b )
{
return a < b ? a : b;
}
Перед компиляцией этот шаблон может принять такой вид:
int min( int a, int b )
{
return a < b ? a : b;
}
long min( long a, long b )
{
return a < b ? a : b;
}
Если Ruby развивался как полноценный, интерпретируемый язык общего назначения, который потом обрел фреймворк Ruby On Rails и наконец систему шаблонов, то PHP изначально был языком шаблонов, т.е. препроцессором, через который можно прогнать любой файл (например, HTML со вставками PHP) и получить результат.
Простой пример на PHP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <html>
<head>
<title>
Тестируем PHP
</title>
</head>
<body>
<?php
echo '<h1>Hello, world!</h1>';
?>
<br />
<?php
$colors = array("red", "green", "blue", "yellow");
foreach ($colors as $value) {
echo "* $value <br />\n";
}
?>
</body>
</html> |
Результат выполнения программы:
<html>
<head>
<title>
Тестируем PHP
</title>
</head>
<body>
<h1>Hello, world!</h1>
<br />
* red <br />
* green <br />
* blue <br />
* yellow <br />
</body>
</html>
Jinja2 — самый популярный шаблонизатор в языке программирования Python. Автор Armin Ronacher из команды http://www.pocoo.org/ не раз приезжал на конференции в Екатеринбург с докладами о своих продуктах.
Синтаксис Jinja2 сильно похож на Django-шаблонизатор, но при этом дает возможность использовать чистые Python выражения и поддерживает гибкую систему расширений.
1 2 3 4 5 | # -*- coding: utf-8 -*-
from jinja2 import Template
template = Template('Hello {{ name }}!')
print(template.render(name=u'Вася')) |
Hello Вася!
{# Это кусок кода, который стал временно не нужен, но удалять жалко
{% for user in users %}
...
{% endfor %}
#}
1 2 3 4 5 | # -*- coding: utf-8 -*-
from jinja2 import Template
text = '{% for item in range(5) %}Hello {{ name }}! {% endfor %}'
template = Template(text)
print(template.render(name=u'Вася')) |
Hello Вася! Hello Вася! Hello Вася! Hello Вася! Hello Вася!
1 2 3 4 5 6 7 | # -*- coding: utf-8 -*-
from jinja2 import Template
template = Template("{% set a, b, c = 'foo', 'фуу', 'föö' %}")
m = template.module
print(m.a)
print(m.b)
print(m.c) |
1 2 3 4 5 6 7 | # -*- coding: utf-8 -*-
from jinja2 import Template
template = Template('{% macro foo() %}42{% endmacro %}23')
m = template.module
print(m)
print(m.foo()) |
1 2 3 4 5 6 7 8 9 10 11 | <!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
{% for item in range(5) %}
Hello {{ name }}!
{% endfor %}
</body>
</html> |
jinja2/3.loader/0.file.py
— чтение шаблона из файла¶1 2 3 4 5 6 | # -*- coding: utf-8 -*-
from jinja2 import Template
html = open('foopkg/templates/0.hello.html').read()
template = Template(html)
print(template.render(name=u'Петя')) |
jinja2/3.loader/foopkg/templates/0.hello.html
¶<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
Hello Петя!
Hello Петя!
Hello Петя!
Hello Петя!
Hello Петя!
</body>
</html>
См.также
1 2 3 4 5 6 | # -*- coding: utf-8 -*-
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('foopkg/templates'))
template = env.get_template('0.hello.html')
print(template.render(name=u'Петя')) |
Результат рендеринга шаблона jinja2/3.loader/foopkg/templates/0.hello.html
1 2 3 4 5 6 7 | # -*- coding: utf-8 -*-
from jinja2 import Environment, PackageLoader
env = Environment(loader=PackageLoader('foopkg', 'templates'))
template = env.get_template('0.hello.html')
print(template.render(name=u'Петя')) |
Результат рендеринга шаблона jinja2/3.loader/foopkg/templates/0.hello.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # -*- coding: utf-8 -*-
from jinja2 import Environment, DictLoader
html = '''<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
{% for item in range(5) %}
Hello {{ name }}!
{% endfor %}
</body>
</html>
'''
env = Environment(loader=DictLoader({'index.html': html}))
template = env.get_template('index.html')
print(template.render(name=u'Петя')) |
Результат рендеринга шаблона jinja2/3.loader/foopkg/templates/0.hello.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # -*- coding: utf-8 -*-
from jinja2 import Environment, FunctionLoader
html = '''<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
{% for item in range(5) %}
Hello {{ name }}!
{% endfor %}
</body>
</html>
'''
def myloader(name):
if name == 'index.html':
return html
env = Environment(loader=FunctionLoader(myloader))
template = env.get_template('index.html')
print(template.render(name=u'Петя')) |
Результат рендеринга шаблона jinja2/3.loader/foopkg/templates/0.hello.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | # -*- coding: utf-8 -*-
from jinja2 import Environment, FunctionLoader, PackageLoader, PrefixLoader
html = '''<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
{% for item in range(5) %}
Hello {{ name }}!
{% endfor %}
</body>
</html>
'''
def myloader(name):
if name == 'index.html':
return html
env = Environment(loader=PrefixLoader({
'foo': FunctionLoader(myloader),
'bar': PackageLoader('foopkg', 'templates')
}))
template1 = env.get_template('foo/index.html')
template2 = env.get_template('bar/0.hello.html')
print(template1.render(name=u'Петя'))
print(template2.render(name=u'Петя')) |
Результат рендеринга шаблона jinja2/3.loader/foopkg/templates/0.hello.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | # -*- coding: utf-8 -*-
from jinja2 import Environment, FunctionLoader, PackageLoader, ChoiceLoader
html = '''<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
{% for item in range(5) %}
Hello {{ name }}!
{% endfor %}
</body>
</html>
'''
def myloader(name):
if name == 'index.html':
return html
env = Environment(loader=ChoiceLoader({
FunctionLoader(myloader),
PackageLoader('foopkg', 'templates')
}))
template1 = env.get_template('index.html')
template2 = env.get_template('0.hello.html')
print(template1.render(name=u'Петя'))
print(template2.render(name=u'Петя')) |
Результат рендеринга шаблона jinja2/3.loader/foopkg/templates/0.hello.html
1 2 3 4 5 6 7 8 9 10 11 12 | # -*- coding: utf-8 -*-
from jinja2 import Environment, FileSystemLoader, ModuleLoader
# Compile template
Environment(loader=FileSystemLoader('foopkg/templates'))\
.compile_templates("foopkg/compiled/foopkg.zip",
py_compile=True) # pyc generate, only for python2
# Environment
env = Environment(loader=ModuleLoader("foopkg/compiled/foopkg.zip"))
template = env.get_template('0.hello.html')
print(template.render(name=u'Петя')) |
Результат рендеринга шаблона jinja2/3.loader/foopkg/templates/0.hello.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # -*- coding: utf-8 -*-
from os.path import exists, getmtime, join
from jinja2 import BaseLoader, Environment, TemplateNotFound
class FoopkgLoader(BaseLoader):
def __init__(self, path="foopkg/templates"):
self.path = path
def get_source(self, environment, template):
path = join(self.path, template)
if not exists(path):
raise TemplateNotFound(template)
mtime = getmtime(path)
with open(path) as f:
source = f.read()
return source, path, lambda: mtime == getmtime(path)
# Environment
env = Environment(loader=FoopkgLoader())
template = env.get_template('0.hello.html')
print(template.render(name=u'Петя')) |
Результат рендеринга шаблона jinja2/3.loader/foopkg/templates/0.hello.html
1 2 3 4 5 6 7 8 9 10 11 12 13 | server {
listen {{ SRC_SERVER_PUB_IP }}:80;
servern_name {{ FQDN }} www.{{ FQDN }}
location / {
proxy_pass http://{{ SRC_SERVER_LOCAL_IP }}:80/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #!/usr/bin/env python
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('.'))
template = env.get_template('nginx_proxy_conf.tpl')
data = {
"SRC_SERVER_PUB_IP": "192.168.0.100",
"SRC_SERVER_LOCAL_IP": "10.0.3.100",
"FQDN": "example.com"
}
conf = template.render(**data)
print(conf)
open("proxy.nginx.conf", "w").write(conf) |
1 2 3 4 5 6 7 8 9 10 11 12 13 | server {
listen 192.168.0.100:80;
servern_name example.com www.example.com
location / {
proxy_pass http://10.0.3.100:80/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
<link rel="stylesheet" href="style.css" />
<title>{% block title %}{% endblock %} - My Webpage</title>
<meta charset='utf-8'>
{% endblock %}
</head>
<body>
<div id="content">{% block content %}{% endblock %}</div>
<div id="footer">
{% block footer %}
© Copyright 2008 by <a href="http://domain.invalid/">you</a>.
{% endblock %}
</div>
</body>
</html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | {% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ super() }}
<style type="text/css">
.important { color: #336699; }
</style>
{% endblock %}
{% block content %}
<h1>Index</h1>
<p class="important">
Welcome {{ name }} to my awesome homepage.
</p>
{% endblock %} |
1 2 3 4 5 6 | # -*- coding: utf-8 -*-
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('.'))
template = env.get_template('index.html')
print(template.render(name=u'Петя')) |
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="style.css" />
<title>Index — My Webpage</title>
<meta charset='utf-8'>
<style type="text/css">
.important { color: #336699; }
</style>
</head>
<body>
<div id="content">
<h1>Index</h1>
<p class="important">
Welcome Петя to my awesome homepage.
</p>
</div>
<div id="footer">
© Copyright 2008 by <a href="http://domain.invalid/">you</a>.
</div>
</body>
</html>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
<link rel="stylesheet" href="style.css" />
<title>{% block title %}{% endblock %} - My Blog</title>
<meta charset='utf-8'>
{% endblock %}
</head>
<body>
<div id="content">{% block content %}{% endblock %}</div>
<br />
<br />
<br />
<div id="footer">
{% block footer %}
© Copyright 2015 by <a href="http://domain.invalid/">you</a>.
{% endblock %}
</div>
</body>
</html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | {% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block content %}
<h1>Simple Blog</h1>
<a href="/article/add">Add article</a>
<br />
<br />
{% for article in articles %}
{{ article.id }} - (<a href="/article/{{ article.id }}/delete">delete</a> |
<a href="/article/{{ article.id }}/edit">edit</a>)
<a href="/article/{{ article.id }}">{{ article.title }}</a><br />
{% endfor %}
{% endblock %} |
1 2 3 4 5 6 7 8 9 10 11 12 13 | {% extends "base.html" %}
{% block title %}Create{% endblock %}
{% block content %}
<h1>Simple Blog -> EDIT</h1>
<form action="" method="POST">
Title:<br>
<input type="text" name="title" value="{{ article.title }}"><br>
Content:<br>
<textarea name="content">{{ article.content }}</textarea><br><br>
<input type="submit" value="Submit">
</form>
{% endblock %} |
1 2 3 4 5 6 7 8 | {% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block content %}
<h1><a href="/">Simple Blog</a> -> READ</h1>
<h2>{{ article.title }}</h2>
{{ article.content }}
{% endblock %} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | from models import ARTICLES
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('templates'))
class BaseBlog(object):
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
class BaseArticle(BaseBlog):
def __init__(self, *args):
super(BaseArticle, self).__init__(*args)
article_id = self.environ['wsgiorg.routing_args'][1]['id']
(self.index,
self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
if art['id'] == int(article_id)),
(None, None))
class BlogIndex(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/html')])
yield str.encode(
env.get_template('index.html').render(articles=ARTICLES)
)
class BlogCreate(BaseBlog):
def __iter__(self):
if self.environ['REQUEST_METHOD'].upper() == 'POST':
from urllib.parse import parse_qs
values = parse_qs(self.environ['wsgi.input'].read())
max_id = max([art['id'] for art in ARTICLES])
ARTICLES.append(
{'id': max_id+1,
'title': values[b'title'].pop().decode(),
'content': values[b'content'].pop().decode()
}
)
self.start('302 Found',
[('Content-Type', 'text/html'),
('Location', '/')])
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield str.encode(
env.get_template('create.html').render(article=None)
)
class BlogRead(BaseArticle):
def __iter__(self):
if not self.article:
self.start('404 Not Found', [('content-type', 'text/plain')])
yield b'not found'
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield str.encode(
env.get_template('read.html').render(article=self.article)
)
class BlogUpdate(BaseArticle):
def __iter__(self):
if self.environ['REQUEST_METHOD'].upper() == 'POST':
from urllib.parse import parse_qs
values = parse_qs(self.environ['wsgi.input'].read())
self.article['title'] = values[b'title'].pop().decode()
self.article['content'] = values[b'content'].pop().decode()
self.start('302 Found',
[('Content-Type', 'text/html'),
('Location', '/')])
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield str.encode(
env.get_template('create.html').render(article=self.article)
)
class BlogDelete(BaseArticle):
def __iter__(self):
self.start('302 Found', # '301 Moved Permanently',
[('Content-Type', 'text/html'),
('Location', '/')])
ARTICLES.pop(self.index)
yield b'' |
Mako — это стандартный шаблонизатор для фреймворка Pylons, написанный Майком Байером (автор SQLAlchemy). Используется на таких сайтах как https://python.org и http://reddit.com. Преимуществом является высокая скорость работы.
1 2 3 4 5 | # -*- coding: utf-8 -*-
from mako.template import Template
template = Template('Hello ${ name }!')
print(template.render(name=u'Вася')) |
Hello Вася!
## Однострочный коммент
<%doc> Это кусок кода, который стал временно не нужен, но удалять жалко
% for user in users:
...
% endfor
</%doc>
1 2 3 4 5 6 7 8 9 10 11 12 13 | server {
listen {{ SRC_SERVER_PUB_IP }}:80;
servern_name {{ FQDN }} www.{{ FQDN }}
location / {
proxy_pass http://{{ SRC_SERVER_LOCAL_IP }}:80/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
} |
1 2 3 4 5 6 7 8 9 | #! /usr/bin/env bash
SRC_SERVER_PUB_IP=192.168.0.100
SRC_SERVER_LOCAL_IP=127.0.0.1
FQDN=example.com
sed -e "s/{{ SRC_SERVER_PUB_IP }}/${SRC_SERVER_PUB_IP}/"\
-e "s/{{ SRC_SERVER_LOCAL_IP }}/${SRC_SERVER_LOCAL_IP}/"\
-e "s/{{ FQDN }}/${FQDN}/g" < 0.nginx_proxy_conf.tpl > proxy.nginx.conf |
bash/proxy.nginx.conf
- результат рендеринга шаблона¶1 2 3 4 5 6 7 8 9 10 11 12 13 | server {
listen 192.168.0.100:80;
servern_name example.com www.example.com
location / {
proxy_pass http://127.0.0.1:80/;
proxy_redirect off;
proxy_set_header Host ;
proxy_set_header X-Real-IP ;
proxy_set_header X-Forwarded-For ;
}
} |
1 2 3 4 5 6 7 8 9 10 11 12 13 | server {
listen ${SRC_SERVER_PUB_IP}:80;
servern_name ${FQDN} www.${FQDN}
location / {
proxy_pass http://${SRC_SERVER_LOCAL_IP}:80/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
} |
1 2 3 4 5 6 7 8 9 10 11 12 13 | #! /usr/bin/env bash
# render a template configuration file
# expand variables + preserve formatting
render_template() {
eval "echo \"$(cat $1)\""
}
SRC_SERVER_PUB_IP=192.168.0.100
SRC_SERVER_LOCAL_IP=127.0.0.1
FQDN=example.com
render_template 1.nginx_proxy_conf.tpl > proxy.nginx.conf |
При загрузке HTML страницы браузер ищет все недостающие медиа-файлы (css, js,
swf, png, svg, gif, jpg, avi, mp3, mp4, …) и подгружает их
отдельными HTTP запросами (см. itcase
).
На картинке выше видно, что FireFox нашел 14 дополнительных ресурсов для этого сайта. Браузер автоматически установит 14 TCP соединений, по одному на каждый ресурс, и попытается получить данные, отправив HTTP запросы. Т.к. установка соединения и сетевые задержки — очень дорогие операции, лучшей практикой является объединение ресурсов в один файл, например можно объединить все JavaScript файлы в один при помощи RequireJS, то же можно проделать и для CSS файлов или для картинок (спрайты), помимо прочего Webpack вообще позволяет запаковывать JavaScript файлы совместно с CSS.
Примечание
Стоит отметить, что протокол HTTP2 лишен этого недостатка, так как загружает ресурсы асинхронно в рамках одного соединения.
Отдачу статических файлов можно настроить через отдельный сервер статики, например Nginx, или реализовать средствами самого Веб-приложения (гораздо медленнее).
Для примера возьмем следующую структуру файлов:
/usr/share/nginx/html/
|-- index.html
|-- index2.html
`-- static_example
`-- static
|-- html-css-js.png
|-- jquery.min.js
|-- script.js
`-- style.css
2 directories, 6 files
index2.html
страница, которая ссылается на другие статические файлы.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | <!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Untitled Document</title>
<link href="/static/style.css" media="all" rel="stylesheet" type="text/css" />
<script src="/static/jquery.min.js"></script>
<script src="/static/script.js"></script>
</head>
<body>
<table width="100%" border="1" cellspacing="10" cellpadding="10">
<tr>
<td width="60%">
<h1>HTML</h1>
<a href="">HTML</a>
<a href="">JS</a>
<a href="" style="color: green">CSS</a>
<hr/>
<p class="replace-text">HTML (от англ. HyperText Markup Language — «язык
гипертекстовой разметки») — стандартный язык разметки документов во
Всемирной паутине. Большинство веб-страниц содержат описание разметки на
языке HTML (или XHTML). Язык HTML интерпретируется браузерами и
отображается в виде документа в удобной для человека форме.</p> <p
style="color: black; background-color: red; color: #fff"> Язык HTML
является приложением («частным случаем») SGML (стандартного обобщённого
языка разметки) и соответствует международному стандарту ISO 8879.</p>
<p class="hide"> Язык XHTML является более строгим вариантом HTML, он
следует всем ограничениям XML и, фактически, XHTML можно воспринимать
как приложение языка XML к области разметки гипертекста.</p> <p> Во
всемирной паутине HTML-страницы, как правило, передаются браузерам от
сервера по протоколам HTTP или HTTPS, в виде простого текста или с
использованием сжатия.</p>
<hr/>
</td>
<td width="40%"><img src="/static/html-css-js.png" class="jquery-image"
width="500" height="293" alt="HTML JS CSS"></td> </tr>
</table>
</body>
</html> |
Сервер Nginx настроен таким образом, что по адресу /example
отдается
страница index2.html
, а по /static
файлы из директории
/usr/share/nginx/html/static_example/static
.
1 2 3 4 5 6 7 8 9 10 11 | # default.nginx
server {
listen 80 default_server;
root /usr/share/nginx/html;
index index.html index.htm;
include includes/fcgi.nginx;
include includes/static.nginx;
} |
1 2 3 4 5 6 7 | location /example {
try_files ../$uri ../$uri/ /index2.html;
}
location /static {
alias /usr/share/nginx/html/static_example/static;
} |
Если скопировать файлы статики в директорию
/usr/share/nginx/html/static_example/static
, то сервер начнет их отдавать:
Приведем предыдущий пример к следующей структуре файлов:
.
├── app.py
├── index.html
└── static
├── html-css-js.png
├── jquery.min.js
├── script.js
└── style.css
1 directory, 6 files
Для отдачи статики используется WSGI-приложение StaticURLParser
из модуля paste.
1 2 3 4 5 6 7 | from paste.urlparser import StaticURLParser
from paste import httpserver
static_app = StaticURLParser(".")
if __name__ == '__main__':
httpserver.serve(static_app, host='0.0.0.0', port='8000') |
По адресу http://localhost:8000 будет открыта страница index.html (действие по умолчанию). Остальные файлы доступны по адресам:
В нашем примере блога определенно не хватает стилей и динамики. Чтобы это исправить, добавим файлы css и js в директорию static, как показано ниже:
.
├── __init__.py
├── models.py
├── requirements.txt
├── static
│ ├── main.css
│ └── main.js
├── templates
│ ├── base.html
│ ├── create.html
│ ├── index.html
│ └── read.html
└── views.py
2 directories, 12 files
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | <!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
<title>{% block title %}{% endblock %} - My Blog</title>
<meta charset='utf-8'>
{% endblock %}
{% block css %}
<link href="/static/main.css" media="all" rel="stylesheet" type="text/css" />
{% endblock %}
</head>
<body>
<div class="wrapper">
<div class="blog">
{% block content %}
{% endblock %}
</div>
<div class="footer">
{% block footer %}
© Copyright 2015 by <a href="http://domain.invalid/">you</a>.
{% endblock %}
</div>
</div>
{% block js %}
<script src="/static/main.js" type="text/javascript"></script>
{% endblock %}
</body>
</html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | .wrapper
{
width: 50%;
max-width: 600px;
margin: 0 auto;
padding: 100px 0 0 0;
min-width: 550px;
}
.blog
{
position: relative;
}
.blog__title
{
font: 1.8em Arial;
}
.blog__title-link
{
color: blue;
}
.blog__title-link:hover
{
color: red;
}
.blog__title-text
{
color: #000;
position: relative;
display: inline-block;
padding: 0 0 0 0.9em;
}
.blog__title-text:after
{
display: block;
content: ">";
position: absolute;
left: 0;
top: 0;
}
.blog__button
{
font: 1.1em Arial;
display: inline-block;
background: green;
color: #fff;
padding: 0.3em 0.5em;
text-decoration: none;
position: absolute;
right: 3.5%;
top: 0;
}
.blog__button:hover
{
background: darkgreen;
}
.blog-list
{
padding: 1em 0 0 0;
border-top: solid 1px #ccc;
margin: 1em 0 0 0;
}
.blog-list__item
{
padding: 0.5em 1em;
}
.blog-list__item:hover
{
background: #efefef;
}
.blog-list__item-id
{
width: 5%;
font: 0.9em Arial;
display: inline-block;
}
.blog-list__item-link
{
width: 65%;
font: 0.9em Arial;
display: inline-block;
color: blue;
}
.blog-list__item-link:hover
{
color: red;
}
.blog-list__item-action
{
width: 28%;
display: inline-block;
text-align: right;
}
.blog-list__item-edit
{
font: 0.9em Arial;
display: inline-block;
background: blue;
color: #fff;
padding: 0.3em 0.6em;
text-decoration: none;
}
.blog-list__item-edit:hover
{
background: darkblue;
}
.blog-list__item-delete
{
font: 0.9em Arial;
display: inline-block;
background: red;
color: #fff;
padding: 0.3em 0.6em;
text-decoration: none;
}
.blog-list__item-delete:hover
{
background: darkred;
}
.blog-item
{
padding: 1em 0 0 0;
border-top: solid 1px #ccc;
margin: 1em 0 0 0;
}
.blog-item__title
{
font: 1.8em Arial;
color: #000;
padding: 0 0 0.5em 0;
}
.blog-item__text
{
font: 0.9em Arial;
color: #000;
}
.blog-form
{
padding: 1em 0 0 0;
border-top: solid 1px #ccc;
margin: 1em 0 0 0;
}
.blog-form-field
{
padding: 0 0 1em 0;
}
.blog-form-field__title
{
font: 0.9em Arial;
color: #000;
padding: 0 0 0.5em;
}
.blog-form-field__input
{
width: 100%;
border: solid 1px #ccc;
font: 0.9em Arial;
padding: 0.3em 0.5em;
box-sizing: border-box;
}
.blog-form-field__textarea
{
width: 100%;
border: solid 1px #ccc;
font: 0.9em Arial;
padding: 0.3em 0.5em;
min-height: 200px;
box-sizing: border-box;
}
.blog-form__button
{
font: 1.2em Arial;
display: inline-block;
background: blue;
color: #fff;
padding: 0.3em 0.6em 0.25em 0.6em;
text-decoration: none;
border: solid 0px red;
cursor: pointer;
}
.blog-form__button:hover
{
background: darkblue;
}
.footer
{
padding: 3em 0 0 1em;
font: 0.8em Arial;
} |
Функция confirm_delete()
выводит окно подтверждения при нажатии на кнопку
удаления статьи.
1 2 3 | function confirm_delete() {
return confirm("Are you sure to delete entry?");
} |
Добавим классы в остальных шаблонах, чтобы наши стили применились.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | {% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block content %}
<div class="blog__title">Simple Blog</div>
<a href="/article/add" class="blog__button">add article</a>
<div class="blog-list">
{% for article in articles %}
<div class="blog-list__item">
<div class="blog-list__item-id">{{ article.id }}</div>
<a href="/article/{{ article.id }}" class="blog-list__item-link">{{ article.title }}</a>
<div class="blog-list__item-action">
<a href="/article/{{ article.id }}/edit" class="blog-list__item-edit">edit</a>
<a href="/article/{{ article.id }}/delete" onclick="return confirm_delete();"
class="blog-list__item-delete">delete</a>
</div>
</div>
{% endfor %}
</div>
{% endblock %} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | {% extends "base.html" %}
{% block title %}{{ article.title }}{% endblock %}
{% block content %}
<div class="blog__title">
<a href="/" class="blog__title-link">Simple Blog</a>
<span class="blog__title-text">{{ article.title }}</span>
</div>
<div class="blog-item">
<div class="blog-item__title">{{ article.title }}</div>
<div class="blog-item__text">{{ article.content }}</div>
</div>
{% endblock %} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | {% extends "base.html" %}
{% block title %}Create{% endblock %}
{% block content %}
<div class="blog__title">
<a href="/" class="blog__title-link">Simple Blog</a>
<span class="blog__title-text">Edit</span>
</div>
<form action="" method="POST" class="blog-form">
<div class="blog-form-field">
<div class="blog-form-field__title">Title:</div>
<input type="text" class="blog-form-field__input" name="title" value="{{ article.title }}"><br>
</div>
<div class="blog-form-field">
<div class="blog-form-field__title">Content:</div>
<textarea class="blog-form-field__textarea" name="content">{{ article.content }}</textarea><br><br>
</div>
<input class="blog-form__button" type="submit" value="Submit">
</form>
{% endblock %} |
В результате получим новое отображение блога, но пока мы не укажем откуда брать статику, он ее не сможет найти.
Новые шаблоны блога без статики
Статику будет отдавать WSGI-приложение StaticURLParser
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | from paste.auth.basic import AuthBasicHandler
import selector
from views import BlogCreate, BlogDelete, BlogIndex, BlogRead, BlogUpdate
def authfunc(environ, username, password):
return username == 'admin' and password == '123'
def make_wsgi_app():
# BasicAuth applications
create = AuthBasicHandler(BlogCreate, 'www', authfunc)
update = AuthBasicHandler(BlogUpdate, 'www', authfunc)
delete = AuthBasicHandler(BlogDelete, 'www', authfunc)
# URL dispatching middleware
dispatch = selector.Selector()
dispatch.add('/', GET=BlogIndex)
dispatch.prefix = '/article'
dispatch.add('/add', GET=create, POST=create)
dispatch.add('/{id:digits}', GET=BlogRead)
dispatch.add('/{id:digits}/edit', GET=update, POST=update)
dispatch.add('/{id:digits}/delete', GET=delete)
# Static files
from paste.urlparser import StaticURLParser
static_app = StaticURLParser("static/")
from paste import urlmap
mapping = urlmap.URLMap()
mapping['/static'] = static_app
from paste.cascade import Cascade
app = Cascade([mapping, dispatch])
return app
if __name__ == '__main__':
from paste.httpserver import serve
app = make_wsgi_app()
serve(app, host='0.0.0.0', port=8000) |
При помощи paste.urlmap.URLMap
добавляется префикс /static/ для наших
файлов из директории static. paste.cascade.Cascade
позволяет запускать
несколько WSGI-приложений одновременно. Таким образом наш блог принял
следующую архитектуру:
И стал по-другому выглядеть:
Новые шаблоны блога со статикой
Окно подтверждения при удалении статьи
Для реальных проектов лучше использовать библиотеку Whitenoise, она поддерживает Python3 и CDN.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | from views import BlogRead, BlogIndex, BlogCreate, BlogDelete, BlogUpdate
from wsgi_basic_auth import BasicAuth
# third-party
import selector
def make_wsgi_app():
passwd = {
'admin': '123'
}
# BasicAuth applications
create = BasicAuth(BlogCreate, 'www', passwd)
update = BasicAuth(BlogUpdate, 'www', passwd)
delete = BasicAuth(BlogDelete, 'www', passwd)
# URL dispatching middleware
dispatch = selector.Selector()
dispatch.add('/', GET=BlogIndex)
dispatch.prefix = '/article'
dispatch.add('/add', GET=create, POST=create)
dispatch.add('/{id:digits}', GET=BlogRead)
dispatch.add('/{id:digits}/edit', GET=update, POST=update)
dispatch.add('/{id:digits}/delete', GET=delete)
return dispatch
if __name__ == '__main__':
app = make_wsgi_app()
from whitenoise import WhiteNoise
app = WhiteNoise(app)
app.add_files('./static/', prefix='static/')
from paste.httpserver import serve
serve(app, host='0.0.0.0', port=8000) |
См.также
В Интернете под пагинацией понимают показ ограниченной части информации на одной веб-странице (например, 10 результатов поиска или 20 форумных трэдов). Она повсеместно используется в веб-приложениях для разбиения большого массива данных на страницы и включает в себя навигационный блок для перехода на другие страницы.
См.также
Модуль paginate
делит список статей на страницы. Номер страницы передается
методом GET, в параметре page. По умолчанию берется первая страница.
p = paginate.Page(
items,
page=1,
items_per_page=42
)
Пример Mako шаблона, использующего Bootstrap4 для пагинации.
<%inherit file="base.mako"/>
<%block name="content">
<h2>${tag.title()}</h2>
<br/>
<div class="row">
%for item in p:
<div class="col">
<div class="row">
<a href="${_static_prefix}/item/${item.id}.html"> 🔗 </a>
</div>
<br/>
<div class="row">
<pre id='id-${item.id}' width=100%>
${item.text}
</pre>
</div>
</div>
%endfor
</div>
# https://v4-alpha.getbootstrap.com/components/pagination/
<div class="row">
<nav aria-label="Page navigation example">
<ul class="pagination">
${p.pager(
url="../$page/index.html".format(tag),
link_attr={'class': 'page_link'},
link_tag=lambda page: '<li class="page-item {} {}"><a class="page-link" href="{}">{}</a></li>'.format(
'active' if page['type'] == 'current_page' else '',
'disabled' if not len(page['href'].strip()) else '',
page['href'],
page['value']
)
)}
</ul>
</nav>
</div>
</%block>
Bootstrap4 pagination
См.также
Для начала наполним блог случайными статьями при помощи функции
generate_lorem_ipsum
из пакета jinja2.utils
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from jinja2.utils import generate_lorem_ipsum
ARTICLES = []
for id, article in enumerate(range(100), start=1):
title = generate_lorem_ipsum(
n=1, # Одно предложение
html=False, # В виде обычного текста
min=2, # Минимум 2 слова
max=5 # Максимум 5
)
content = generate_lorem_ipsum()
ARTICLES.append(
{'id': id, 'title': title, 'content': content}
) |
Много статей не помещаются на экран
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class BlogIndex(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/html')])
# Get page number
from urllib.parse import parse_qs
values = parse_qs(self.environ['QUERY_STRING'])
# Wrap articles to paginated list
from paginate import Page
page = values.get('page', ['1', ]).pop()
paged_articles = Page(
ARTICLES,
page=page,
items_per_page=8,
)
yield str.encode(
env.get_template('index.html').render(
articles=paged_articles
)
) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | {% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block content %}
<div class="blog__title">Simple Blog</div>
<a href="/article/add" class="blog__button">add article</a>
<div class="blog-list">
{% for article in articles %}
<div class="blog-list__item">
<div class="blog-list__item-id">{{ article.id }}</div>
<a href="/article/{{ article.id }}" class="blog-list__item-link">{{ article.title }}</a>
<div class="blog-list__item-action">
<a href="/article/{{ article.id }}/edit" class="blog-list__item-edit">edit</a>
<a href="/article/{{ article.id }}/delete" onclick="return confirm_delete();"
class="blog-list__item-delete">delete</a>
</div>
</div>
{% endfor %}
</div>
<div class="paginator">
{{ articles.pager(url="?page=$page") }}
</div>
{% endblock %} |
В результате на каждой странице отображаются только 8 статей.
Блог со страницами
См.также
WebOb — это библиотека, сериализующая HTTP запрос (текст) в объект и позволяющая генерировать HTTP ответы. В частности работает с окружение WSGI.
Изначально библиотеку написал Ян Бикинг, затем разработкой занимался Сергей Щетинин, а теперь она перешла в руки Pylons Foundation.
Вместо странных конструкций вида:
from urlparse import parse_qs
values = parse_qs(environ['QUERY_STRING'])
page = values.get('page', ['1', ]).pop()
Мы можем использовать:
from webob import Request
req = Request(environ)
page = req.params.get('page', '1')
Класс Request
оборачивает окружение, пришедшее от
Веб-сервера, в случае HTTP-запроса.
Мы можем сами создать окружение для класса Request
и
получить объект запроса, как в примере ниже.
1 2 3 4 5 6 7 8 9 10 11 12 13 | environ = {
'HTTP_HOST': 'localhost:80',
'PATH_INFO': '/article',
'QUERY_STRING': 'id=1',
'REQUEST_METHOD': 'GET',
'SCRIPT_NAME': ''
}
from webob import Request
req = Request(environ)
from pprint import pprint
pprint(req.environ) |
1 2 3 4 5 | {'HTTP_HOST': 'localhost:80',
'PATH_INFO': '/article',
'QUERY_STRING': 'id=1',
'REQUEST_METHOD': 'GET',
'SCRIPT_NAME': ''} |
Request
имеет конструктор, который создает минимальное
окружение запроса. При помощи метода blank
можно имитировать HTTP запрос:
1 2 3 4 5 | from webob import Request
req = Request.blank('/blog?page=4')
from pprint import pprint
pprint(req.environ) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | {'HTTP_HOST': 'localhost:80',
'PATH_INFO': '/blog',
'QUERY_STRING': 'page=4',
'REQUEST_METHOD': 'GET',
'SCRIPT_NAME': '',
'SERVER_NAME': 'localhost',
'SERVER_PORT': '80',
'SERVER_PROTOCOL': 'HTTP/1.0',
'wsgi.errors': <open file '<stderr>', mode 'w' at 0x7f4ff5d111e0>,
'wsgi.input': <_io.BytesIO object at 0x7f4ff3b622f0>,
'wsgi.multiprocess': False,
'wsgi.multithread': False,
'wsgi.run_once': False,
'wsgi.url_scheme': 'http',
'wsgi.version': (1, 0)} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from webob import Request
req = Request.blank('/blog?page=4')
print(req.method)
print(req.scheme)
print(req.path_info)
print(req.host)
print(req.host_url)
print(req.application_url)
print(req.path_url)
print(req.url)
print(req.path)
print(req.path_qs)
print(req.query_string) |
1 2 3 4 5 6 7 8 9 10 11 | GET
http
/blog
localhost:80
http://localhost
http://localhost
http://localhost/blog
http://localhost/blog?page=4
/blog
/blog?page=4
page=4 |
1 2 3 4 5 6 7 | from webob import Request
req = Request.blank('/test?check=a&check=b&name=Bob')
print(req.GET)
print(req.GET['check'])
print(req.GET.getall('check'))
print(list(req.GET.items())) |
1 2 3 4 | GET([('check', 'a'), ('check', 'b'), ('name', 'Bob')])
b
['a', 'b']
[('check', 'a'), ('check', 'b'), ('name', 'Bob')] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from webob import Request
req = Request.blank('/test')
print(req.POST) # empty
print(list(req.POST.items()))
print()
# Set POST
req.method = 'POST'
req.body = b'name=Vasya&email=vasya@example.com'
print(req.POST) # not empty
print(req.POST['name'])
print(req.POST['email']) |
1 2 3 4 5 6 | <NoVars: Not a form request>
[]
MultiDict([('name', 'Vasya'), ('email', 'vasya@example.com')])
Vasya
vasya@example.com |
Если вы не уверенны, каким методом были отправлены данные, можно воспользоваться
атрибутом params
.
1 2 3 4 5 6 7 8 9 10 11 | from webob import Request
req = Request.blank('/test?check=a&check=b&name=Bob')
# Set POST
req.method = 'POST'
req.body = b'name=Vasya&email=vasya@example.com'
print(req.params)
print(req.params.getall('check'))
print(req.params['email'])
print(req.params['name']) |
1 2 3 4 | NestedMultiDict([('check', 'a'), ('check', 'b'), ('name', 'Bob'), ('name', 'Vasya'), ('email', 'vasya@example.com')])
['a', 'b']
vasya@example.com
Bob |
1 2 3 4 5 6 7 8 | from webob import Request
req = Request.blank('/test')
# Set Cookie
req.headers['Cookie'] = 'session_id=9999999;foo=abcdef;bar=2'
print(req.cookies)
print(req.cookies['foo']) |
1 2 | <RequestCookies (dict-like) with values {'bar': '2', 'foo': 'abcdef', 'session_id': '9999999'}>
abcdef |
webob.request.Request
умеет запускать WSGI-приложения. Это может
понадобиться, например, при написании тестов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | from webob import Request
def wsgi_app(environ, start_response):
request = Request(environ)
if request.path == '/test':
start_response('200 OK', [('Content-type', 'text/plain')])
return ['Hi!']
start_response('404 Not Found', [('Content-type', 'text/plain')])
req = Request.blank('/test')
status, headers, app_iter = req.call_application(wsgi_app)
print(status)
print(headers)
print(app_iter)
print
req = Request.blank('/bar')
status, headers, app_iter = req.call_application(wsgi_app)
print(status)
print(headers)
print(app_iter) |
1 2 3 4 5 6 7 | 200 OK
[('Content-type', 'text/plain')]
['Hi!']
404 Not Found
[('Content-type', 'text/plain')]
None |
Класс, который содержит все необходимое для создания ответа WSGI-приложения.
Конструктор класса Response
имеет минимальный набор
для HTTP ответа:
>>> from webob import Response
>>> res = Response()
>>> res.status
'200 OK'
>>> res.headerlist
[('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '0')]
>>> res.body
''
В процессе выполнения программы ответ можно переопределить:
>>> res.status = 404
>>> res.status
'404 Not Found'
>>> res.status_code
404
>>> res.headerlist = [('Content-type', 'text/html')]
>>> res.body = b'test'
>>> print res
404 Not Found
Content-type: text/html
Content-Length: 4
test
>>> res.body = u"test"
Traceback (most recent call last):
...
TypeError: You cannot set Response.body to a unicode object (use Response.text)
>>> res.text = u"test"
Traceback (most recent call last):
...
AttributeError: You cannot access Response.text unless charset is set
>>> res.charset = 'utf8'
>>> res.text = u"test"
>>> res.body
'test'
Также можно задать значения передав их в конструктор, например
Response(charset='utf8')
.
>>> from webob import Response
>>> resp = Response(body=b'Hello World!')
>>> resp.content_type
'text/html'
>>> resp.content_type = 'text/plain'
>>> print resp
200 OK
Content-Length: 12
Content-Type: text/plain; charset=UTF-8
Hello World!
get_response
генерирует HTTP ответ.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | from webob import Request, Response
def wsgi_app(environ, start_response):
response = Response()
response.content_type = 'text/plain'
parts = []
for name, value in sorted(environ.items()):
parts.append('%s: %r' % (name, value))
response.body = str.encode(
'\n'.join(parts)
)
return response(environ, start_response)
req = Request.blank('/test')
print(req.call_application(wsgi_app)) # WSGI-application response
print()
print(req.get_response(wsgi_app)) # HTTP response |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | ('200 OK', [('Content-Type', 'text/plain; charset=UTF-8'), ('Content-Length', '411')], [b"HTTP_HOST: 'localhost:80'\nPATH_INFO: '/test'\nQUERY_STRING: ''\nREQUEST_METHOD: 'GET'\nSCRIPT_NAME: ''\nSERVER_NAME: 'localhost'\nSERVER_PORT: '80'\nSERVER_PROTOCOL: 'HTTP/1.0'\nwsgi.errors: <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>\nwsgi.input: <_io.BytesIO object at 0x7f692e219048>\nwsgi.multiprocess: False\nwsgi.multithread: False\nwsgi.run_once: False\nwsgi.url_scheme: 'http'\nwsgi.version: (1, 0)"])
200 OK
Content-Type: text/plain; charset=UTF-8
Content-Length: 411
HTTP_HOST: 'localhost:80'
PATH_INFO: '/test'
QUERY_STRING: ''
REQUEST_METHOD: 'GET'
SCRIPT_NAME: ''
SERVER_NAME: 'localhost'
SERVER_PORT: '80'
SERVER_PROTOCOL: 'HTTP/1.0'
wsgi.errors: <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>
wsgi.input: <_io.BytesIO object at 0x7f692e219048>
wsgi.multiprocess: False
wsgi.multithread: False
wsgi.run_once: False
wsgi.url_scheme: 'http'
wsgi.version: (1, 0) |
>>> from webob.exc import *
>>> exc = HTTPTemporaryRedirect(location='foo')
>>> req = Request.blank('/path/to/something')
>>> print str(req.get_response(exc)).strip()
307 Temporary Redirect
Location: http://localhost/path/to/foo
Content-Length: 126
Content-Type: text/plain; charset=UTF-8
307 Temporary Redirect
The resource has been moved to http://localhost/path/to/foo; you should be redirected automatically.
Добавим декоратор wsgify
, который будет делать для каждого «вида» всю
WSGI-магию и добавлять объект request
.
1 2 3 4 5 6 7 8 | def wsgify(view):
from webob import Request
def wrapped(environ, start_response):
request = Request(environ)
app = view(request).response()
return app(environ, start_response)
return wrapped |
В самих представлениях request
передается как параметр конструктора, а
ответ реализуется в виде метода класса response
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @wsgify
class BlogIndex(object):
def __init__(self, request):
self.page = request.GET.get('page', '1')
from paginate import Page
self.paged_articles = Page(
ARTICLES,
page=self.page,
items_per_page=8,
)
def response(self):
from webob import Response
return Response(env.get_template('index.html')
.render(articles=self.paged_articles)
.encode('utf-8')) |
1 2 3 | @wsgify
class BlogIndex(object):
... |
Метод response
должен возвращать WSGI-приложение. В нашем случае это объект
класса Response
из библиотеки webob
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @wsgify
class BlogIndex(object):
def __init__(self, request):
self.page = request.GET.get('page', '1')
from paginate import Page
self.paged_articles = Page(
ARTICLES,
page=self.page,
items_per_page=8,
)
def response(self):
from webob import Response
return Response(env.get_template('index.html')
.render(articles=self.paged_articles)
.encode('utf-8')) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @wsgify
class BlogCreate(object):
def __init__(self, request):
self.request = request
def response(self):
from webob import Response
if self.request.method == 'POST':
max_id = max([art['id'] for art in ARTICLES])
ARTICLES.append(
{'id': max_id+1,
'title': self.request.POST['title'],
'content': self.request.POST['content']
}
)
return Response(status=302, location='/')
return Response(env.get_template('create.html').render(article=None)) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @wsgify
class BlogCreate(object):
def __init__(self, request):
self.request = request
def response(self):
from webob import Response
if self.request.method == 'POST':
max_id = max([art['id'] for art in ARTICLES])
ARTICLES.append(
{'id': max_id+1,
'title': self.request.POST['title'],
'content': self.request.POST['content']
}
)
return Response(status=302, location='/')
return Response(env.get_template('create.html').render(article=None)) |
1 2 3 | @wsgify
class BlogCreate(object):
... |
1 2 3 4 5 6 7 8 9 | class BaseArticle(object):
def __init__(self, request):
self.request = request
article_id = self.request.environ['wsgiorg.routing_args'][1]['id']
(self.index,
self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
if art['id'] == int(article_id)),
(None, None)) |
1 2 3 4 5 6 7 8 9 10 11 12 | class BlogRead(BaseArticle):
def __iter__(self):
if not self.article:
self.start('404 Not Found', [('content-type', 'text/plain')])
yield b'not found'
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield str.encode(
env.get_template('read.html').render(article=self.article)
) |
1 2 3 4 5 6 7 8 9 | @wsgify
class BlogRead(BaseArticle):
def response(self):
from webob import Response
if not self.article:
return Response(status=404)
return Response(env.get_template('read.html')
.render(article=self.article)) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class BlogUpdate(BaseArticle):
def __iter__(self):
if self.environ['REQUEST_METHOD'].upper() == 'POST':
from urllib.parse import parse_qs
values = parse_qs(self.environ['wsgi.input'].read())
self.article['title'] = values[b'title'].pop().decode()
self.article['content'] = values[b'content'].pop().decode()
self.start('302 Found',
[('Content-Type', 'text/html'),
('Location', '/')])
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield str.encode(
env.get_template('create.html').render(article=self.article)
) |
1 2 3 4 5 6 7 8 9 10 11 | @wsgify
class BlogUpdate(BaseArticle):
def response(self):
from webob import Response
if self.request.method == 'POST':
self.article['title'] = self.request.POST['title']
self.article['content'] = self.request.POST['content']
return Response(status=302, location='/')
return Response(env.get_template('create.html')
.render(article=self.article)) |
1 2 3 4 5 6 7 8 | class BlogDelete(BaseArticle):
def __iter__(self):
self.start('302 Found', # '301 Moved Permanently',
[('Content-Type', 'text/html'),
('Location', '/')])
ARTICLES.pop(self.index)
yield b'' |
1 2 3 4 5 6 7 | @wsgify
class BlogDelete(BaseArticle):
def response(self):
from webob import Response
ARTICLES.pop(self.index)
return Response(status=302, location='/') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | from models import ARTICLES
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('templates'))
def wsgify(view):
from webob import Request
def wrapped(environ, start_response):
request = Request(environ)
app = view(request).response()
return app(environ, start_response)
return wrapped
class BaseArticle(object):
def __init__(self, request):
self.request = request
article_id = self.request.environ['wsgiorg.routing_args'][1]['id']
(self.index,
self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
if art['id'] == int(article_id)),
(None, None))
@wsgify
class BlogIndex(object):
def __init__(self, request):
self.page = request.GET.get('page', '1')
from paginate import Page
self.paged_articles = Page(
ARTICLES,
page=self.page,
items_per_page=8,
)
def response(self):
from webob import Response
return Response(env.get_template('index.html')
.render(articles=self.paged_articles)
.encode('utf-8'))
@wsgify
class BlogCreate(object):
def __init__(self, request):
self.request = request
def response(self):
from webob import Response
if self.request.method == 'POST':
max_id = max([art['id'] for art in ARTICLES])
ARTICLES.append(
{'id': max_id+1,
'title': self.request.POST['title'],
'content': self.request.POST['content']
}
)
return Response(status=302, location='/')
return Response(env.get_template('create.html').render(article=None))
@wsgify
class BlogRead(BaseArticle):
def response(self):
from webob import Response
if not self.article:
return Response(status=404)
return Response(env.get_template('read.html')
.render(article=self.article))
@wsgify
class BlogUpdate(BaseArticle):
def response(self):
from webob import Response
if self.request.method == 'POST':
self.article['title'] = self.request.POST['title']
self.article['content'] = self.request.POST['content']
return Response(status=302, location='/')
return Response(env.get_template('create.html')
.render(article=self.article))
@wsgify
class BlogDelete(BaseArticle):
def response(self):
from webob import Response
ARTICLES.pop(self.index)
return Response(status=302, location='/') |
См.также
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from jinja2 import Template
from wtforms import BooleanField, Form, StringField, validators
class RegistrationForm(Form):
username = StringField('Username', [validators.Length(min=4, max=25)])
email = StringField('Email Address', [validators.Length(min=6, max=35)])
rules = BooleanField('I accept the site rules',
[validators.InputRequired()])
if __name__ == '__main__':
form = RegistrationForm(username="root")
template = Template("""
<form method="POST" action="">
{% for field in form.data %}
{{ form[field].label }} |
{{ form[field] }}
<br />
{% endfor %}
<input type="submit" value="Ok">
</form>
""")
print(template.render(form=form)) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <form method="POST" action="">
<label for="username">Username</label> |
<input id="username" name="username" type="text" value="root">
<br />
<label for="rules">I accept the site rules</label> |
<input id="rules" name="rules" type="checkbox" value="y">
<br />
<label for="email">Email Address</label> |
<input id="email" name="email" type="text" value="">
<br />
<input type="submit" value="Ok">
</form>
|
Deform — это Python библиотека для генерации форм. Deform использует Colander как генератор схемы, Peppercorn для десериализации данных из формы и шаблонизатор Chameleon.
Основные задачи, которые выполняет Deform:
Примеры форм http://deformdemo.repoze.org/
См.также
Colander - десериализует данные полученные как XML, JSON, HTTP POST запрос и проверяет правильность их заполнения по заранее заданной схеме.
Для создания простой формы нам понадобится:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import colander
import deform
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('templates'))
class Contact(colander.MappingSchema):
email = colander.SchemaNode(colander.String(), validator=colander.Email())
name = colander.SchemaNode(colander.String())
message = colander.SchemaNode(colander.String(),
widget=deform.widget.TextAreaWidget())
def simple_form(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
from webob import Request
request = Request(environ)
form = deform.Form(Contact(), buttons=('submit',))
template = env.get_template('simple.html')
if request.POST:
submitted = request.POST.items()
try:
form.validate(submitted)
except deform.ValidationFailure as e:
return template.render(form=e.render())
data = {'email': 'jon.staley@fundingoptions.com',
'name': 'Jon',
'message': 'Hello World'}
return template.render(form=form.render(data))
if __name__ == '__main__':
from paste.httpserver import serve
serve(simple_form, host='0.0.0.0', port=8000) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import colander
import deform
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('templates'))
class Contact(colander.MappingSchema):
email = colander.SchemaNode(colander.String(), validator=colander.Email())
name = colander.SchemaNode(colander.String())
message = colander.SchemaNode(colander.String(),
widget=deform.widget.TextAreaWidget())
def simple_form(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
from webob import Request
request = Request(environ)
form = deform.Form(Contact(), buttons=('submit',))
template = env.get_template('simple.html')
if request.POST:
submitted = request.POST.items()
try:
form.validate(submitted)
except deform.ValidationFailure as e:
return template.render(form=e.render())
data = {'email': 'jon.staley@fundingoptions.com',
'name': 'Jon',
'message': 'Hello World'}
return template.render(form=form.render(data))
if __name__ == '__main__':
from paste.httpserver import serve
serve(simple_form, host='0.0.0.0', port=8000) |
1 2 3 4 5 6 7 8 9 10 | <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>simple</title>
</head>
<body>
{{ form }}
</body>
</html> |
Сгенерированная форма
Валидация формы
Добавим стилей:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>simple</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.96.1/css/materialize.min.css" />
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.96.1/js/materialize.min.js"></script>
<style type="text/css">
.deformFormFieldset {
border: none;
padding: 0;
margin: 0;
}
.control-label {
left: 0 !important;
}
.form-group
{
position: relative;
margin: 1em 0 0 0;
}
.alert-danger
{
top: -1.5em;
position: relative;
}
p.help-block[id*="error"]
{
width: 10em;
color: red !important;
font-style: normal;
padding: 0;
margin: 0 0 1em 0;
line-height: 1.5;
font-family: "Roboto", sans-serif;
font-weight: normal;
position: absolute;
left: -12em;
text-align: right;
top: 0.85em;
}
.required:after
{
content: "*";
position: relative;
font-family: "Roboto", sans-serif;
font-weight: normal;
color: red;
}
.errorMsgLbl
{
background: red;
font-family: "Roboto", sans-serif;
font-weight: normal;
padding: 0.5em 1em;
color: #fff;
margin: 0 0 1em 0;
}
.alert-danger .errorMsg
{
display: none;
}
</style>
</head>
<body>
<div class="form" style="width: 500px; margin: 0 auto; padding: 50px">
<h1>Simple Form</h1>
{{ form }}
</div>
</body>
<script type="text/javascript">
$(function() {
$('#item-deformField1').addClass('input-field').addClass('col');
$('#item-deformField2').addClass('input-field').addClass('col');
$('#item-deformField3').addClass('input-field').addClass('col');
$('#deformField3').addClass('materialize-textarea');
});
</script>
</html> |
Сгенерированная форма с применением CSS стилей
Валидация формы с применением CSS стилей
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | import colander
import deform
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('templates'))
class AddressSchema(colander.MappingSchema):
line1 = colander.SchemaNode(colander.String(), title='Address line 1')
line2 = colander.SchemaNode(colander.String(), title='Address line 2',
missing=None)
line3 = colander.SchemaNode(colander.String(), title='Address line 3',
missing=None)
town = colander.SchemaNode(colander.String(), title='Town')
postcode = colander.SchemaNode(colander.String(), title='Postcode')
class Business(AddressSchema):
business_name = colander.SchemaNode(colander.String(),
title='Business Name',
insert_before='line1')
def inheritance_form(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
from webob import Request
request = Request(environ)
form = deform.Form(Business(), buttons=('submit',))
template = env.get_template('simple_with_css.html')
if request.POST:
submitted = request.POST.items()
try:
form.validate(submitted)
except deform.ValidationFailure as e:
return template.render(form=e.render())
return template.render(form=form.render())
if __name__ == '__main__':
from paste.httpserver import serve
serve(inheritance_form, host='0.0.0.0', port=8000) |
Наследование Colander схемы
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | import colander
import deform
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('templates'))
def month_validator(node, month):
if month.isdigit():
int_month = int(month)
if not 0 < int_month < 13:
raise colander.Invalid(node,
'Please enter a number between 1 and 12')
else:
raise colander.Invalid(node, 'Please enter a number')
class AddressSchema(colander.MappingSchema):
line1 = colander.SchemaNode(colander.String(), title='Address line 1')
line2 = colander.SchemaNode(colander.String(), title='Address line 2',
missing=None)
line3 = colander.SchemaNode(colander.String(), title='Address line 3',
missing=None)
town = colander.SchemaNode(colander.String(), title='Town')
postcode = colander.SchemaNode(colander.String(), title='Postcode')
class Business(AddressSchema):
business_name = colander.SchemaNode(colander.String(),
title='Business Name',
insert_before='line1')
start_month = colander.SchemaNode(colander.String(), title='Start month',
validator=month_validator)
def custom_validator_form(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
from webob import Request
request = Request(environ)
form = deform.Form(Business(), buttons=('submit',))
template = env.get_template('simple_with_css.html')
if request.POST:
submitted = request.POST.items()
try:
form.validate(submitted)
except deform.ValidationFailure as e:
return template.render(form=e.render()).encode("utf-8")
return template.render(form=form.render()).encode("utf-8")
if __name__ == '__main__':
from paste.httpserver import serve
serve(custom_validator_form, host='0.0.0.0', port=8000) |
Кастомная валидация поля
См.также
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | import colander
import deform
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('templates'))
def get_session(request):
return request.environ.get('paste.session.factory', lambda: {})()
def get_csrf_token(session):
if 'csrf' not in session:
from uuid import uuid4
session['csrf'] = uuid4().hex
return session['csrf']
@colander.deferred
def deferred_csrf_default(node, kw):
request = kw.get('request')
session = get_session(request)
csrf_token = get_csrf_token(session)
return csrf_token
@colander.deferred
def deferred_csrf_validator(node, kw):
def validate_csrf_token(node, value):
request = kw.get('request')
session = get_session(request)
csrf_token = get_csrf_token(session)
if value != csrf_token:
raise colander.Invalid(node, 'Bad CSRF token')
return validate_csrf_token
class CSRFSchema(colander.Schema):
csrf = colander.SchemaNode(colander.String(),
default=deferred_csrf_default,
validator=deferred_csrf_validator,
# widget=deform.widget.HiddenWidget(), )
)
class Contact(CSRFSchema):
email = colander.SchemaNode(colander.String(), validator=colander.Email())
name = colander.SchemaNode(colander.String())
message = colander.SchemaNode(colander.String(),
widget=deform.widget.TextAreaWidget())
def custom_validator_form(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
from webob import Request
request = Request(environ)
session = get_session(request)
session['csrf'] = get_csrf_token(session)
schema = Contact().bind(request=request)
form = deform.Form(schema, buttons=('submit',))
template = env.get_template('simple_with_css.html')
if request.POST:
submitted = request.POST.items()
try:
form.validate(submitted)
except deform.ValidationFailure as e:
return template.render(form=e.render()).encode("utf-8")
session.pop('csrf')
return template.render(form='OK')
return template.render(form=form.render()).encode("utf-8")
if __name__ == '__main__':
from paste.httpserver import serve
from paste.session import SessionMiddleware
app = SessionMiddleware(custom_validator_form)
serve(app, host='0.0.0.0', port=8000) |
Ключ CSRF
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | import colander
import deform
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('templates'))
def get_session(request):
return request.environ.get('paste.session.factory', lambda: {})()
def get_csrf_token(session):
if 'csrf' not in session:
from uuid import uuid4
session['csrf'] = uuid4().hex
return session['csrf']
@colander.deferred
def deferred_csrf_default(node, kw):
request = kw.get('request')
session = get_session(request)
csrf_token = get_csrf_token(session)
return csrf_token
@colander.deferred
def deferred_csrf_validator(node, kw):
def validate_csrf_token(node, value):
request = kw.get('request')
session = get_session(request)
csrf_token = get_csrf_token(session)
if value != csrf_token:
raise colander.Invalid(node, 'Bad CSRF token')
return validate_csrf_token
class CSRFSchema(colander.Schema):
csrf = colander.SchemaNode(colander.String(),
default=deferred_csrf_default,
validator=deferred_csrf_validator,
# widget=deform.widget.HiddenWidget(), )
)
class Contact(CSRFSchema):
email = colander.SchemaNode(colander.String(), validator=colander.Email())
name = colander.SchemaNode(colander.String())
message = colander.SchemaNode(colander.String(),
widget=deform.widget.TextAreaWidget())
def custom_validator_form(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
from webob import Request
request = Request(environ)
session = get_session(request)
session['csrf'] = get_csrf_token(session)
schema = Contact().bind(request=request)
form = deform.Form(schema, buttons=('submit',))
template = env.get_template('simple_with_css.html')
if request.POST:
submitted = request.POST.items()
try:
form.validate(submitted)
except deform.ValidationFailure as e:
return template.render(form=e.render()).encode("utf-8")
session.pop('csrf')
return template.render(form='OK')
return template.render(form=form.render()).encode("utf-8")
if __name__ == '__main__':
from paste.httpserver import serve
from paste.session import SessionMiddleware
app = SessionMiddleware(custom_validator_form)
serve(app, host='0.0.0.0', port=8000) |
Ключ CSRF
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | import os
import colander
import deform
from jinja2 import Environment, FileSystemLoader
from pkg_resources import resource_filename
env = Environment(loader=FileSystemLoader('templates'))
deform_path = os.path.abspath('templates/deform')
deform_templates = resource_filename('deform', 'templates')
print(deform_templates)
print(deform_path)
search_path = (deform_path, deform_templates)
renderer = deform.ZPTRendererFactory(search_path)
class Contact(colander.MappingSchema):
email = colander.SchemaNode(colander.String(), validator=colander.Email())
name = colander.SchemaNode(colander.String())
message = colander.SchemaNode(colander.String(),
widget=deform.widget.TextAreaWidget())
def custom_template_form(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
from webob import Request
request = Request(environ)
form = deform.Form(Contact(), buttons=('submit',), renderer=renderer)
template = env.get_template('simple_with_css.html')
if request.POST:
submitted = request.POST.items()
try:
form.validate(submitted)
except deform.ValidationFailure as e:
return template.render(form=e.render()).encode("utf-8")
data = {'email': 'jon.staley@fundingoptions.com',
'name': 'Jon',
'message': 'Hello World'}
return template.render(form=form.render(data)).encode("utf-8")
if __name__ == '__main__':
from paste.httpserver import serve
serve(custom_template_form, host='0.0.0.0', port=8000) |
$ tree templates/deform/
templates/deform/
├── form.pt
└── mapping_item.pt
0 directories, 2 files
Переопределенный шаблон form.pt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | import os
import colander
import deform
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('templates'))
def jinja2_renderer(template_name, **kw):
kw['_'] = str # Hook for translation string with gettext
from jinja2 import Template
deform_jinja_path = os.path.abspath('templates/deform_jinja2')
jinja2_template = os.path.join(deform_jinja_path,
template_name + '.jinja2')
template = Template(open(jinja2_template).read())
return template.render(**kw)
class Contact(colander.MappingSchema):
email = colander.SchemaNode(colander.String(), validator=colander.Email())
name = colander.SchemaNode(colander.String())
message = colander.SchemaNode(colander.String(),
widget=deform.widget.TextAreaWidget())
def custom_template_form(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
from webob import Request
request = Request(environ)
form = deform.Form(Contact(), buttons=('submit',),
renderer=jinja2_renderer)
template = env.get_template('simple_with_css.html')
if request.POST:
submitted = request.POST.items()
try:
form.validate(submitted)
except deform.ValidationFailure as e:
return template.render(form=e.render()).encode("utf-8")
data = {'email': 'jon.staley@fundingoptions.com',
'name': 'Jon',
'message': 'Hello World'}
return template.render(form=form.render(data)).encode("utf-8")
if __name__ == '__main__':
from paste.httpserver import serve
serve(custom_template_form, host='0.0.0.0', port=8000) |
$ tree templates/deform_jinja2/
templates/deform_jinja2/
├── autocomplete_input.jinja2
├── checkbox_choice.jinja2
├── checkbox.jinja2
├── checked_input.jinja2
├── checked_password.jinja2
├── dateinput.jinja2
├── dateparts.jinja2
├── datetimeinput.jinja2
├── file_upload.jinja2
├── form.jinja2
├── hidden.jinja2
├── mapping_item.jinja2
├── mapping.jinja2
├── moneyinput.jinja2
├── password.jinja2
├── radio_choice.jinja2
├── readonly
│ ├── checkbox_choice.jinja2
│ ├── checkbox.jinja2
│ ├── checked_input.jinja2
│ ├── checked_password.jinja2
│ ├── dateparts.jinja2
│ ├── file_upload.jinja2
│ ├── form.jinja2
│ ├── mapping_item.jinja2
│ ├── mapping.jinja2
│ ├── password.jinja2
│ ├── radio_choice.jinja2
│ ├── richtext.jinja2
│ ├── select.jinja2
│ ├── sequence_item.jinja2
│ ├── sequence.jinja2
│ ├── textarea.jinja2
│ └── textinput.jinja2
├── richtext.jinja2
├── select.jinja2
├── sequence_item.jinja2
├── sequence.jinja2
├── textarea.jinja2
└── textinput.jinja2
1 directory, 39 files
1 2 3 4 5 6 7 8 9 10 11 12 | <textarea tal:define="rows rows|field.widget.rows;
cols cols|field.widget.cols;
css_class css_class|field.widget.css_class;
oid oid|field.oid;
name name|field.name;
style style|field.widget.style"
tal:attributes="rows rows;
cols cols;
class string: form-control ${css_class or ''};
style style"
id="${oid}"
name="${name}">${cstruct}</textarea> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <textarea
style="background: green;color:white;"
{% if field.widget.rows %}
rows="{{ field.widget.rows }}"
{% endif %}
{% if field.widget.cols %}
cols="{{ field.widget.cols }}"
{% endif %}
{% if field.widget.css_class %}
class="{{ field.widget.css_class }}"
{% endif %}
id="{{ field.oid }}"
name="{{ field.name }}"
{% if field.description %}
placeholder="{{ _(field.description) }}"
{% endif %}>{{ cstruct }}</textarea> |
Переопределенный шаблон textarea.jinja2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | import deform
import colander
from common import get_csrf_token, get_session
@colander.deferred
def deferred_csrf_default(node, kw):
request = kw.get('request')
session = get_session(request)
csrf_token = get_csrf_token(session)
return csrf_token
@colander.deferred
def deferred_csrf_validator(node, kw):
def validate_csrf_token(node, value):
request = kw.get('request')
session = get_session(request)
csrf_token = get_csrf_token(session)
if value != csrf_token:
raise colander.Invalid(node, 'Bad CSRF token')
return validate_csrf_token
class CSRFSchema(colander.Schema):
csrf = colander.SchemaNode(colander.String(),
default=deferred_csrf_default,
validator=deferred_csrf_validator,
widget=deform.widget.HiddenWidget(), )
class CreateArticle(CSRFSchema):
title = colander.SchemaNode(colander.String())
content = colander.SchemaNode(
colander.String(),
widget=deform.widget.TextAreaWidget(
css_class="blog-form-field__textarea")
) |
1 2 3 4 5 6 7 8 9 | def get_session(request):
return request.environ.get('paste.session.factory', lambda: {})()
def get_csrf_token(session):
if 'csrf' not in session:
from uuid import uuid4
session['csrf'] = uuid4().hex
return session['csrf'] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | #! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
"""
Simple blog
"""
from paste.auth.basic import AuthBasicHandler
import selector
from views import BlogCreate, BlogDelete, BlogIndex, BlogRead, BlogUpdate
def authfunc(environ, username, password):
return username == 'admin' and password == '123'
def make_wsgi_app():
# BasicAuth applications
create = AuthBasicHandler(BlogCreate, 'www', authfunc)
update = AuthBasicHandler(BlogUpdate, 'www', authfunc)
delete = AuthBasicHandler(BlogDelete, 'www', authfunc)
# URL dispatching middleware
dispatch = selector.Selector()
dispatch.add('/', GET=BlogIndex)
dispatch.prefix = '/article'
dispatch.add('/add', GET=create, POST=create)
dispatch.add('/{id:digits}', GET=BlogRead)
dispatch.add('/{id:digits}/edit', GET=update, POST=update)
dispatch.add('/{id:digits}/delete', GET=delete)
# Static files
from paste.urlparser import StaticURLParser
static_app = StaticURLParser("static/")
from paste import urlmap
mapping = urlmap.URLMap()
mapping['/static'] = static_app
from paste.cascade import Cascade
app = Cascade([mapping, dispatch])
return app
if __name__ == '__main__':
from paste.httpserver import serve
from paste.session import SessionMiddleware
app = make_wsgi_app()
app = SessionMiddleware(app)
serve(app, host='0.0.0.0', port=8000) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | import deform
from jinja2 import Environment, FileSystemLoader
from webob import Request, Response
from common import get_csrf_token, get_session
from models import ARTICLES
env = Environment(loader=FileSystemLoader('templates'))
def wsgify(view):
def wrapped(environ, start_response):
request = Request(environ)
app = view(request).response()
return app(environ, start_response)
return wrapped
class BaseArticle(object):
def __init__(self, request):
self.request = request
article_id = self.request.environ['wsgiorg.routing_args'][1]['id']
(self.index,
self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
if art['id'] == int(article_id)),
(None, None))
class BaseArticleForm(object):
def get_form(self):
from forms import CreateArticle
self.session = get_session(self.request)
self.session['csrf'] = get_csrf_token(self.session)
schema = CreateArticle().bind(request=self.request)
submit = deform.Button(name='submit',
css_class='blog-form__button')
self.form = deform.Form(schema, buttons=(submit,))
return self.form
@wsgify
class BlogIndex(object):
def __init__(self, request):
self.page = request.GET.get('page', '1')
from paginate import Page
self.paged_articles = Page(
ARTICLES,
page=self.page,
items_per_page=8,
)
def response(self):
return Response(env.get_template('index.html')
.render(articles=self.paged_articles))
@wsgify
class BlogCreate(BaseArticleForm):
def __init__(self, request):
self.request = request
def response(self):
if self.request.method == 'POST':
submitted = self.request.POST.items()
try:
self.get_form().validate(submitted)
except deform.ValidationFailure as e:
return Response(
env.get_template('create.html').render(form=e.render()))
max_id = max([art['id'] for art in ARTICLES])
ARTICLES.append(
{'id': max_id+1,
'title': self.request.POST['title'],
'content': self.request.POST['content']
}
)
self.session = get_session(self.request).pop('csrf')
return Response(status=302, location='/')
return Response(env.get_template('create.html')
.render(form=self.get_form().render()))
@wsgify
class BlogRead(BaseArticle):
def response(self):
if not self.article:
return Response(status=404)
return Response(env.get_template('read.html')
.render(article=self.article))
@wsgify
class BlogUpdate(BaseArticle, BaseArticleForm):
def response(self):
if self.request.method == 'POST':
submitted = self.request.POST.items()
try:
self.get_form().validate(submitted)
except deform.ValidationFailure as e:
return Response(
env.get_template('create.html').render(form=e.render()))
self.article['title'] = self.request.POST['title']
self.article['content'] = self.request.POST['content']
self.session = get_session(self.request).pop('csrf')
return Response(status=302, location='/')
return Response(
env.get_template('create.html')
.render(form=self.get_form().render(self.article)))
@wsgify
class BlogDelete(BaseArticle):
def response(self):
ARTICLES.pop(self.index)
return Response(status=302, location='/') |
1 2 3 4 5 6 7 8 9 10 11 12 13 | {% extends "base.html" %}
{% block title %}Create{% endblock %}
{% block content %}
<div class="blog__title">
<a href="/" class="blog__title-link">Simple Blog</a>
<span class="blog__title-text">Edit</span>
</div>
<form action="" method="POST" class="blog-form">
{{ form }}
</form>
{% endblock %} |
Теперь форма имеет валидацию, защиту от CSRF атак и генерируется автоматически при помощи Deform.
Валидация формы
См.также
Beaker — это библиотека предназначенная, для кэширования и создания сессии, как в веб-приложениях, так и в чистых Python скриптах. Имеет WSGI-middleware для WSGI-приложений и декоратор (Декораторы) для простых приложений.
get_session
создает сессию¶1 2 3 4 5 6 7 8 | from beaker.session import Session
def get_session(request={}, **kwargs):
"""A shortcut for creating :class:`Session` instance"""
options = {}
options.update(**kwargs)
return Session(request, **options) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | # -*- coding: utf-8 -*-
from pprint import pprint
from common import get_session
if '__main__' in __name__:
"""Test if the data is actually persistent across requests"""
session = get_session()
session['Suomi'] = 'Kimi Räikkönen'
session['Great Britain'] = 'Jenson Button'
session['Deutchland'] = 'Sebastian Vettel'
session.save()
print("Session ID: " + session.id)
pprint(session)
print
print("Check session")
session2 = get_session(id=session.id)
assert 'Suomi' in session2
assert 'Great Britain' in session2
assert 'Deutchland' in session2
assert session2['Suomi'] == 'Kimi Räikkönen'
assert session2['Great Britain'] == 'Jenson Button'
assert session2['Deutchland'] == 'Sebastian Vettel'
print("OK")
print
assert session2['Russian'] == 'Alexey Popov' |
1 2 3 4 5 6 7 8 9 10 11 12 | Session ID: a628f9a5f15e48f99b06d3a710791499
{'Deutchland': 'Sebastian Vettel',
'Great Britain': 'Jenson Button',
'Suomi': 'Kimi Räikkönen',
'_accessed_time': 1475907168.6420162,
'_creation_time': 1475907168.6420162}
Check session
OK
Traceback (most recent call last):
File "0.session.py", line 27, in <module>
assert session2['Russian'] == 'Alexey Popov'
KeyError: 'Russian' |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # -*- coding: utf-8 -*-
from pprint import pprint
from common import get_session
if '__main__' in __name__:
"""Test if the data is actually persistent across requests"""
session = get_session()
session['Suomi'] = 'Kimi Räikkönen'
session['Great Britain'] = 'Jenson Button'
session['Deutchland'] = 'Sebastian Vettel'
session.save()
print("Session ID: " + session.id)
pprint(session)
session.delete()
print
print("Delete session")
print("Session ID: " + session.id)
pprint(session)
assert 'Suomi' not in session
assert 'Great Britain' not in session
assert 'Deutchland' not in session
assert 'Russian' not in session |
1 2 3 4 5 6 7 8 9 | Session ID: f0f6d2a4e02841e49d97ee5ba7ac3985
{'Deutchland': 'Sebastian Vettel',
'Great Britain': 'Jenson Button',
'Suomi': 'Kimi Räikkönen',
'_accessed_time': 1475907306.4009516,
'_creation_time': 1475907306.4009516}
Delete session
Session ID: f0f6d2a4e02841e49d97ee5ba7ac3985
{} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | # -*- coding: utf-8 -*-
from pprint import pprint
from common import get_session
if '__main__' in __name__:
"""Test if the data is actually persistent across requests"""
session = get_session()
session['Suomi'] = 'Kimi Räikkönen'
session['Great Britain'] = 'Jenson Button'
session['Deutchland'] = 'Sebastian Vettel'
session.save()
print("Session ID: " + session.id)
pprint(session)
session2 = get_session(id=session.id)
del session2['Suomi']
session2['Great Britain'] = 'Lewis Hamilton'
session2['Deutchland'] = 'Michael Schumacher'
session2['España'] = 'Fernando Alonso'
print
print("Modified session")
print("Session ID: " + session2.id)
pprint(session2)
session2.revert()
print
print("Revert session")
print("Session ID: " + session2.id)
pprint(session2) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | Session ID: 2887074c36af4aadad354a79bbdb2cc4
{'Deutchland': 'Sebastian Vettel',
'Great Britain': 'Jenson Button',
'Suomi': 'Kimi Räikkönen',
'_accessed_time': 1475907344.1409602,
'_creation_time': 1475907344.1409602}
Modified session
Session ID: 2887074c36af4aadad354a79bbdb2cc4
{'Deutchland': 'Michael Schumacher',
'España': 'Fernando Alonso',
'Great Britain': 'Lewis Hamilton',
'_accessed_time': 1475907344.1411998,
'_creation_time': 1475907344.1409602}
Revert session
Session ID: 2887074c36af4aadad354a79bbdb2cc4
{'Deutchland': 'Sebastian Vettel',
'Great Britain': 'Jenson Button',
'Suomi': 'Kimi Räikkönen',
'_accessed_time': 1475907344.1411998,
'_creation_time': 1475907344.1409602} |
По умолчанию сессии хранятся в оперативной памяти и при завершении программы удаляются. Чтобы сессии хранились постоянно, нужно указать место на диске:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # -*- coding: utf-8 -*-
from pprint import pprint
from common import get_session
if '__main__' in __name__:
"""Test if the data is actually persistent across requests"""
session = get_session(data_dir='./cache', type='file')
session['Suomi'] = 'Kimi Räikkönen'
session['Great Britain'] = 'Jenson Button'
session['Deutchland'] = 'Sebastian Vettel'
session.save()
print("Session ID: " + session.id)
pprint(session)
session2 = get_session(id=session.id, data_dir='./cache', type='file')
print
print("File storage session")
print("Session ID: " + session2.id)
pprint(session2) |
1 2 3 4 5 6 7 8 9 10 11 12 13 | Session ID: 214e9b8724334492a814e5b0b1a797ff
{'Deutchland': 'Sebastian Vettel',
'Great Britain': 'Jenson Button',
'Suomi': 'Kimi Räikkönen',
'_accessed_time': 1475907695.6439698,
'_creation_time': 1475907695.6439698}
File storage session
Session ID: 214e9b8724334492a814e5b0b1a797ff
{'Deutchland': 'Sebastian Vettel',
'Great Britain': 'Jenson Button',
'Suomi': 'Kimi Räikkönen',
'_accessed_time': 1475907695.6447632,
'_creation_time': 1475907695.6439698} |
cache/
├── container_file
│ └── 1
│ └── 18
│ └── 18b9908ab7514d8e8d16ae05e1eb09e0.cache
└── container_file_lock
└── c
└── c6
└── c6e93db703a3eea0207cc7efca5ddd0cbb201919.lock
6 directories, 2 files
1 2 3 4 5 | Ђ}q X sessionq}q(X _accessed_timeqGAХю%MеќИX SuomiqX Kimi RГ¤ikkГ¶nenqX
Great BritainqX
Jenson ButtonqX _creation_timeqGAХю%MеќИX
Deutchlandq X Sebastian Vettelq
us. |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | # -*- coding: utf-8 -*-
from pprint import pprint
from common import get_session
if '__main__' in __name__:
"""Test if the data is actually persistent across requests"""
session = get_session(
type='ext:memcached',
url='memcached:11211',
)
session['Suomi'] = 'Kimi Räikkönen'
session['Great Britain'] = 'Jenson Button'
session['Deutchland'] = 'Sebastian Vettel'
session.save()
print("Session ID: " + session.id)
pprint(session)
session2 = get_session(
id=session.id,
type='ext:memcached',
url='memcached:11211'
)
print
print("Memcached storage session")
print("Session ID: " + session2.id)
pprint(session2) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Session ID: 8c549978a6984a648d2dadba44becd23
{'Deutchland': 'Sebastian Vettel',
'Great Britain': 'Jenson Button',
'Suomi': 'Kimi R\xc3\xa4ikk\xc3\xb6nen',
'_accessed_time': 1429279819.517085,
'_creation_time': 1429279819.517085}
Memcached storage session
Session ID: 8c549978a6984a648d2dadba44becd23
{'Deutchland': 'Sebastian Vettel',
'Great Britain': 'Jenson Button',
'Suomi': 'Kimi R\xc3\xa4ikk\xc3\xb6nen',
'_accessed_time': 1429279819.52295,
'_creation_time': 1429279819.517085} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | from beaker.middleware import SessionMiddleware
def simple_app(environ, start_response):
# Get the session object from the environ
session = environ['beaker.session']
# Set some other session variable
session['counter'] = session.get('counter', 0) + 1
session.save()
start_response('200 OK', [('Content-type', 'text/plain')])
return [
str.encode(
'Counter value is: {}'.format(session['counter'])
)
]
# Configure the SessionMiddleware
session_opts = {
'session.type': 'file',
'session.data_dir': './cache2/data',
'session.lock_dir': './cache2/lock',
'session.cookie_expires': True,
}
wsgi_app = SessionMiddleware(simple_app, session_opts)
if __name__ == '__main__':
from paste.httpserver import serve
serve(wsgi_app, host='0.0.0.0', port=8000) |
Первый запуск страницы
На страницу заходили 84 раза
Если хранить кэш на диске, то при значении 13
получим:
$ hexdump -v -C cache2/data/container_file/a/a1/a138e686410c40ef9014549d8b339cc9.cache
00000000 80 03 7d 71 00 58 07 00 00 00 73 65 73 73 69 6f |..}q.X....sessio|
00000010 6e 71 01 7d 71 02 28 58 07 00 00 00 63 6f 75 6e |nq.}q.(X....coun|
00000020 74 65 72 71 03 4b 0d 58 0e 00 00 00 5f 61 63 63 |terq.K.X...._acc|
00000030 65 73 73 65 64 5f 74 69 6d 65 71 04 47 41 d5 fe |essed_timeq.GA..|
00000040 24 8e 01 39 c1 58 0e 00 00 00 5f 63 72 65 61 74 |$..9.X...._creat|
00000050 69 6f 6e 5f 74 69 6d 65 71 05 47 41 d5 fe 24 87 |ion_timeq.GA..$.|
00000060 8d 20 eb 75 73 2e |. .us.|
00000066
См.также
См.также
Несмотря на стандарт SQL (ISO/IEC 9075), отдельные СУБД имеют много различий. Чтобы программистам не вникать в реализацию каждой из них, придумали общее API (PEP 249) скрывающее эти детали. Любой Python пакет реализующий это API взаимозаменяем.
PEP 249 это только спецификация, реализацию которой вам придется выполнить самостоятельно или воспользоваться уже готовой, например для sqlite3.
Также существуют реализации для других СУБД:
Большинство из них могут быть установлены стандартным способом:
$ pip install psycopg2
$ pip install mysql-python
- 0 Модуль не поддерживает потоки.
- 1 Потоки могут совместно использовать модуль, но не соединения.
- 2 Потоки могут совместно использовать модуль и соединения.
- 3 Потоки могут совместно использовать модуль, соединения и курсоры. (Под совместным использованием здесь понимается возможность использования упомянутых ресурсов без применения семафоров).
- «format» Форматирование в стиле языка ANSI C (например, «%s», «%i» ).
- «pyformat» Использование именованных спецификаторов формата в стиле Python ( «%(item)s» )
- «qmark» Использование знаков «?» для пометки мест подстановки параметров.
- «numeric» Использование номеров позиций ( «:1» ).
- «named» Использование имен подставляемых параметров ( «:name» ).
Доступ к базе данных осуществляется с помощью объекта-соединения (connection object). DB-API-совместимый модуль должен предоставлять функцию-конструктор connect() для класса объектов-соединений. Конструктор должен иметь следующие именованные параметры:
Объект-соединение, получаемый в результате успешного вызова функции
connect()
, должен иметь следующие методы:
Курсор (от англ. cursor - CURrrent Set Of Records, текущий набор записей) служит для работы с результатом запроса. Результатом запроса обычно является одна или несколько прямоугольных таблиц со столбцами-полями и строками-записями. Приложение может читать и обрабатывать полученные таблицы и записи в таблице по одной, поэтому в курсоре хранится информация о текущей таблице и записи. Конкретный курсор в любой момент времени связан с выполнением одной SQL-инструкции.
fetchmany()
. По умолчанию равен 1.rowcount - Количество записей, полученных или затронутых в результате выполнения последнего запроса. В случае отсутствия execute-запросов или невозможности указать количество записей равен -1.
description - Этот доступный только для чтения атрибут является последовательностью из семиэлементных последовательностей. Каждая из этих последовательностей содержит информацию, описывающую один столбец результата:
Первые два элемента (имя и тип) обязательны, а вместо остальных (размер для вывода, внутренний размер, точность, масштаб, возможность задания пустого значения) может быть значение None. Этот атрибут может быть равным None для операций, не возвращающих значения.
DB-API 2.0 предусматривает названия для объектов-типов, используемых для описания полей базы данных:
Объект | Тип |
---|---|
STRING | Строка и символ |
BINARY | Бинарный объект |
NUMBER | Число |
DATETIME | Дата и время |
ROWID | Идентификатор записи |
None | NULL-значение (отсутствующее значение) |
С каждым типом данных (в реальности это - классы) связан конструктор. Совместимый с DB-API модуль должен определять следующие конструкторы:
DB API спецификация требует реализацию классов исключений следующей структуры:
StandardError
├──Warning
└──Error
├──InterfaceError (a problem with the db api)
└──DatabaseError (a problem with the database)
├──DataError (bad data, values out of range, etc.)
├──OperationalError (the db has an issue out of our control)
├──IntegrityError
├──InternalError
├──ProgrammingError (something wrong with the operation)
└──NotSupportedError (the operation is not supported)
См.также
SQLite - это БД которая хранит базу в одном файле и не требует отдельного
процесса для запуска, при этом использует не стандартный вариант языка SQL.
Такой подход позволяет встроить sqlite
прямо в программу, без необходимости
установки сервера БД. Пример использования sqlite
в C++
:
#include <stdio.h>
#include <sqlite3.h>
static int callback(void *NotUsed, int argc, char **argv, char **azColName){
int i;
for(i=0; i<argc; i++){
printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
printf("\n");
return 0;
}
int main(int argc, char **argv){
sqlite3 *db;
char *zErrMsg = 0;
int rc;
if( argc!=3 ){
fprintf(stderr, "Usage: %s DATABASE SQL-STATEMENT\n", argv[0]);
return(1);
}
rc = sqlite3_open(argv[1], &db);
if( rc ){
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return(1);
}
rc = sqlite3_exec(db, argv[2], callback, 0, &zErrMsg);
if( rc!=SQLITE_OK ){
fprintf(stderr, "SQL error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}
sqlite3_close(db);
return 0;
}
SQLite можно использовать для хранения внутренних данных программы (например FireFox хранит куки в sqlite) или для создания прототипа приложения, а затем портировать код в крупную БД типа Postgres.
Модуль sqlite3
совместим c DB-API 2.0 спецификацией, опиcаной в
PEP 249.
Чтобы использовать этот модуль, вы должны сначала создать объект
sqlite3.Connection
который представляет базу данных.
import sqlite3
conn = sqlite3.connect('example.sqlite')
Также можно создать БД в ОЗУ при помощи специального имени :memory:
.
import sqlite3
conn = sqlite3.connect(':memory:')
После создания объекта sqlite3.Connection
, можно создать объект
sqlite3.Cursor
и вызвать метод sqlite3.Cursor.execute()
для выполнения SQL запросов.
c = conn.cursor()
# Создание таблицы
c.execute('''CREATE TABLE stocks
(date text, trans text, symbol text, qty real, price real)''')
# Добавление записи
c.execute("INSERT INTO stocks VALUES ('2006-01-05','BUY','RHAT',100,35.14)")
# Сохранение (commit) изменений
conn.commit()
# Закрытие соединения.
# Если изменения не были сохранены (метод commit), то данные пропадут.
conn.close()
Для «экранирования» данных используйте ?
заместо %s
:
# Никогда так не делайте -- не безопасно!
symbol = 'RHAT'
c.execute("SELECT * FROM stocks WHERE symbol = '%s'" % symbol)
# Правильно
t = ('RHAT',)
c.execute('SELECT * FROM stocks WHERE symbol=?', t)
print(c.fetchone())
# Запись сразу нескольких объектов за раз
purchases = [('2006-03-28', 'BUY', 'IBM', 1000, 45.00),
('2006-04-05', 'BUY', 'MSFT', 1000, 72.00),
('2006-04-06', 'SELL', 'IBM', 500, 53.00),
]
c.executemany('INSERT INTO stocks VALUES (?,?,?,?,?)', purchases)
Чтение данных:
>>> for row in c.execute('SELECT * FROM stocks ORDER BY price'):
print(row)
('2006-01-05', 'BUY', 'RHAT', 100, 35.14)
('2006-03-28', 'BUY', 'IBM', 1000, 45.0)
('2006-04-06', 'SELL', 'IBM', 500, 53.0)
('2006-04-05', 'BUY', 'MSFT', 1000, 72.0)
Psycopg - это самая популярная библиотека для PostgreSQL в языке программирования python. Основные преимущества ее, это реализация DB-API 2.0 спецификации и потокобезопасность. Написана на Си, как обертка над libpq.
>>> import psycopg2
# Подключение к существующей базе
>>> conn = psycopg2.connect("dbname=test user=postgres")
# Open a cursor to perform database operations
>>> cur = conn.cursor()
# Выполнение SQL запроса: создает новую базу
>>> cur.execute("CREATE TABLE test (id serial PRIMARY KEY, num integer, data varchar);")
# Pass data to fill a query placeholders and let Psycopg perform
# the correct conversion (no more SQL injections!)
>>> cur.execute("INSERT INTO test (num, data) VALUES (%s, %s)",
... (100, "abc'def"))
# Query the database and obtain data as Python objects
>>> cur.execute("SELECT * FROM test;")
>>> cur.fetchone()
(1, 100, "abc'def")
# Make the changes to the database persistent
>>> conn.commit()
# Close communication with the database
>>> cur.close()
>>> conn.close()
См.также
ORM (англ. object-relational mapping, рус. объектно-реляционное отображение) — технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных». Существуют как проприетарные, так и свободные реализации этой технологии.
SQLAlchemy — это библиотека на языке Python для работы с реляционными СУБД с применением технологии ORM. Служит для синхронизации объектов Python и записей реляционной базы данных. SQLAlchemy позволяет описывать структуры баз данных и способы взаимодействия с ними на языке Python без использования SQL.
Диаграмма уровней SQLAlchemy
Использование SQLAlchemy для автоматической генерации SQL-кода имеет несколько преимуществ по сравнению с ручным написанием SQL:
Простейший пример с использованием SQLite в оперативной памяти:
1 2 3 4 | >>> from sqlalchemy import create_engine
>>> engine = create_engine('sqlite:///:memory:')
>>> engine.execute("select 'Hello, World!'").scalar()
u'Hello, World!' |
См.также
Создадим две таблицы и добавим сотрудников (employee
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from sqlalchemy import create_engine
import os
if os.path.exists("some.db"):
os.remove("some.db")
e = create_engine("sqlite:///some.db")
e.execute("""
create table employee (
emp_id integer primary key,
emp_name varchar
)
""")
e.execute("""
create table employee_of_month (
emp_id integer primary key,
emp_name varchar
)
""")
e.execute("""insert into employee(emp_name) values ('ed')""")
e.execute("""insert into employee(emp_name) values ('jack')""")
e.execute("""insert into employee(emp_name) values ('fred')""") |
SQL запрос:
create table employee (
emp_id integer primary key,
emp_name varchar
);
create table employee_of_month (
emp_id integer primary key,
emp_name varchar
);
insert into employee(emp_name) values ('ed');
insert into employee(emp_name) values ('jack');
insert into employee(emp_name) values ('fred');
$ sqlite3 some.db
sqlite> .tables
employee employee_of_month
sqlite> SELECT * FROM employee;
1|ed
2|jack
3|fred
sqlite> SELECT * FROM employee_of_month;
sqlite>
Функция sqlalchemy.create_engine()
создает новый экземпляр класса
sqlalchemy.engine.Engine
который предоставляет подключение к базе данных.
1 2 | from sqlalchemy import create_engine
engine = create_engine("sqlite:///some.db") |
Метод sqlalchemy.engine.Engine.execute()
выполняет SQL запрос
в нашем соединении и возвращает объект класса sqlalchemy.engine.ResultProxy
.
1 2 3 4 | result = engine.execute(
"select emp_id, emp_name from "
"employee where emp_id=:emp_id",
emp_id=3) |
В результате выполнится следующий SQL запрос:
select emp_id, emp_name from employee where emp_id=3;
Объект класса sqlalchemy.engine.ResultProxy
реализует некоторые методы
из спецификации DB-API 2.0:
sqlalchemy.engine.ResultProxy.fetchone()
sqlalchemy.engine.ResultProxy.fetchmany()
sqlalchemy.engine.ResultProxy.fetchall()
Результат запроса похож на список.
1 2 | row = result.fetchone()
print(row) # (3, u'fred') |
Но также выполняет функции словаря.
1 | print(row['emp_name']) # u'fred' |
Объект класса sqlalchemy.engine.ResultProxy
является итератором,
поэтому можно получить список всех строк в цикле.
1 2 3 4 5 6 | result = engine.execute("select * from employee")
for row in result:
print(row)
# (1, u'ed')
# (2, u'jack')
# (3, u'fred') |
Тоже самое делает функция sqlalchemy.engine.ResultProxy.fetchall()
1 2 3 | result = engine.execute("select * from employee")
print(result.fetchall())
# [(1, u'ed'), (2, u'jack'), (3, u'fred')] |
Соединение закроется автоматически после выполнения SQL запроса,
но можно это сделать и вручную, при помощи метода
sqlalchemy.engine.ResultProxy.close()
1 | result.close() |
sqlalchemy.engine.Engine.execute()
автоматически подтверждает транзакцию
в текущем соединении
(выполняет COMMIT)
1 2 | engine.execute("insert into employee_of_month (emp_name) values (:emp_name)",
emp_name='fred') |
Мы можем контролировать соединение используя метод
sqlalchemy.engine.Engine.connect()
1 2 3 4 | conn = engine.connect()
result = conn.execute("select * from employee")
result.fetchall()
conn.close() |
Он также дает возможность управлять транзакциями.
Транзакция является объектом класса sqlalchemy.engine.Transaction
и содержит в себе следующие методы:
sqlalchemy.engine.Transaction.close()
- выполняет rollback
sqlalchemy.engine.Transaction.commit()
- подтверждает транзакциюsqlalchemy.engine.Transaction.rollback()
- отменяет транзакциюМетод sqlalchemy.engine.Transaction.commit()
позволяет вам вручную подтвердить транзакцию.
1 2 3 4 5 6 | conn = engine.connect()
trans = conn.begin()
conn.execute("insert into employee (emp_name) values (:emp_name)", emp_name="wendy")
conn.execute("update employee_of_month set emp_name = :emp_name", emp_name="wendy")
trans.commit()
conn.close() |
SQL запрос:
BEGIN;
insert into employee (emp_name) values 'wendy';
update employee_of_month set emp_name = 'wendy';
COMMIT;
1 2 3 4 | print(engine.execute("select * from employee").fetchall())
print(engine.execute("select * from employee_of_month").fetchall())
# [(1, u'ed'), (2, u'jack'), (3, u'fred'), (4, u'wendy')]
# [(1, u'wendy')] |
Метод sqlalchemy.engine.Transaction.rollback()
отменяет транзакцию, откатывая данные к начальному состоянию.
1 2 3 4 5 6 | conn = engine.connect()
trans = conn.begin()
conn.execute("insert into employee (emp_name) values (:emp_name)", emp_name="wendy")
conn.execute("update employee_of_month set emp_name = :emp_name", emp_name="wendy")
trans.rollback()
conn.close() |
SQL запрос:
BEGIN;
insert into employee (emp_name) values 'wendy';
update employee_of_month set emp_name = 'wendy';
ROLLBACK;
1 2 3 4 | print(engine.execute("select * from employee").fetchall())
print(engine.execute("select * from employee_of_month").fetchall())
# [(1, u'ed'), (2, u'jack'), (3, u'fred')]
# [(1, u'fred')] |
Контекстный менеджер немного упрощает это процесс:
1 2 3 | with engine.begin() as conn:
conn.execute("insert into employee (emp_name) values (:emp_name)", emp_name="mary")
conn.execute("update employee_of_month set emp_name = :emp_name", emp_name="mary") |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | # slide:: s
from sqlalchemy import create_engine
import os
if os.path.exists("some.db"):
os.remove("some.db")
e = create_engine("sqlite:///some.db")
e.execute("""
create table employee (
emp_id integer primary key,
emp_name varchar
)
""")
e.execute("""
create table employee_of_month (
emp_id integer primary key,
emp_name varchar
)
""")
e.execute("""insert into employee(emp_name) values ('ed')""")
e.execute("""insert into employee(emp_name) values ('jack')""")
e.execute("""insert into employee(emp_name) values ('fred')""")
# # slide::
# ## title:: Engine Basics
# create_engine() builds a *factory* for database connections.
from sqlalchemy import create_engine
engine = create_engine("sqlite:///some.db")
# ## slide:: p
# Engine features an *execute()* method that will run a query on
# a connection for us.
result = engine.execute(
"select emp_id, emp_name from "
"employee where emp_id=:emp_id",
emp_id=3)
# ## slide::
# the result object we get back features methods like fetchone(),
# fetchall()
row = result.fetchone()
# ## slide:: i
# the row looks like a tuple
row
# ## slide:: i
# but also acts like a dictionary
row['emp_name']
# ## slide::
# results close automatically when all rows are exhausted, but we can
# also close explicitly.
result.close()
# ## slide:: p
# result objects can also be iterated
result = engine.execute("select * from employee")
for row in result:
print(row)
# ## slide:: p
# the fetchall() method is a shortcut to producing a list
# of all rows.
result = engine.execute("select * from employee")
print(result.fetchall())
# ## slide:: p
# The execute() method of Engine will *autocommit*
# statements like INSERT by default.
engine.execute("insert into employee_of_month (emp_name) values (:emp_name)",
emp_name='fred')
# ## slide:: p
# We can control the scope of connection using connect().
conn = engine.connect()
result = conn.execute("select * from employee")
result.fetchall()
conn.close()
# ## slide:: p
# to run several statements inside a transaction, Connection
# features a begin() method that returns a Transaction.
conn = engine.connect()
trans = conn.begin()
conn.execute("insert into employee (emp_name) values (:emp_name)",
emp_name="wendy")
conn.execute("update employee_of_month set emp_name = :emp_name",
emp_name="wendy")
trans.commit()
conn.close()
# ## slide:: p
# a context manager is supplied to streamline this process.
with engine.begin() as conn:
conn.execute("insert into employee (emp_name) values (:emp_name)",
emp_name="mary")
conn.execute("update employee_of_month set emp_name = :emp_name",
emp_name="mary")
# ## slide::
# ## title:: Exercises
# Assuming this table:
#
# CREATE TABLE employee (
# emp_id INTEGER PRIMARY KEY,
# emp_name VARCHAR(30)
# }
#
# And using the "engine.execute()" method to invoke a statement:
#
# 1. Execute an INSERT statement that will insert
# the row with emp_name='dilbert'.
# The primary key column can be omitted so that it is
# generated automatically.
#
# 2. SELECT all rows from the employee table.
# |
См.также
Для описания структуры базы данных используют 3 основных класса:
sqlalchemy.schema.Table
- таблицаsqlalchemy.schema.Column
- поле таблицыsqlalchemy.schema.MetaData
- список таблицА также типы полей описанные в модуле sqlalchemy.types
:
1 2 3 4 5 6 7 8 9 10 | from sqlalchemy import MetaData
from sqlalchemy import Table, Column
from sqlalchemy import Integer, String
metadata = MetaData()
user_table = Table('user', metadata,
Column('id', Integer, primary_key=True),
Column('name', String),
Column('fullname', String)
) |
Создание таблиц таким образом описывают структуру базы данных независимо от
объектно-реляционного отображения.
Объект sqlalchemy.schema.Table
представляет имя и другие атрибуты
текущей таблицы. Его коллекция объектов Column
представляет информацию об
именах и типах для определенных столбцов таблицы.
Дополнительно в описание схемы базы данных можно включить внешние ключи, индексы, последовательности и т.д.:
sqlalchemy.schema.ForeignKey
- внешние ключиsqlalchemy.schema.Index
- индексыsqlalchemy.schema.Sequence
- последовательностиВся информация о таблицах базы данных складывается в объект класса
sqlalchemy.schema.MetaData
. Получить список таблиц можно при помощи
атрибута sqlalchemy.schema.MetaData.tables
.
Базовые объекты пакета sqlalchemy.schema
Объекты Table
и Column
уникальны по сравнению со всеми остальными
объектами из пакета для работы со схемами, так как они используют двойное
наследование от объектов из пакетов sqlalchemy.schema
и
sqlalchemy.sql.expression
, работая не только как конструкции уровня
обработки схем, но также и как синтаксические единицы языка для создания
выражений SQL. Это отношение проиллюстрировано на
sqlalchemy_table_crossover
.
Двойная жизнь объектов Table и Column
1 2 3 4 5 | >>> user_table
Table('user', MetaData(bind=None),
Column('id', Integer(), table=<user>, primary_key=True, nullable=False),
Column('name', String(), table=<user>),
Column('fullname', String(), table=<user>), schema=None) |
1 2 | >>> user_table.name
'user' |
Поля таблицы хранятся в списке sqlalchemy.schema.Table.columns
или его
более коротком варианте sqlalchemy.schema.Table.c
.
1 2 3 4 5 6 7 8 9 10 | >>> user_table.c
<sqlalchemy.sql.expression.ImmutableColumnCollection object at 0x7fee7d18c450>
>>> print(user_table.c)
['user.id', 'user.name', 'user.fullname']
>>> user_table.c.id
Column('id', Integer(), table=<user>, primary_key=True, nullable=False)
>>> user_table.c.name
Column('name', String(), table=<user>)
>>> user_table.c.fullname
Column('fullname', String(), table=<user>) |
Сами поля тоже содержат информацию о себе, например в атрибутах name
и
type
.
1 2 3 4 5 6 7 8 9 10 11 12 13 | >>> user_table.c.id
Column('id', Integer(), table=<user>, primary_key=True, nullable=False)
>>> user_table.c.id.name
'id'
>>> user_table.c.id.type
Integer()
>>>
>>> user_table.c.name
Column('name', String(), table=<user>)
>>> user_table.c.name.name
'name'
>>> user_table.c.name.type
String() |
Первичные ключи таблицы можно получить при помощи атрибута
sqlalchemy.schema.Table.primary_key
1 2 3 4 5 6 | >>> user_table.primary_key
PrimaryKeyConstraint(Column('id', Integer(), table=<user>, primary_key=True, nullable=False))
>>> print(user_table.primary_key.columns)
['user.id']
>>> user_table.primary_key.columns.id
Column('id', Integer(), table=<user>, primary_key=True, nullable=False) |
Объект класса sqlalchemy.schema.Table
является частью механизма SQL
выражений в sqlalchemy
и содержит в себе множество вспомогательных
методов для построения SQL запросов:
sqlalchemy.schema.Table.select()
sqlalchemy.schema.Table.delete()
sqlalchemy.schema.Table.insert()
sqlalchemy.schema.Table.update()
sqlalchemy.schema.Table.join()
sqlalchemy.schema.Table.outerjoin()
1 2 3 4 5 6 7 8 9 10 11 12 | >>> print(user_table.select())
SELECT "user".id, "user".name, "user".fullname
FROM "user"
>>> print(user_table.delete())
DELETE FROM "user"
>>> print(user_table.insert())
INSERT INTO "user" (id, name, fullname) VALUES (:id, :name, :fullname)
>>> print(user_table.update())
UPDATE "user" SET id=:id, name=:name, fullname=:fullname |
Все таблицы из списка sqlalchemy.schema.MetaData
можно создать при
помощи метода sqlalchemy.schema.MetaData.create_all()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | >>> from sqlalchemy import create_engine
>>> engine = create_engine("sqlite://")
>>> metadata.create_all(engine)
[SQL]: PRAGMA table_info("user")
[SQL]: ()
[SQL]:
CREATE TABLE user (
id INTEGER NOT NULL,
name VARCHAR,
fullname VARCHAR,
PRIMARY KEY (id)
)
[SQL]: ()
[SQL]: COMMIT |
Для создания, удаления одной таблицы необходимо использовать методы класса
sqlalchemy.schema.Table
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | >>> user_table.drop(engine)
[SQL]:
DROP TABLE user
[SQL]: ()
[SQL]: COMMIT
>>> user_table.create(engine)
[SQL]:
CREATE TABLE user (
id INTEGER NOT NULL,
name VARCHAR,
fullname VARCHAR,
PRIMARY KEY (id)
)
[SQL]: ()
[SQL]: COMMIT |
Типы полей описаны в модуле sqlalchemy.types
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | >>> from sqlalchemy import String, Numeric, DateTime, Enum
>>> fancy_table = Table('fancy', metadata,
... Column('key', String(50), primary_key=True),
... Column('timestamp', DateTime),
... Column('amount', Numeric(10, 2)),
... Column('type', Enum('a', 'b', 'c'))
... )
>>> fancy_table.create(engine)
[SQL]:
CREATE TABLE fancy (
"key" VARCHAR(50) NOT NULL,
timestamp DATETIME,
amount NUMERIC(10, 2),
type VARCHAR(1),
PRIMARY KEY ("key"),
CHECK (type IN ('a', 'b', 'c'))
)
[SQL]: ()
[SQL]: COMMIT |
Индексы создаются при помощи параметра index
в классе
sqlalchemy.schema.Column
или при помощи объекта класса
sqlalchemy.schema.Index
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | >>> meta = MetaData()
>>> mytable = Table('mytable', meta,
... # an indexed column, with index "ix_mytable_col1"
... Column('col1', Integer, index=True),
...
... # a uniquely indexed column with index "ix_mytable_col2"
... Column('col2', Integer, index=True, unique=True),
...
... Column('col3', Integer),
... Column('col4', Integer),
...
... Column('col5', Integer),
... Column('col6', Integer),
... )
>>> from sqlalchemy import Index
>>> Index('idx_col34', mytable.c.col3, mytable.c.col4)
Index('idx_col34', Column('col3', Integer(), table=<mytable>), Column('col4', Integer(), table=<mytable>))
>>> Index('myindex', mytable.c.col5, mytable.c.col6, unique=True)
Index('myindex', Column('col5', Integer(), table=<mytable>), Column('col6', Integer(), table=<mytable>), unique=True)
>>> mytable.create(engine)
[SQL]:
CREATE TABLE mytable (
col1 INTEGER,
col2 INTEGER,
col3 INTEGER,
col4 INTEGER,
col5 INTEGER,
col6 INTEGER
)
[SQL]: ()
[SQL]: COMMIT
[SQL]: CREATE UNIQUE INDEX myindex ON mytable (col5, col6)
[SQL]: ()
[SQL]: COMMIT
[SQL]: CREATE INDEX idx_col34 ON mytable (col3, col4)
[SQL]: ()
[SQL]: COMMIT
[SQL]: CREATE INDEX ix_mytable_col1 ON mytable (col1)
[SQL]: ()
[SQL]: COMMIT
[SQL]: CREATE UNIQUE INDEX ix_mytable_col2 ON mytable (col2)
[SQL]: ()
[SQL]: COMMIT |
Внешние ключи обычно используют как ссылки на первичные ключи. Для описания
внешнего ключа в схеме нужно использовать класс
sqlalchemy.schema.ForeignKey
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | >>> from sqlalchemy import ForeignKey
>>> addresses_table = Table('address', metadata,
... Column('id', Integer, primary_key=True),
... Column('email_address', String(100), nullable=False),
... Column('user_id', Integer, ForeignKey('user.id'))
... )
>>> addresses_table.create(engine)
[SQL]:
CREATE TABLE address (
id INTEGER NOT NULL,
email_address VARCHAR(100) NOT NULL,
user_id INTEGER,
PRIMARY KEY (id),
FOREIGN KEY(user_id) REFERENCES user (id)
)
[SQL]: ()
[SQL]: COMMIT |
sqlalchemy.schema.ForeignKey
это более краткая запись следующей
конструкции sqlalchemy.schema.ForeignKeyConstraint
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | >>> from sqlalchemy import Unicode, UnicodeText, DateTime
>>> from sqlalchemy import ForeignKeyConstraint
>>> story_table = Table('story', metadata,
... Column('story_id', Integer, primary_key=True),
... Column('version_id', Integer, primary_key=True),
... Column('headline', Unicode(100), nullable=False),
... Column('body', UnicodeText)
... )
>>> published_table = Table('published', metadata,
... Column('pub_id', Integer, primary_key=True),
... Column('pub_timestamp', DateTime, nullable=False),
... Column('story_id', Integer),
... Column('version_id', Integer),
... ForeignKeyConstraint(
... ['story_id', 'version_id'],
... ['story.story_id', 'story.version_id'])
... )
>>> metadata.create_all(engine)
[SQL]: PRAGMA table_info("user")
[SQL]: ()
[SQL]: PRAGMA table_info("fancy")
[SQL]: ()
[SQL]: PRAGMA table_info("story")
[SQL]: ()
[SQL]: PRAGMA table_info("published")
[SQL]: ()
[SQL]: PRAGMA table_info("address")
[SQL]: ()
[SQL]:
CREATE TABLE story (
story_id INTEGER NOT NULL,
version_id INTEGER NOT NULL,
headline VARCHAR(100) NOT NULL,
body TEXT,
PRIMARY KEY (story_id, version_id)
)
[SQL]: ()
[SQL]: COMMIT
[SQL]:
CREATE TABLE published (
pub_id INTEGER NOT NULL,
pub_timestamp DATETIME NOT NULL,
story_id INTEGER,
version_id INTEGER,
PRIMARY KEY (pub_id),
FOREIGN KEY(story_id, version_id) REFERENCES story (story_id, version_id)
)
[SQL]: ()
[SQL]: COMMIT |
См.также
В информатике отражение или рефлексия (холоним интроспекции, англ. reflection) означает процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.
В SQLAlchemy рефлексия означает автоматическую загрузку схемы таблицы из
уже существующей базы данных. Реализуется через параметр autoload
в
конструкторе класса sqlalchemy.schema.Table.autoload
.
1 2 3 4 5 6 7 8 9 10 11 12 | >>> metadata2 = MetaData()
>>> user_reflected = Table('user', metadata2, autoload=True, autoload_with=engine)
[SQL]: PRAGMA table_info("user")
[SQL]: ()
[SQL]: PRAGMA foreign_key_list("user")
[SQL]: ()
[SQL]: PRAGMA index_list("user")
[SQL]: ()
>>> print(user_reflected.c)
['user.id', 'user.name', 'user.fullname'] |
Для отражения всех таблиц существует метод
sqlalchemy.schema.MetaData.reflect()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | >>> meta = MetaData()
>>> meta.reflect(bind=engine)
[SQL]: SELECT name FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type='table' ORDER BY name
[SQL]: ()
[SQL]: PRAGMA table_info("address")
[SQL]: ()
[SQL]: PRAGMA foreign_key_list("address")
[SQL]: ()
[SQL]: PRAGMA table_info("user")
[SQL]: ()
[SQL]: PRAGMA foreign_key_list("user")
[SQL]: ()
[SQL]: PRAGMA index_list("user")
[SQL]: ()
[SQL]: PRAGMA index_list("address")
[SQL]: ()
[SQL]: PRAGMA table_info("fancy")
[SQL]: ()
[SQL]: PRAGMA foreign_key_list("fancy")
[SQL]: ()
[SQL]: PRAGMA index_list("fancy")
[SQL]: ()
[SQL]: PRAGMA table_info("mytable")
[SQL]: ()
[SQL]: PRAGMA foreign_key_list("mytable")
[SQL]: ()
[SQL]: PRAGMA index_list("mytable")
[SQL]: ()
[SQL]: PRAGMA index_info("ix_mytable_col2")
[SQL]: ()
[SQL]: PRAGMA index_info("ix_mytable_col1")
[SQL]: ()
[SQL]: PRAGMA index_info("idx_col34")
[SQL]: ()
[SQL]: PRAGMA index_info("myindex")
[SQL]: ()
[SQL]: PRAGMA table_info("published")
[SQL]: ()
[SQL]: PRAGMA foreign_key_list("published")
[SQL]: ()
[SQL]: PRAGMA table_info("story")
[SQL]: ()
[SQL]: PRAGMA foreign_key_list("story")
[SQL]: ()
[SQL]: PRAGMA index_list("story")
[SQL]: ()
[SQL]: PRAGMA index_list("published")
[SQL]: ()
>>> new_user_table = meta.tables['user']
>>> new_fancy_table = meta.tables['fancy'] |
Интроспекция (англ. type introspection) в программировании — возможность в
некоторых объектно-ориентированных языках определить тип и структуру объекта во
время выполнения программы. В SQLAlchemy возможность анализа схемы базы
данных. Для анализа используется функция sqlalchemy.inspection.inspect()
.
1 2 3 4 5 6 7 8 | >>> from sqlalchemy import inspect
>>> inspector = inspect(engine)
>>>
>>> inspector.get_table_names()
[SQL]: SELECT name FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type='table' ORDER BY name
[SQL]: ()
[u'address', u'fancy', u'mytable', u'published', u'story', u'user'] |
1 2 3 4 5 | >>> inspector.get_columns('address')
[SQL]: PRAGMA table_info("address")
[SQL]: ()
[{'primary_key': 1, 'nullable': False, 'default': None, 'autoincrement': True, 'type': INTEGER(), 'name': u'id'}, {'primary_key': 0, 'nullable': False, 'default': None, 'autoincrement': True, 'type': VARCHAR(length=100), 'name': u'email_address'}, {'primary_key': 0, 'nullable': True, 'default': None, 'autoincrement': True, 'type': INTEGER(), 'name': u'user_id'}] |
1 2 3 4 5 | >>> inspector.get_foreign_keys('address')
[SQL]: PRAGMA foreign_key_list("address")
[SQL]: ()
[{'referred_table': u'user', 'referred_columns': [u'id'], 'referred_schema': None, 'name': None, 'constrained_columns': [u'user_id']}] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | # ## title:: Schema and MetaData
# The structure of a relational schema is represented in Python
# using MetaData, Table, and other objects.
from sqlalchemy import MetaData
from sqlalchemy import Table, Column
from sqlalchemy import Integer, String
metadata = MetaData()
user_table = Table('user', metadata,
Column('id', Integer, primary_key=True),
Column('name', String),
Column('fullname', String)
)
# Table provides a single point of information regarding
# the structure of a table in a schema.
user_table.name
# The .c. attribute of Table is an associative array
# of Column objects, keyed on name.
user_table.c.name
# It's a bit like a Python dictionary but not totally.
print(user_table.c)
# Column itself has information about each Column, such as
# name and type
user_table.c.name.name
user_table.c.name.type
# Table has other information available, such as the collection
# of columns which comprise the table's primary key.
user_table.primary_key
# The Table object is at the core of the SQL expression
# system - this is a quick preview of that.
print(user_table.select())
# Table and MetaData objects can be used to generate a schema
# in a database.
from sqlalchemy import create_engine
engine = create_engine("sqlite://")
metadata.create_all(engine)
# Types are represented using objects such as String, Integer,
# DateTime. These objects can be specified as "class keywords",
# or can be instantiated with arguments.
from sqlalchemy import String, Numeric, DateTime, Enum
fancy_table = Table('fancy', metadata,
Column('key', String(50), primary_key=True),
Column('timestamp', DateTime),
Column('amount', Numeric(10, 2)),
Column('type', Enum('a', 'b', 'c'))
)
fancy_table.create(engine)
# table metadata also allows for constraints and indexes.
# ForeignKey is used to link one column to a remote primary
# key.
from sqlalchemy import ForeignKey
addresses_table = Table('address', metadata,
Column('id', Integer, primary_key=True),
Column('email_address', String(100), nullable=False),
Column('user_id', Integer, ForeignKey('user.id'))
)
addresses_table.create(engine)
# ForeignKey is a shortcut for ForeignKeyConstraint,
# which should be used for composite references.
from sqlalchemy import Unicode, UnicodeText, DateTime
from sqlalchemy import ForeignKeyConstraint
story_table = Table('story', metadata,
Column('story_id', Integer, primary_key=True),
Column('version_id', Integer, primary_key=True),
Column('headline', Unicode(100), nullable=False),
Column('body', UnicodeText)
)
published_table = Table('published', metadata,
Column('pub_id', Integer, primary_key=True),
Column('pub_timestamp', DateTime, nullable=False),
Column('story_id', Integer),
Column('version_id', Integer),
ForeignKeyConstraint(
['story_id', 'version_id'],
['story.story_id', 'story.version_id'])
)
# create_all() by default checks for tables existing already
metadata.create_all(engine)
# ## title:: Exercises
# 1. Write a Table construct corresponding to this CREATE TABLE
# statement.
#
# CREATE TABLE network (
# network_id INTEGER PRIMARY KEY,
# name VARCHAR(100) NOT NULL,
# created_at DATETIME NOT NULL,
# owner_id INTEGER,
# FOREIGN KEY owner_id REFERENCES user(id)
# )
#
# 2. Then emit metadata.create_all(), which will
# emit CREATE TABLE for this table (it will skip
# those that already exist).
#
# The necessary types are imported here:
# ## title:: Reflection
# 'reflection' refers to loading Table objects based on
# reading from an existing database.
metadata2 = MetaData()
user_reflected = Table('user', metadata2, autoload=True, autoload_with=engine)
print(user_reflected.c)
# Information about a database at a more specific level is available
# using the Inspector object.
from sqlalchemy import inspect
inspector = inspect(engine)
# the inspector provides things like table names:
inspector.get_table_names()
# column information
inspector.get_columns('address')
# constraints
inspector.get_foreign_keys('address')
# ## title:: Exercises
#
# 1. Using 'metadata2', reflect the "network" table in the same way
# we just did 'user', then display the columns (or bonus, display
# just the column names)
#
# 2. Using "inspector", print a list of all table names that
# include a column called "story_id"
# |
См.также
В момент начала разработки SQLAlchemy способ генерации SQL-запросов не был ясен. Текстовый язык мог быть хорошим кандидатом; это стандартный подход, лежащий в основе таких широко известных инструментов объектно-реляционного отображения, как HQL из состава Hibernate. В случае использования языка программирования Python, однако, был доступен более занимательный вариант: использование объектов и выражений языка Python для генерации древовидных структур представления запросов, причем возможным было даже изменение назначения операторов языка Python с целью использования их для формирования SQL-запросов.
Хотя рассматриваемый инструмент и не был первым инструментом, выполняющим подобные функции, следует упомянуть о библиотеке SQLBuilder из состава SQLObject от Ian Bicking, которая была использована как образец при создании системы работы с объектами языка Python и операторами, используемыми в рамках языка формирования запросов SQLAlchemy. При использовании данного подхода объекты языка Python представляют лексические части SQL-запроса. Методы этих объектов, также как и перегружаемые операторы, позволяют генерировать новые унаследованные от существующих лексические конструкции. Наиболее часто используемым объектом является представляющий столбец объект «Column» - библиотека SQLObject будет представлять такие объекты в рамках класса объектно-реляционного отображения, используя пространство имен с доступом посредством атрибута .q; также в SQLAlchemy объявлен атрибут с именем .c. Этот атрибут .c на сегодняшний день поддерживается и используется для представления элементов основной части, подвергающихся выборке, таких, как объекты, представляющие таблицы и запросы выборки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | >>> from sqlalchemy import MetaData, Table, Column, String, Integer
>>> metadata = MetaData()
>>> user_table = Table('user', metadata,
... Column('id', Integer, primary_key=True),
... Column('username', String(50)),
... Column('fullname', String(50))
... )
>>> from sqlalchemy import create_engine
>>> engine = create_engine("sqlite://")
>>> metadata.create_all(engine)
[SQL]: PRAGMA table_info("user")
[SQL]: ()
[SQL]:
CREATE TABLE user (
id INTEGER NOT NULL,
username VARCHAR(50),
fullname VARCHAR(50),
PRIMARY KEY (id)
)
[SQL]: ()
[SQL]: COMMIT |
Каждая колонка в SQAlchemy является частью класса
sqlalchemy.sql.expression.ColumnElement
.
В примере ниже показывается соответствие SQL выражения с Python выражением
сравнения. Такое преобразование возможно при помощи реализации «магического»
Python метода sqlalchemy.sql.expression.ColumnElement.__eq__()
.
1 2 3 4 5 6 7 8 | >>> user_table.c.username
Column('username', String(length=50), table=<user>)
>>>
>>> user_table.c.username == 'ed'
<sqlalchemy.sql.expression.BinaryExpression object at 0x7fb829e60a90>
>>>
>>> str(user_table.c.username == 'ed')
'"user".username = :username_1' |
Комбинация нескольких выражений
1 2 3 4 | >>> print(
... (user_table.c.username == 'ed') | (user_table.c.username == 'jack')
... )
"user".username = :username_1 OR "user".username = :username_2 |
SQL операторы OR и AND соответствуют побитовым операторам в Python |
и
&
или функциям sqlalchemy.sql.expression.or_()
и
sqlalchemy.sql.expression.and_()
.
1 2 3 4 5 6 7 8 9 10 11 12 | >>> from sqlalchemy import and_, or_
>>> print(
... and_(
... user_table.c.fullname == 'ed jones',
... or_(
... user_table.c.username == 'ed',
... user_table.c.username == 'jack'
... )
... )
... )
"user".fullname = :fullname_1 AND ("user".username = :username_1 OR "user".username = :username_2) |
Многие операторы наследуются из класса
sqlalchemy.sql.operators.ColumnOperators
SQLAlchemy оператор | Название оператора | Python оператор |
---|---|---|
sqlalchemy.sql.operators.ColumnOperators.__add__() |
add | + |
sqlalchemy.sql.operators.ColumnOperators.__and__() |
and | & |
sqlalchemy.sql.expression.ColumnElement.__eq__() |
equal | == |
sqlalchemy.sql.operators.ColumnOperators.__ge__() |
greater equal | >= |
sqlalchemy.sql.operators.ColumnOperators.__gt__() |
greater than | > |
sqlalchemy.sql.expression.ColumnElement.__le__() |
less equal | <= |
sqlalchemy.sql.expression.ColumnElement.__lt__() |
less than | < |
sqlalchemy.sql.expression.ColumnElement.__ne__() |
not equal | != |
sqlalchemy.sql.operators.ColumnOperators.__or__() |
or | | |
sqlalchemy.sql.operators.ColumnOperators.in_() |
in | in |
sqlalchemy.sql.operators.ColumnOperators.notin_() |
not in | not in |
1 2 3 4 5 6 7 8 9 10 11 12 | >>> print(user_table.c.id == 5)
"user".id = :id_1
>>> print(user_table.c.id >= 5)
"user".id >= :id_1
>>> print(user_table.c.id > 5)
"user".id > :id_1
>>> print(user_table.c.id <= 5)
"user".id <= :id_1
>>> print(user_table.c.id < 5)
"user".id < :id_1
>>> print(user_table.c.id != 5)
"user".id != :id_1 |
Сравнение с None
преобразуется в SQL конструкцию IS NULL
.
1 2 3 4 | >>> print(user_table.c.id != None)
"user".id IS NOT NULL
>>> print(user_table.c.id == None)
"user".id IS NULL |
1 2 3 4 | >>> print((user_table.c.id == None) | (user_table.c.fullname == 'Vasya'))
"user".id IS NULL OR "user".fullname = :fullname_1
>>> print((user_table.c.id == None) & (user_table.c.fullname == 'Vasya'))
"user".id IS NULL AND "user".fullname = :fullname_1 |
Арифметический оператор сложения
1 2 | >>> print(user_table.c.id + 5)
"user".id + :id_1 |
Python оператор сложения автоматически определяет строки и подставляет SQL
оператор конкатенации ||
1 2 | >>> print(user_table.c.fullname + "some name")
"user".fullname || :fullname_1 |
1 2 3 4 5 | >>> print(user_table.c.username.in_(["wendy", "mary", "ed"]))
"user".username IN (:username_1, :username_2, :username_3)
>>> print(user_table.c.username.notin_(["wendy", "mary", "ed"]))
"user".username NOT IN (:username_1, :username_2, :username_3) |
Скомпилированное выражение является объектом класса
sqlalchemy.sql.compiler.SQLCompiler
Диалекты разных СУБД описаны в модулях:
sqlalchemy.dialects.firebird
sqlalchemy.dialects.mssql
sqlalchemy.dialects.mysql
sqlalchemy.dialects.oracle
sqlalchemy.dialects.postgresql
sqlalchemy.dialects.sqlite
sqlalchemy.dialects.sybase
1 2 3 | >>> from sqlalchemy.dialects import sqlite
>>> print(expression.compile(dialect=sqlite.dialect()))
user.username = ? |
1 2 3 4 5 | >>> expression = user_table.c.username == 'ed'
>>> from sqlalchemy.dialects import mysql
>>> print(expression.compile(dialect=mysql.dialect()))
user.username = %s |
1 2 3 | >>> from sqlalchemy.dialects import postgresql
>>> print(expression.compile(dialect=postgresql.dialect()))
"user".username = %(username_1)s |
1 2 3 | >>> from sqlalchemy.dialects import firebird
>>> print(expression.compile(dialect=firebird.dialect()))
"user".username = :username_1 |
1 2 3 | >>> from sqlalchemy.dialects import mssql
>>> print(expression.compile(dialect=mssql.dialect()))
[user].username = :username_1 |
При компиляции SQL выражения буквенные значения преобразуются в параметры, они
доступны через атрибут sqlalchemy.sql.compiler.SQLCompiler.params
1 2 3 4 | >>> expression = user_table.c.username == 'ed'
>>> compiled = expression.compile()
>>> compiled.params
{u'username_1': 'ed'} |
Параметры извлекаются при выполнении запроса
1 2 3 4 5 6 7 8 | >>> engine.execute(
... user_table.select().where(user_table.c.username == 'ed')
... )
[SQL]: SELECT user.id, user.username, user.fullname
FROM user
WHERE user.username = ?
[SQL]: ('ed',)
<sqlalchemy.engine.result.ResultProxy object at 0x7f3714aec3d0> |
SQL запросы INSERT можно формировать при помощи метода
sqlalchemy.schema.Table.insert()
.
1 2 3 4 5 6 7 | >>> insert_stmt = user_table.insert().values(username='ed', fullname='Ed Jones')
>>> conn = engine.connect()
>>> result = conn.execute(insert_stmt)
[SQL]: INSERT INTO user (username, fullname) VALUES (?, ?)
[SQL]: ('ed', 'Ed Jones')
[SQL]: COMMIT |
Результат выполнения содержит в себе значение primary_key
добавленной
записи в БД.
1 2 | >>> result.inserted_primary_key
[1] |
Запись нескольких строк в таблицу за раз.
1 2 3 4 5 6 7 8 9 | >>> conn.execute(user_table.insert(), [
... {'username': 'jack', 'fullname': 'Jack Burger'},
... {'username': 'wendy', 'fullname': 'Wendy Weathersmith'}
>>> ])
[SQL]: INSERT INTO user (username, fullname) VALUES (?, ?)
[SQL]: (('jack', 'Jack Burger'), ('wendy', 'Wendy Weathersmith'))
[SQL]: COMMIT
<sqlalchemy.engine.result.ResultProxy object at 0x7f3714aec810> |
SQL запросы SELECT
можно формировать при помощи функции
sqlalchemy.sql.expression.select()
.
1 2 3 4 5 6 7 8 9 10 11 12 | >>> from sqlalchemy import select
>>> select_stmt = select([user_table.c.username, user_table.c.fullname]).\
... where(user_table.c.username == 'ed')
>>> result = conn.execute(select_stmt)
>>> for row in result:
... print(row)
[SQL]: SELECT user.username, user.fullname
FROM user
WHERE user.username = ?
[SQL]: ('ed',)
(u'ed', u'Ed Jones') |
Выбор всех полей таблицы.
sqlalchemy.engine.ResultProxy.fetchall()
1 2 3 4 5 6 | >>> select_stmt = select([user_table])
>>> conn.execute(select_stmt).fetchall()
FROM user
[SQL]: ()
[(1, u'ed', u'Ed Jones'), (2, u'jack', u'Jack Burger'), (3, u'wendy', u'Wendy Weathersmith'), (4, u'jack', u'Jack Burger'), (5, u'wendy', u'Wendy Weathersmith')] |
Условие WHERE можно указать как метод класса Select
sqlalchemy.sql.expression.Select.where()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | >>> select_stmt = select([user_table]).\
... where(
... or_(
... user_table.c.username == 'ed',
... user_table.c.username == 'wendy'
... )
... )
>>> conn.execute(select_stmt).fetchall()
[press return to run code]
[SQL]: SELECT user.id, user.username, user.fullname
FROM user
WHERE user.username = ? OR user.username = ?
[SQL]: ('ed', 'wendy')
[(1, u'ed', u'Ed Jones'), (3, u'wendy', u'Wendy Weathersmith'), (5, u'wendy', u'Wendy Weathersmith')] |
Несколько вызовов метода where
сливаются в одно SQL выражения при помощи
оператора AND
.
1 2 3 4 5 6 7 8 9 | >>> select_stmt = select([user_table]).\
... where(user_table.c.username == 'ed').\
... where(user_table.c.fullname == 'ed jones')
>>> conn.execute(select_stmt).fetchall()
[SQL]: SELECT user.id, user.username, user.fullname
FROM user
WHERE user.username = ? AND user.fullname = ?
[SQL]: ('ed', 'ed jones') |
ORDER BY
соответствует методу
sqlalchemy.sql.expression.Select.order_by()
1 2 3 4 5 6 7 8 | >>> select_stmt = select([user_table]).\
... order_by(user_table.c.username)
>>> print(conn.execute(select_stmt).fetchall())
[SQL]: SELECT user.id, user.username, user.fullname
FROM user ORDER BY user.username
[SQL]: ()
[(1, u'ed', u'Ed Jones'), (2, u'jack', u'Jack Burger'), (4, u'jack', u'Jack Burger'), (3, u'wendy', u'Wendy Weathersmith'), (5, u'wendy', u'Wendy Weathersmith')] |
FOREIGN KEY
соответствует классу sqlalchemy.schema.ForeignKey
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | >>> from sqlalchemy import ForeignKey
>>> address_table = Table("address", metadata,
... Column('id', Integer, primary_key=True),
... Column('user_id', Integer, ForeignKey('user.id'),
... nullable=False),
... Column('email_address', String(100), nullable=False)
... )
>>> metadata.create_all(engine)
[SQL]: PRAGMA table_info("user")
[SQL]: ()
[SQL]: PRAGMA table_info("address")
[SQL]: ()
[SQL]:
CREATE TABLE address (
id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
email_address VARCHAR(100) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(user_id) REFERENCES user (id)
)
[SQL]: ()
[SQL]: COMMIT
>>> conn.execute(address_table.insert(), [
... {"user_id": 1, "email_address": "ed@ed.com"},
... {"user_id": 1, "email_address": "ed@gmail.com"},
... {"user_id": 2, "email_address": "jack@yahoo.com"},
... {"user_id": 3, "email_address": "wendy@gmail.com"},
>>> ])
[SQL]: INSERT INTO address (user_id, email_address) VALUES (?, ?)
[SQL]: ((1, 'ed@ed.com'), (1, 'ed@gmail.com'), (2, 'jack@yahoo.com'), (3, 'wendy@gmail.com'))
[SQL]: COMMIT
<sqlalchemy.engine.result.ResultProxy object at 0x7f3714b0b9d0> |
Два объекта sqlalchemy.schema.Table
могут быть связанны при помощи
метода sqlalchemy.schema.Table.join()
.
1 2 3 4 5 6 7 8 | >>> join_obj = user_table.join(address_table,
... user_table.c.id == address_table.c.user_id)
>>> print(join_obj)
"user" JOIN address ON "user".id = address.user_id
>>> ForeignKey
<class 'sqlalchemy.schema.ForeignKey'>
>>> |
Условие ON подставляется автоматически
1 2 3 | >>> join_obj = user_table.join(address_table)
>>> print(join_obj)
"user" JOIN address ON "user".id = address.user_id |
Выполнить SQL запрос следующей конструкции SELECT FROM JOIN
можно при
помощи метода sqlalchemy.sql.expression.Select.select_from()
.
1 2 3 4 5 6 7 | >>> select_stmt = select([user_table, address_table]).select_from(join_obj)
>>> conn.execute(select_stmt).fetchall()
[SQL]: SELECT user.id, user.username, user.fullname, address.id, address.user_id, address.email_address
FROM user JOIN address ON user.id = address.user_id
[SQL]: ()
[(1, u'ed', u'Ed Jones', 1, 1, u'ed@ed.com'), (1, u'ed', u'Ed Jones', 2, 1, u'ed@gmail.com'), (2, u'jack', u'Jack Burger', 3, 2, u'jack@yahoo.com'), (3, u'wendy', u'Wendy Weathersmith', 4, 3, u'wendy@gmail.com')] |
1 2 3 4 5 6 7 8 9 10 11 | >>> select_stmt = select([user_table]).where(user_table.c.username == 'ed')
>>> print(
... select([select_stmt.c.username]).
... where(select_stmt.c.username == 'ed')
... )
SELECT username
FROM (SELECT "user".id AS id, "user".username AS username, "user".fullname AS fullname
FROM "user"
WHERE "user".username = :username_1)
WHERE username = :username_2 |
Конструкция AS
добавляется при помощи метода
sqlalchemy.sql.expression.Select.alias()
.
1 2 3 4 5 6 7 8 9 10 | >>> select_alias = select_stmt.alias()
>>> print(
... select([select_alias.c.username]).
... where(select_alias.c.username == 'ed')
... )
SELECT anon_1.username
FROM (SELECT "user".id AS id, "user".username AS username, "user".fullname AS fullname
FROM "user"
WHERE "user".username = :username_1) AS anon_1
WHERE anon_1.username = :username_2 |
Примечание
Более сложный пример
Подзапрос использует дополнительно конструкции GROUP BY
и count()
:
1 2 3 4 5 6 7 8 9 10 | >>> from sqlalchemy import func
>>> address_subq = select([
... address_table.c.user_id,
... func.count(address_table.c.id).label('count')
... ]).\
... group_by(address_table.c.user_id).\
... alias()
>>> print(address_subq)
SELECT address.user_id, count(address.id) AS count
FROM address GROUP BY address.user_id |
Вложенный запрос применяет алиас подзапроса.
1 2 3 4 5 6 7 8 9 10 11 12 13 | >>> username_plus_count = select([
... user_table.c.username,
... address_subq.c.count
... ]).select_from(
... user_table.join(address_subq)
... ).order_by(user_table.c.username)
>>> conn.execute(username_plus_count).fetchall()
[SQL]: SELECT user.username, anon_1.count
FROM user JOIN (SELECT address.user_id AS user_id, count(address.id) AS count
FROM address GROUP BY address.user_id) AS anon_1 ON user.id = anon_1.user_id ORDER BY user.username
[SQL]: ()
[(u'ed', 2), (u'jack', 1), (u'wendy', 1)] |
Скаля́р (от лат. scalaris — ступенчатый) — величина, каждое значение которой может быть выражено одним числом. В математике под «числами» могут подразумеваться элементы произвольного поля, тогда когда в физике имеются в виду действительные или комплексные числа. О функции, принимающей скалярные значения, говорят как о скалярной функции.
—WikiPedia
Скалярный SELECT вернет только одно поле одной строки.
1 2 3 4 5 6 7 8 | >>> address_sel = select([
... func.count(address_table.c.id)
... ]).\
... where(user_table.c.id == address_table.c.user_id)
>>> print(address_sel)
SELECT count(address.id) AS count_1
FROM address, "user"
WHERE "user".id = address.user_id |
Чтобы его вызвать в подзапросе нужно использовать метод
sqlalchemy.sql.expression.Select.as_scalar()
1 2 3 4 5 6 7 8 9 | >>> select_stmt = select([user_table.c.username, address_sel.as_scalar()])
>>> conn.execute(select_stmt).fetchall()
[SQL]: SELECT user.username, (SELECT count(address.id) AS count_1
FROM address
WHERE user.id = address.user_id) AS anon_1
FROM user
[SQL]: ()
[(u'ed', 2), (u'jack', 1), (u'wendy', 1), (u'jack', 0), (u'wendy', 0)] |
sqlalchemy.schema.Table.update()
1 2 3 4 5 6 7 8 | >>> update_stmt = address_table.update().\
... values(email_address="jack@msn.com").\
... where(address_table.c.email_address == "jack@yahoo.com")
>>> result = conn.execute(update_stmt)
[SQL]: UPDATE address SET email_address=? WHERE address.email_address = ?
[SQL]: ('jack@msn.com', 'jack@yahoo.com')
[SQL]: COMMIT |
UPDATE запрос значение которого строится из значения полей текущей записи
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | >>> update_stmt = user_table.update().\
... values(fullname=user_table.c.username +
... " " + user_table.c.fullname)
>>> result = conn.execute(update_stmt)
[SQL]: UPDATE user SET fullname=(user.username || ? || user.fullname)
[SQL]: (' ',)
[SQL]: COMMIT
>>> conn.execute(select([user_table])).fetchall()
[SQL]: SELECT user.id, user.username, user.fullname
FROM user
[SQL]: ()
[(1, u'ed', u'ed Ed Jones'), (2, u'jack', u'jack Jack Burger'), (3, u'wendy', u'wendy Wendy Weathersmith'), (4, u'jack', u'jack Jack Burger'), (5, u'wendy', u'wendy Wendy Weathersmith')] |
sqlalchemy.schema.Table.delete()
1 2 3 4 5 6 7 | >>> delete_stmt = address_table.delete().\
... where(address_table.c.email_address == "ed@ed.com")
>>> result = conn.execute(delete_stmt)
[SQL]: DELETE FROM address WHERE address.email_address = ?
[SQL]: ('ed@ed.com',)
[SQL]: COMMIT |
Количество удаленных строк (применимо и для UPDATE).
1 2 | >>> result.rowcount
1 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 | # ## slide::
# ## title:: SQL Expression Language
# We begin with a Table object
from sqlalchemy import MetaData, Table, Column, String, Integer
metadata = MetaData()
user_table = Table('user', metadata, Column('id', Integer,
primary_key=True),
Column('username', String(50)),
Column('fullname', String(50)))
# ## slide:: p
# new SQLite database and generate the table.
from sqlalchemy import create_engine
engine = create_engine("sqlite://")
metadata.create_all(engine)
# ## slide::
# as we saw earlier, Table has a collection of Column objects,
# which we can access via table.c.<columnname>
user_table.c.username
# ## slide::
# Column is part of a class known as "ColumnElement",
# which exhibit custom Python expression behavior.
user_table.c.username == 'ed'
# ## slide:: i
# They become SQL when evaluated as a string.
str(user_table.c.username == 'ed')
# ## slide::
# ColumnElements can be further combined to produce more ColumnElements
print((user_table.c.username == 'ed') | (user_table.c.username == 'jack'))
# ## slide::
# OR and AND are available with |, &, or or_() and and_()
from sqlalchemy import and_, or_
print(and_(user_table.c.fullname == 'ed jones',
or_(user_table.c.username == 'ed',
user_table.c.username == 'jack')))
# ## slide::
# comparison operators
print(user_table.c.id > 5)
# ## slide::
# Compare to None produces IS NULL
print(user_table.c.fullname == None) # noqa
# ## slide::
# "+" might mean "addition"....
print(user_table.c.id + 5)
# ## slide:: i
# ...or might mean "string concatenation"
print(user_table.c.fullname + "some name")
# ## slide::
# an IN
print(user_table.c.username.in_(["wendy", "mary", "ed"]))
# ## slide::
# Expressions produce different strings according to *dialect*
# objects.
expression = user_table.c.username == 'ed'
# ## slide:: i
# MySQL....
from sqlalchemy.dialects import mysql
print(expression.compile(dialect=mysql.dialect()))
# ## slide:: i
# PostgreSQL...
from sqlalchemy.dialects import postgresql
print(expression.compile(dialect=postgresql.dialect()))
# ## slide::
# the Compiled object also converts literal values to "bound"
# parameters.
compiled = expression.compile()
compiled.params
# ## slide::
# The "bound" parameters are extracted when we execute()
engine.execute(user_table.select().where(user_table.c.username == 'ed'))
# ## slide::
# ## title:: Exercises
# Produce these expressions using "user_table.c.fullname",
# "user_table.c.id", and "user_table.c.username":
#
# 1. user.fullname = 'ed'
#
# 2. user.fullname = 'ed' AND user.id > 5
#
# 3. user.username = 'edward' OR (user.fullname = 'ed' AND user.id > 5)
#
# ## slide:: p
# we can insert data using the insert() construct
insert_stmt = user_table.insert().values(username='ed', fullname='Ed Jones')
conn = engine.connect()
result = conn.execute(insert_stmt)
# ## slide:: i
# executing an insert() gives us the "last inserted id"
result.inserted_primary_key
# ## slide:: p
# insert() and other DML can run multiple parameters at once.
conn.execute(user_table.insert(),
[{'username': 'jack',
'fullname': 'Jack Burger'},
{'username': 'wendy',
'fullname': 'Wendy Weathersmith'}])
# ## slide:: p
# select() is used to produce any SELECT statement.
from sqlalchemy import select
select_stmt = select([user_table.c.username, user_table.c.fullname]).\
where(user_table.c.username == 'ed')
result = conn.execute(select_stmt)
for row in result:
print(row)
# ## slide:: p
# select all columns from a table
select_stmt = select([user_table])
conn.execute(select_stmt).fetchall()
# ## slide:: p
# specify a WHERE clause
select_stmt = select([user_table]).\
where(
or_(
user_table.c.username == 'ed',
user_table.c.username == 'wendy'
)
)
conn.execute(select_stmt).fetchall()
# ## slide:: p
# specify multiple WHERE, will be joined by AND
select_stmt = select([user_table]).\
where(user_table.c.username == 'ed').\
where(user_table.c.fullname == 'ed jones')
conn.execute(select_stmt).fetchall()
# ## slide:: p
# ordering is applied using order_by()
select_stmt = select([user_table]).\
order_by(user_table.c.username)
print(conn.execute(select_stmt).fetchall())
# ## slide::
# ## title:: Exercises
# 1. use user_table.insert() and "r = conn.execute()" to emit this
# statement:
#
# INSERT INTO user (username, fullname) VALUES ('dilbert', 'Dilbert Jones')
#
# 2. What is the value of 'user.id' for the above INSERT statement?
#
# 3. Using "select([user_table])", execute this SELECT:
#
# SELECT id, username, fullname FROM user WHERE username = 'wendy' OR
# username = 'dilbert' ORDER BY fullname
#
#
# ## slide:: p
# ## title:: Joins / Foreign Keys
# We create a new table to illustrate multi-table operations
from sqlalchemy import ForeignKey
address_table = Table("address", metadata, Column('id', Integer,
primary_key=True),
Column('user_id', Integer, ForeignKey('user.id'),
nullable=False),
Column('email_address', String(100),
nullable=False))
metadata.create_all(engine)
# ## slide:: p
# data
conn.execute(address_table.insert(),
[{"user_id": 1,
"email_address": "ed@ed.com"},
{"user_id": 1,
"email_address": "ed@gmail.com"},
{"user_id": 2,
"email_address": "jack@yahoo.com"},
{"user_id": 3,
"email_address": "wendy@gmail.com"}, ])
# ## slide::
# two Table objects can be joined using join()
#
# <left>.join(<right>, [<onclause>]).
join_obj = user_table.join(address_table,
user_table.c.id == address_table.c.user_id)
print(join_obj)
# ## slide::
# ForeignKey allows the join() to figure out the ON clause automatically
join_obj = user_table.join(address_table)
print(join_obj)
# ## slide:: pi
# to SELECT from a JOIN, use select_from()
select_stmt = select([user_table, address_table]).select_from(join_obj)
conn.execute(select_stmt).fetchall()
# ## slide::
# the select() object is a "selectable" just like Table.
# it has a .c. attribute also.
select_stmt = select([user_table]).where(user_table.c.username == 'ed')
print(select([select_stmt.c.username]).where(select_stmt.c.username == 'ed'))
# ## slide::
# In SQL, a "subquery" is usually an alias() of a select()
select_alias = select_stmt.alias()
print(select([select_alias.c.username]).where(select_alias.c.username == 'ed'))
# ## slide::
# A subquery against "address" counts addresses per user:
from sqlalchemy import func
address_subq = select([
address_table.c.user_id,
func.count(address_table.c.id).label('count')
]).\
group_by(address_table.c.user_id).\
alias()
print(address_subq)
# ## slide:: i
# we use join() to link the alias() with another select()
username_plus_count = select([
user_table.c.username, address_subq.c.count
]).select_from(user_table.join(address_subq)).order_by(user_table.c.username)
# ## slide:: i
conn.execute(username_plus_count).fetchall()
# ## slide::
# ## title:: Exercises
# Produce this SELECT:
#
# SELECT fullname, email_address FROM user JOIN address
# ON user.id = address.user_id WHERE username='ed'
# ORDER BY email_address
#
# ## slide::
# ## title:: Scalar selects, updates, deletes
# a *scalar select* returns exactly one row and one column
address_sel = select([
func.count(address_table.c.id)
]).\
where(user_table.c.id == address_table.c.user_id)
print(address_sel)
# ## slide:: ip
# scalar selects can be used in column expressions,
# specify it using as_scalar()
select_stmt = select([user_table.c.username, address_sel.as_scalar()])
conn.execute(select_stmt).fetchall()
# ## slide:: p
# to round out INSERT and SELECT, this is an UPDATE
update_stmt = address_table.update().\
values(email_address="jack@msn.com").\
where(address_table.c.email_address == "jack@yahoo.com")
result = conn.execute(update_stmt)
# ## slide:: p
# an UPDATE can also use expressions based on other columns
update_stmt = user_table.update().\
values(fullname=user_table.c.username +
" " + user_table.c.fullname)
result = conn.execute(update_stmt)
# ## slide:: i
conn.execute(select([user_table])).fetchall()
# ## slide:: p
# and this is a DELETE
delete_stmt = address_table.delete().\
where(address_table.c.email_address == "ed@ed.com")
result = conn.execute(delete_stmt)
# ## slide:: i
# UPDATE and DELETE have a "rowcount", number of rows matched
# by the WHERE clause.
result.rowcount
# ## slide::
# ## title:: Exercises
# 1. Execute this UPDATE - keep the "result" that's returned
#
# UPDATE user SET fullname='Ed Jones' where username='ed'
#
# 2. how many rows did the above statement update?
#
# 3. Tricky bonus! Combine update() along with select().as_scalar()
# to execute this UPDATE:
#
# UPDATE user SET fullname=fullname ||
# (select email_address FROM address WHERE user_id=user.id)
# WHERE username IN ('jack', 'wendy')
#
# ## slide:: |
См.также
Переключим наше внимание на объектно-реляционное отображение. Первой целью является использование описанной нами системы таблиц метаданных для предоставления возможности переноса функций заданного пользователем класса на коллекцию столбцов в таблице базы данных. Второй целью является предоставление возможности описания отношений между заданными пользователем классами, которые будут основываться на отношениях между таблицами в базе данных.
В SQLAlchemy такая связь называется «отображением», что соответствует широко известному шаблону проектирования с названием «DataMapper», описанному в книге Martin Flower с названием Patterns of Enterprise Application Architecture.
В целом, система объектно-реляционного отображения SQLAlchemy была разработана с применением большого количества приемов, которые описал в своей книге Martin Flower. Она также подверглась значительному влиянию со стороны известной системы реляционного отображения Hibernate для языка программирования Java и продукта SQLObject для языка программирования Python от Ian Bicking.
Объект класса sqlalchemy.orm.mapper.Mapper
связывает колонки из схемы
таблицы и атрибуты Python класса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | # -*- coding: utf-8 -*-
from sqlalchemy import Table, MetaData, Column, Integer, String, ForeignKey
from sqlalchemy.orm import mapper, relationship
metadata = MetaData()
user = Table('user', metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50)),
Column('fullname', String(50)),
Column('password', String(12))
)
address = Table('address', metadata,
Column('id', Integer, primary_key=True),
Column('user_id', Integer, ForeignKey('user.id')),
Column('email_address', String(50))
)
class User(object):
pass
class Address(object):
pass
print(dir(User))
mapper(
User, user,
properties={
'addresses': relationship(Address, backref='user',
order_by=address.c.id)
})
print(dir(User))
mapper(Address, address) |
>>> ['__class__', '__delattr__', '__dict__', '__doc__', '__format__',
'__getattribute__', '__hash__', '__init__', '__module__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__']
>>> ['__class__', '__delattr__', '__dict__', '__doc__', '__format__',
'__getattribute__', '__hash__', '__init__', '__module__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__', '_sa_class_manager',
'addresses', 'fullname', 'id', 'name', 'password']
Каждый класс, представляющий таблицу в БД, должен наследоваться от базового
класса который создается при помощи функции
sqlalchemy.ext.declarative.declarative_base()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | >>> from sqlalchemy.ext.declarative import declarative_base
>>> Base = declarative_base()
>>> from sqlalchemy import Column, Integer, String
>>> class User(Base):
... __tablename__ = 'user'
... id = Column(Integer, primary_key=True)
... name = Column(String)
... fullname = Column(String)
... def __repr__(self):
... return "<User(%r, %r)>" % (
... self.name, self.fullname
... ) |
Для каждого класса унаследованного от базового автоматически создается схема
таблицы (объект класса sqlalchemy.schema.Table
) и привязывается к нему
через атрибут __table__
.
1 2 3 4 | >>> User.__table__
Table('user', MetaData(bind=None), Column('id', Integer(), table=<user>,
primary_key=True, nullable=False), Column('name', String(), table=<user>),
Column('fullname', String(), table=<user>), schema=None) |
Любой класс таблицы автоматически ассоциируется с объектом
sqlalchemy.schema.Table
, который автоматически добавляется в список
sqlalchemy.schema.MetaData
. Базовый класс Base
, созданный при
помощи функции sqlalchemy.ext.declarative.declarative_base()
, является
более высокоуровневой абстракцией над sqlalchemy.schema.MetaData
,
которая позволяет описывать таблицы декларативным способом. Таким образом все
классы-таблицы имеют свою схему, которая хранится в атрибуте metadata
базового класса Base
:
1 2 3 4 5 6 | >>> Base.metadata
MetaData(bind=None)
>>> Base.metadata.tables.items()
[('user', Table('user', MetaData(bind=None), Column('id', Integer(),
table=<user>, primary_key=True, nullable=False), Column('name', String(),
table=<user>), Column('fullname', String(), table=<user>), schema=None))] |
Благодаря тому что Base
содержит в себе объект
sqlalchemy.schema.MetaData
, вы можете пользоваться всеми его
возможностями.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | >>> from sqlalchemy import create_engine
>>> engine = create_engine('sqlite://')
>>> Base.metadata.create_all(engine)
[SQL]: SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
[SQL]: ()
[SQL]: SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
[SQL]: ()
[SQL]: PRAGMA table_info("user")
[SQL]: ()
[SQL]:
CREATE TABLE user (
id INTEGER NOT NULL,
name VARCHAR,
fullname VARCHAR,
PRIMARY KEY (id)
)
[SQL]: ()
[SQL]: COMMIT |
Объект класса sqlalchemy.orm.mapper.Mapper
связывает колонки из схемы
таблицы и атрибуты из класса таблицы унаследованного от Base
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | # -*- coding: utf-8 -*-
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String)
fullname = Column(String)
password = Column(String)
addresses = relationship("Address", backref="user",
order_by="Address.id")
class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
user_id = Column(ForeignKey('user.id'))
email_address = Column(String)
address1 = Address(email_address="vas@example.com")
address2 = Address(email_address="vas2@example.com")
address3 = Address(email_address="vasya@example.com")
print("Mapper relationship: " + str(User.__mapper__.relationships))
print("Mapper columns: " + str(User.__mapper__.c.items()))
print
user1 = User(name="Вася")
user1.addresses = [address1, address2, address3]
print("User1 columns: " + str(user1.__table__.c.items()))
print
print(address1.user.name) |
1 2 3 4 5 6 7 8 9 10 11 12 | Mapper relationship: <sqlalchemy.util._collections.ImmutableProperties object at 0x7ffeae32da28>
Mapper columns: [('id', Column('id', Integer(), table=<user>,
primary_key=True, nullable=False)), ('name', Column('name', String(),
table=<user>)), ('fullname', Column('fullname', String(), table=<user>)),
('password', Column('password', String(), table=<user>))]
User1 columns: [('id', Column('id', Integer(), table=<user>,
primary_key=True, nullable=False)), ('name', Column('name', String(),
table=<user>)), ('fullname', Column('fullname', String(), table=<user>)),
('password', Column('password', String(), table=<user>))]
Вася |
Декларативно описанный класс таблицы содержит в себе конструктор по умолчанию.
1 | >>> ed_user = User(name='ed', fullname='Edward Jones') |
Можно переопределить конструктор вручную
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class User(Base):
__tablename__ = 'user'
def __init__(self, name, fullname):
self.name = name
self.fullname = fullname
id = Column(Integer, primary_key=True)
name = Column(String)
fullname = Column(String)
password = Column(String)
addresses = relationship("Address", backref="user",
order_by="Address.id") |
Поле User.id
является первичным ключом, если его значение не указанно явно
или такой id
не существует в БД, то объект считается новым. После записи
объекта в БД, значение поля id
автоматически присваивается.
1 2 3 4 | >>> print(ed_user.name, ed_user.fullname)
('ed', 'Edward Jones')
>>> print(ed_user.id)
None |
См.также
Сессии являются более абстрактным уровнем над механизмом соединения с СУБД
sqlalchemy.engine.Engine
. Они включают в себя функции хранения
состояния объектов таблиц и записи этого состояния, по требованию, в БД.
Примечание
Анологию сессий в SQLAlchemy можно провести с системой контроля версий Git.
Ресурсы
git
управляет файлами.SQAlchemy
манипулирует объектам таблиц (будущие записи в таблицах).Состояние ресурсов
Область подготовленных файлов (staging area) — это обычный файл, обычно хранящийся в каталоге Git, который содержит информацию о том, какие файлы должны войти в следующий коммит.
git add README.txt
В SQLAlchemy это сессия которая хранить в себе объекты для дальнейшей записи в БД.
session.add(ed_user)
Запись состояния
Создает рабочую копию файлов, добавленных в staging area
.
git commit
Записывает объекты, добавленные ранее в сессию, в базу данных.
session.commit()
Существуют даже расширения для SQLAlchemy которые позволяют хранить данные в git репозитории вместо СУБД, используя при этом только возможности ORM библиотеки SQLAlchemy, т.к. модуль соединений с БД и конструктор SQL выражения для git не нужен (https://github.com/matthias-k/gitdb2).
Сессии создаются при помощи экземпляра класса
sqlalchemy.orm.session.Session
.
1 2 | >>> from sqlalchemy.orm import Session
>>> session = Session(bind=engine) |
Для добавления объекта (представляющего таблицу) в сессию, необходимо
использовать метод sqlalchemy.orm.session.Session.add()
.
1 | >>> session.add(ed_user) |
Перед выполнением любого запроса из сессии, состояние сессии автоматически
переносится в БД. В нашем случае, не сохраненный объект ed_user
добавляется
в БД.
1 2 3 4 5 6 7 8 9 10 11 12 13 | >>> our_user = session.query(User).filter_by(name='ed').first()
[SQL]: BEGIN (implicit)
[SQL]: INSERT INTO user (name, fullname) VALUES (?, ?)
[SQL]: ('ed', 'Edward Jones')
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.name = ?
LIMIT ? OFFSET ?
[SQL]: ('ed', 1, 0)
>>> our_user
<User('ed', 'Edward Jones')> |
Теперь у пользователя ed_user
появилось значение id
.
1 2 3 4 5 6 | >>> our_user.id
1
>>> ed_user.id
1
>>> ed_user == our_user is ed_user
True |
Добавление нескольких объектов в сессию за раз:
1 2 3 4 5 | >>> session.add_all([
... User(name='wendy', fullname='Wendy Weathersmith'),
... User(name='mary', fullname='Mary Contrary'),
... User(name='fred', fullname='Fred Flinstone')
>>> ]) |
Если объект, находящийся в сессии, поменялся, то он помечается как dirty
.
Все измененные объекты в сессии доступны через атрибут
sqlalchemy.orm.session.Session.dirty
1 2 3 | >>> ed_user.fullname = 'Ed Jones'
>>> session.dirty
IdentitySet([<User('ed', 'Ed Jones')>]) |
Новые объекты, попавшие в сессию после ее сохранения или в новую сессию,
доступны через атрибут sqlalchemy.orm.session.Session.new
1 2 3 4 | >>> session.new
IdentitySet([<User('fred', 'Fred Flinstone')>,
<User('wendy', 'Wendy Weathersmith')>,
<User('mary', 'Mary Contrary')>]) |
Метод sqlalchemy.orm.session.Session.commit()
сохраняет состояние сессии
в БД и подтверждает SQL транзакцию, в рамках которой выполнялись все
предыдущие запросы.
1 2 3 4 5 6 7 8 9 10 11 | >>> session.commit()
[SQL]: UPDATE user SET fullname=? WHERE user.id = ?
[SQL]: ('Ed Jones', 1)
[SQL]: INSERT INTO user (name, fullname) VALUES (?, ?)
[SQL]: ('wendy', 'Wendy Weathersmith')
[SQL]: INSERT INTO user (name, fullname) VALUES (?, ?)
[SQL]: ('mary', 'Mary Contrary')
[SQL]: INSERT INTO user (name, fullname) VALUES (?, ?)
[SQL]: ('fred', 'Fred Flinstone')
[SQL]: COMMIT |
После выполнения COMMIT
сессия не привязана ни к одной транзакции в СУБД.
Любые изменения объектов в сессии создадут новую транзакцию.
1 2 3 4 5 6 7 8 | >>> ed_user.fullname
[SQL]: BEGIN (implicit)
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.id = ?
[SQL]: (1,)
u'Ed Jones' |
Создадим новые изменения объектов и отравим SQL запрос с этими изменениями.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | >>> ed_user.name = 'Edwardo'
>>> fake_user = User(name='fakeuser', fullname='Invalid')
>>> session.add(fake_user)
>>> ed_user
<User('Edwardo', u'Ed Jones')>
>>> session.query(User).filter(User.name.in_(['Edwardo', 'fakeuser'])).all()
[SQL]: UPDATE user SET name=? WHERE user.id = ?
[SQL]: ('Edwardo', 1)
[SQL]: INSERT INTO user (name, fullname) VALUES (?, ?)
[SQL]: ('fakeuser', 'Invalid')
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.name IN (?, ?)
[SQL]: ('Edwardo', 'fakeuser')
[<User('Edwardo', u'Ed Jones')>, <User('fakeuser', 'Invalid')>] |
Несмотря на то что SQL запросы были выполнены в СУБД, мы все еще находимся в
транзакции. Поэтому любые изменения в сессии, даже если они выполнили SQL
запрос в СУБД, всегда можно отметить при помощи метода
sqlalchemy.orm.session.Session.rollback()
.
1 2 | >>> session.rollback()
[SQL]: ROLLBACK |
После ROLLBACK
сессия не привязана ни к одной транзакции в СУБД. Поэтому
при изменении объектов в сессии создастся новая транзакция. Причем данные
предыдущей сессии не были записаны.
1 2 3 4 5 6 7 8 | >>> ed_user.name
[SQL]: BEGIN (implicit)
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.id = ?
[SQL]: (1,)
u'ed' |
1 2 3 4 5 6 7 8 9 10 | >>> fake_user in session
False
>>> session.query(User).filter(User.name.in_(['ed', 'fakeuser'])).all()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.name IN (?, ?)
[SQL]: ('ed', 'fakeuser')
[<User(u'ed', u'Ed Jones')>] |
Операции над атрибутами класса таблицы равносильны операциям над объектом
sqlalchemy.schema.Column
. Поэтому их можно использовать в конструкторе
SQL запросов. Результатом выполнения SQL выражения будет список
значений записи в БД.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | >>> print(User.name == "ed")
"user".name = :name_1
>>> from sqlalchemy import select
>>> sel = select([User.name, User.fullname]).\
... where(User.name == 'ed').\
... order_by(User.id)
>>> session.connection().execute(sel).fetchall()
[SQL]: SELECT user.name, user.fullname
FROM user
WHERE user.name = ? ORDER BY user.id
[SQL]: ('ed',)
[(u'ed', u'Ed Jones')] |
ORM позволяет конструировать запросы при помощи метода
sqlalchemy.orm.session.Session.query()
. Этот метод создает объект класса
sqlalchemy.orm.query.Query
, который является более высокой абстракцией
конструктора SQL выражения в SQLAlchemy.
ORM, в отличии от стандартного конструктора SQL выражения, позволяет создавать запросы более наглядно и возвращать результат в виде объектов которые привязаны к сессии.
1 2 3 4 5 6 7 8 | >>> query = session.query(User).filter(User.name == 'ed').order_by(User.id)
>>> query.all()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.name = ? ORDER BY user.id
[SQL]: ('ed',)
[<User(u'ed', u'Ed Jones')>] |
Можно также возвращать чистые значения полей, как это делают SQL выражения.
1 2 3 4 5 6 7 8 9 10 | >>> for name, fullname in session.query(User.name, User.fullname):
... print(name, fullname)
[SQL]: SELECT user.name AS user_name, user.fullname AS user_fullname
FROM user
[SQL]: ()
(u'ed', u'Ed Jones')
(u'wendy', u'Wendy Weathersmith')
(u'mary', u'Mary Contrary')
(u'fred', u'Fred Flinstone') |
Или комбинировать значения полей с объектами.
1 2 3 4 5 6 7 8 9 10 | >>> for row in session.query(User, User.name):
... print(row.User, row.name)
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
[SQL]: ()
(<User(u'ed', u'Ed Jones')>, u'ed')
(<User(u'wendy', u'Wendy Weathersmith')>, u'wendy')
(<User(u'mary', u'Mary Contrary')>, u'mary')
(<User(u'fred', u'Fred Flinstone')>, u'fred') |
Выбор конкретной строки запроса делается не средствами языка Python, а на
стороне СУБД, за счет конструкции LIMIT ? OFFSET ?
, что значительно
ускоряет выполнение запроса. Для программиста это выглядит прозрачно, как будто
он работает с Python списком.
1 2 3 4 5 6 7 8 | >>> u = session.query(User).order_by(User.id)[2]
>>> print(u)
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user ORDER BY user.id
LIMIT ? OFFSET ?
[SQL]: (1, 2)
<User(u'mary', u'Mary Contrary')> |
Аналогично работают и Python срезы.
1 2 3 4 5 6 7 8 9 | >>> for u in session.query(User).order_by(User.id)[1:3]:
... print(u)
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user ORDER BY user.id
LIMIT ? OFFSET ?
[SQL]: (2, 1)
<User(u'wendy', u'Wendy Weathersmith')>
<User(u'mary', u'Mary Contrary')> |
Условие WHERE
соответствует методу
sqlalchemy.orm.query.Query.filter_by()
.
1 2 3 4 5 6 7 8 | >>> for name, in session.query(User.name).\
... filter_by(fullname='Ed Jones'):
... print(name)
[SQL]: SELECT user.name AS user_name
FROM user
WHERE user.fullname = ?
[SQL]: ('Ed Jones',) |
Или более функциональному методу sqlalchemy.orm.query.Query.filter()
.
1 2 3 4 5 6 7 8 9 | >>> for name, in session.query(User.name).\
... filter(User.fullname == 'Ed Jones'):
... print(name)
[SQL]: SELECT user.name AS user_name
FROM user
WHERE user.fullname = ?
[SQL]: ('Ed Jones',)
ed |
1 2 3 4 5 6 7 8 9 10 11 12 13 | >>> from sqlalchemy import or_
>>> for name, in session.query(User.name).\
... filter(or_(User.fullname == 'Ed Jones', User.id < 5)):
... print(name)
[SQL]: SELECT user.name AS user_name
FROM user
WHERE user.fullname = ? OR user.id < ?
[SQL]: ('Ed Jones', 5)
ed
wendy
mary
fred |
Последовательное выполнение методов sqlalchemy.orm.query.Query.filter()
соединяет условия WHERE
при помощи оператора AND
, аналогично
конструкции select().where()
.
1 2 3 4 5 6 7 8 9 | >>> for user in session.query(User).\
... filter(User.name == 'ed').\
... filter(User.fullname == 'Ed Jones'):
... print(user)
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.name = ? AND user.fullname = ?
[SQL]: ('ed', 'Ed Jones')
<User(u'ed', u'Ed Jones')> |
Сам объект класса sqlalchemy.orm.query.Query
не выполняет обращений к
БД.
1 2 | >>> query = session.query(User).filter_by(fullname='Ed Jones')
>>> |
Для этого существуют специальные методы этого класса, например
sqlalchemy.orm.query.Query.all()
.
1 2 3 4 5 6 7 | >>> query.all()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.fullname = ?
[SQL]: ('Ed Jones',)
[<User(u'ed', u'Ed Jones')>] |
sqlalchemy.orm.query.Query.first()
- выполнит запрос и вернет первую
строку запроса или None
.
1 2 3 4 5 6 7 8 | >>> query.first()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.fullname = ?
LIMIT ? OFFSET ?
[SQL]: ('Ed Jones', 1, 0)
<User(u'ed', u'Ed Jones')> |
sqlalchemy.orm.query.Query.one()
- выполнит запрос, вернет первую строку
запроса и проверит что она одна и только одна, иначе вызовет исключение
sqlalchemy.orm.exc.NoResultFound
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | >>> query.one()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.fullname = ?
[SQL]: ('Ed Jones',)
<User(u'ed', u'Ed Jones')>
>>>
>>> query = session.query(User).filter_by(fullname='nonexistent')
>>> query.one()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.fullname = ?
[SQL]: ('nonexistent',)
Traceback (most recent call last):
File "/home/user/.virtualenvs/lectures/local/lib/python2.7/site-packages/sliderepl/core.py", line 291, in run
exec_(co, environ)
File "/home/user/.virtualenvs/lectures/local/lib/python2.7/site-packages/sliderepl/compat.py", line 24, in exec_
exec("""exec _code_ in _globs_, _locs_""")
File "<string>", line 1, in <module>
File "<input>", line 1, in <module>
File "/home/user/.virtualenvs/lectures/local/lib/python2.7/site-packages/sqlalchemy/orm/query.py", line 2478, in one
raise orm_exc.NoResultFound("No row was found for one()")
NoResultFound: No row was found for one() |
Если результат запроса вернет больше строк чем одну, это тоже расценивается как
ошибка sqlalchemy.orm.exc.MultipleResultsFound
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | >>> query = session.query(User)
>>> query.one()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
[SQL]: ()
Traceback (most recent call last):
File "/home/uralbash/.virtualenvs/sacrud/local/lib/python2.7/site-packages/sliderepl/core.py", line 291, in run
exec_(co, environ)
File "/home/uralbash/.virtualenvs/sacrud/local/lib/python2.7/site-packages/sliderepl/compat.py", line 24, in exec_
exec("""exec _code_ in _globs_, _locs_""")
File "<string>", line 1, in <module>
File "<input>", line 1, in <module>
File "/home/uralbash/.virtualenvs/sacrud/local/lib/python2.7/site-packages/sqlalchemy/orm/query.py", line 2481, in one
"Multiple rows were found for one()")
MultipleResultsFound: Multiple rows were found for one() |
Новый класс Address
имеет связь Many-To-One с таблицей User
.
Связь между Python классов осуществляется при помощи функции
sqlalchemy.orm.relationship()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | >>> from sqlalchemy import ForeignKey
>>> from sqlalchemy.orm import relationship
>>> class Address(Base):
... __tablename__ = 'address'
... id = Column(Integer, primary_key=True)
... email_address = Column(String, nullable=False)
... user_id = Column(Integer, ForeignKey('user.id'))
... user = relationship("User", backref="addresses")
... def __repr__(self):
... return "<Address(%r)>" % self.email_address |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | >>> Base.metadata.create_all(engine)
[SQL]: PRAGMA table_info("user")
[SQL]: ()
[SQL]: PRAGMA table_info("address")
[SQL]: ()
[SQL]:
CREATE TABLE address (
id INTEGER NOT NULL,
email_address VARCHAR NOT NULL,
user_id INTEGER,
PRIMARY KEY (id),
FOREIGN KEY(user_id) REFERENCES user (id)
)
[SQL]: ()
[SQL]: COMMIT |
Благодаря параметру backref
, класс User
получает обратную ссылку на
класс Adress
.
1 2 3 | >>> jack = User(name='jack', fullname='Jack Bean')
>>> jack.addresses
[] |
Добавим пользователю адреса.
1 2 3 4 5 | >>> jack.addresses = [
... Address(email_address='jack@gmail.com'),
... Address(email_address='j25@yahoo.com'),
... Address(email_address='jack@hotmail.com'),
... ] |
sqlalchemy.orm.backref()
добавляет ссылки друг на друга для каждого
объекта.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | >>> jack
<User('jack', 'Jack Bean')>
>>> jack.addresses[1]
<Address('j25@yahoo.com')>
>>> jack.addresses[1].user
<User('jack', 'Jack Bean')>
>>> jack.addresses[1].user.addresses[1]
<Address('j25@yahoo.com')>
>>> jack.addresses[1].user.addresses[1].user
<User('jack', 'Jack Bean')>
>>> jack.addresses[1].user.addresses[1].user.addresses[1].user.addresses[1].user
<User('jack', 'Jack Bean')>
>>> jack.addresses[1].user.addresses[1].user.addresses[2].user.addresses[0].user
<User('jack', 'Jack Bean')> |
Добавление в сессию объекта, который ссылается на другие объекты, автоматически включает их тоже.
1 2 3 4 5 6 | >>> session.add(jack)
>>> session.new
IdentitySet([<Address('jack@hotmail.com')>,
<Address('jack@gmail.com')>,
<User('jack', 'Jack Bean')>,
<Address('j25@yahoo.com')>]) |
1 2 3 4 5 6 7 8 9 10 11 | >>> session.commit()
[SQL]: INSERT INTO user (name, fullname) VALUES (?, ?)
[SQL]: ('jack', 'Jack Bean')
[SQL]: INSERT INTO address (email_address, user_id) VALUES (?, ?)
[SQL]: ('jack@gmail.com', 5)
[SQL]: INSERT INTO address (email_address, user_id) VALUES (?, ?)
[SQL]: ('j25@yahoo.com', 5)
[SQL]: INSERT INTO address (email_address, user_id) VALUES (?, ?)
[SQL]: ('jack@hotmail.com', 5)
[SQL]: COMMIT |
После подтверждения транзакции (COMMIT), обращение по ссылке создаст новую транзакцию и считает значения из БД.
1 2 3 4 5 6 7 8 9 10 11 12 | >>> jack.addresses
[SQL]: BEGIN (implicit)
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.id = ?
[SQL]: (5,)
[SQL]: SELECT address.id AS address_id, address.email_address AS address_email_address, address.user_id AS address_user_id
FROM address
WHERE ? = address.user_id
[SQL]: (5,)
[<Address(u'jack@gmail.com')>, <Address(u'j25@yahoo.com')>, <Address(u'jack@hotmail.com')>] |
Теперь, считанные объекты находятся в памяти, до тех пор пока мы опять не подтвердим транзакцию (COMMIT) или отменим ее (ROLLBACK).
1 2 | >>> jack.addresses
[<Address(u'jack@gmail.com')>, <Address(u'j25@yahoo.com')>, <Address(u'jack@hotmail.com')>] |
Привяжем адрес к другому пользователю.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | >>> fred = session.query(User).filter_by(name='fred').one()
>>> jack.addresses[1].user = fred
>>> fred.addresses
>>> session.commit()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.name = ?
[SQL]: ('fred',)
[SQL]: UPDATE address SET user_id=? WHERE address.id = ?
[SQL]: (4, 2)
[SQL]: SELECT address.id AS address_id, address.email_address AS address_email_address, address.user_id AS address_user_id
FROM address
WHERE ? = address.user_id
[SQL]: (4,)
[<Address(u'j25@yahoo.com')>]
[SQL]: COMMIT |
Выполнение операции implicit JOIN.
1 2 3 4 5 6 7 8 9 10 11 | >>> session.query(User, Address).filter(User.id == Address.user_id).all()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS
user_fullname, address.id AS address_id, address.email_address AS
address_email_address, address.user_id AS address_user_id FROM user,
address
WHERE user.id = address.user_id
[SQL]: ()
[(<User(u'jack', u'Jack Bean')>, <Address(u'jack@gmail.com')>),
(<User(u'fred', u'Fred Flinstone')>, <Address(u'j25@yahoo.com')>),
(<User(u'jack', u'Jack Bean')>, <Address(u'jack@hotmail.com')>)] |
Явный JOIN.
1 2 3 4 5 6 7 8 9 10 | >>> session.query(User, Address).join(Address, User.id == Address.user_id).all()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS
user_fullname, address.id AS address_id, address.email_address AS
address_email_address, address.user_id AS address_user_id FROM user JOIN
address ON user.id = address.user_id
[SQL]: ()
[(<User(u'jack', u'Jack Bean')>, <Address(u'jack@gmail.com')>),
(<User(u'fred', u'Fred Flinstone')>, <Address(u'j25@yahoo.com')>),
(<User(u'jack', u'Jack Bean')>, <Address(u'jack@hotmail.com')>)] |
Более краткий и понятный способ использовать ссылку на таблицу для связи.
1 2 3 4 5 6 7 8 9 10 | >>> session.query(User, Address).join(User.addresses).all()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS
user_fullname, address.id AS address_id, address.email_address AS
address_email_address, address.user_id AS address_user_id FROM user JOIN
address ON user.id = address.user_id
[SQL]: ()
[(<User(u'jack', u'Jack Bean')>, <Address(u'jack@gmail.com')>),
(<User(u'fred', u'Fred Flinstone')>, <Address(u'j25@yahoo.com')>),
(<User(u'jack', u'Jack Bean')>, <Address(u'jack@hotmail.com')>)] |
В простых случаях можно передавать только класс таблицы.
1 2 3 4 5 6 7 8 9 10 | >>> session.query(User, Address).join(Address).all()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS
user_fullname, address.id AS address_id, address.email_address AS
address_email_address, address.user_id AS address_user_id FROM user JOIN
address ON user.id = address.user_id
[SQL]: ()
[(<User(u'jack', u'Jack Bean')>, <Address(u'jack@gmail.com')>),
(<User(u'fred', u'Fred Flinstone')>, <Address(u'j25@yahoo.com')>),
(<User(u'jack', u'Jack Bean')>, <Address(u'jack@hotmail.com')>)] |
JOIN с условием WHERE.
1 2 3 4 5 6 7 8 9 | >>> session.query(User.name).join(User.addresses).\
... filter(Address.email_address == 'jack@gmail.com').first()
[SQL]: SELECT user.name AS user_name
FROM user JOIN address ON user.id = address.user_id
WHERE address.email_address = ?
LIMIT ? OFFSET ?
[SQL]: ('jack@gmail.com', 1, 0)
(u'jack',) |
Явный вызов конструкции SELECT FROM JOIN используя метод
sqlalchemy.orm.query.Query.select_from()
.
1 2 3 4 5 6 7 8 9 10 | >>> session.query(User, Address).select_from(Address).join(Address.user).all()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS
user_fullname, address.id AS address_id, address.email_address AS
address_email_address, address.user_id AS address_user_id FROM address
JOIN user ON user.id = address.user_id
[SQL]: ()
[(<User(u'jack', u'Jack Bean')>, <Address(u'jack@gmail.com')>),
(<User(u'fred', u'Fred Flinstone')>, <Address(u'j25@yahoo.com')>),
(<User(u'jack', u'Jack Bean')>, <Address(u'jack@hotmail.com')>)] |
Запросы ссылающиеся на одну сущность более чем один раз, нуждаются в алиасах.
Алиасы задаются при помощи функции sqlalchemy.orm.aliased()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | >>> from sqlalchemy.orm import aliased
>>> a1, a2 = aliased(Address), aliased(Address)
>>> session.query(User).\
... join(a1).\
... join(a2).\
... filter(a1.email_address == 'jack@gmail.com').\
... filter(a2.email_address == 'jack@hotmail.com').\
... all()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user JOIN address AS address_1 ON user.id = address_1.user_id JOIN
address AS address_2 ON user.id = address_2.user_id WHERE
address_1.email_address = ? AND address_2.email_address = ?
[SQL]: ('jack@gmail.com', 'jack@hotmail.com')
[<User(u'jack', u'Jack Bean')>] |
Подзапросы автоматически использую алиасы.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | >>> from sqlalchemy import func
>>> subq = session.query(
... func.count(Address.id).label('count'),
... User.id.label('user_id')
... ).\
... join(Address.user).\
... group_by(User.id).\
... subquery()
>>> session.query(User.name, func.coalesce(subq.c.count, 0)).\
... outerjoin(subq, User.id == subq.c.user_id).all()
[SQL]: SELECT user.name AS user_name, coalesce(anon_1.count, ?) AS coalesce_1
FROM user LEFT OUTER JOIN (SELECT count(address.id) AS count, user.id AS user_id
FROM address JOIN user ON user.id = address.user_id GROUP BY user.id) AS anon_1
ON user.id = anon_1.user_id [SQL]: (0,)
[(u'ed', 0), (u'wendy', 0), (u'mary', 0), (u'fred', 1), (u'jack', 2)] |
При каждом обращении к ссылкам объекта в цикле, вызывается новый запрос:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | >>> for user in session.query(User):
... print(user, user.addresses)
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS
user_fullname FROM user
[SQL]: ()
[SQL]: SELECT address.id AS address_id, address.email_address AS
address_email_address, address.user_id AS address_user_id FROM address
WHERE ? = address.user_id
[SQL]: (1,)
(<User(u'ed', u'Ed Jones')>, [])
[SQL]: SELECT address.id AS address_id, address.email_address AS
address_email_address, address.user_id AS address_user_id FROM address
WHERE ? = address.user_id
[SQL]: (2,)
(<User(u'wendy', u'Wendy Weathersmith')>, [])
[SQL]: SELECT address.id AS address_id, address.email_address AS
address_email_address, address.user_id AS address_user_id FROM address
WHERE ? = address.user_id
[SQL]: (3,)
(<User(u'mary', u'Mary Contrary')>, [])
[SQL]: SELECT address.id AS address_id, address.email_address AS
address_email_address, address.user_id AS address_user_id FROM address
WHERE ? = address.user_id
[SQL]: (4,)
(<User(u'fred', u'Fred Flinstone')>, [<Address(u'j25@yahoo.com')>])
(<User(u'jack', u'Jack Bean')>, [<Address(u'jack@gmail.com')>,
<Address(u'jack@hotmail.com')>]) |
Чтобы этого избежать нужно использовать опцию предварительной загрузки
sqlalchemy.orm.subqueryload()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | >>> session.rollback() # so we can see the load happen again.
>>> from sqlalchemy.orm import subqueryload
>>> for user in session.query(User).options(subqueryload(User.addresses)):
... print(user, user.addresses)
[SQL]: ROLLBACK
[SQL]: BEGIN (implicit)
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
[SQL]: ()
[SQL]: SELECT address.id AS address_id, address.email_address AS
address_email_address, address.user_id AS address_user_id, anon_1.user_id
AS anon_1_user_id FROM (SELECT user.id AS user_id
FROM user) AS anon_1 JOIN address ON anon_1.user_id = address.user_id ORDER BY anon_1.user_id
[SQL]: ()
(<User(u'ed', u'Ed Jones')>, [])
(<User(u'wendy', u'Wendy Weathersmith')>, [])
(<User(u'mary', u'Mary Contrary')>, [])
(<User(u'fred', u'Fred Flinstone')>, [<Address(u'j25@yahoo.com')>])
(<User(u'jack', u'Jack Bean')>, [<Address(u'jack@gmail.com')>, <Address(u'jack@hotmail.com')>]) |
Или sqlalchemy.orm.joinedload()
чтобы уместить все в один запрос.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | >>> session.rollback()
>>> from sqlalchemy.orm import joinedload
>>> for user in session.query(User).options(joinedload(User.addresses)):
... print(user, user.addresses)
[SQL]: ROLLBACK
[SQL]: BEGIN (implicit)
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS
user_fullname, address_1.id AS address_1_id, address_1.email_address AS
address_1_email_address, address_1.user_id AS address_1_user_id FROM user
LEFT OUTER JOIN address AS address_1 ON user.id = address_1.user_id
[SQL]: ()
(<User(u'ed', u'Ed Jones')>, [])
(<User(u'wendy', u'Wendy Weathersmith')>, [])
(<User(u'mary', u'Mary Contrary')>, [])
(<User(u'fred', u'Fred Flinstone')>, [<Address(u'j25@yahoo.com')>])
(<User(u'jack', u'Jack Bean')>, [<Address(u'jack@gmail.com')>, <Address(u'jack@hotmail.com')>]) |
Удаление адреса из списка пользователя User.addresses
, поменяет значение
поля FOREIGN KEY на NULL, но не удалит саму запись.
1 2 3 4 5 6 7 8 9 10 11 | >>> jack = session.query(User).filter_by(name='jack').one()
>>> del jack.addresses[0]
>>> session.commit()
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.name = ?
[SQL]: ('jack',)
[SQL]: UPDATE address SET user_id=? WHERE address.id = ?
[SQL]: (None, 1)
[SQL]: COMMIT |
Мы можем настроить связи между таблицами на каскадное удаление.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | >>> User.addresses.property.cascade = "all, delete, delete-orphan"
>>> fred = session.query(User).filter_by(name='fred').one()
>>> del fred.addresses[0]
>>> session.commit()
[SQL]: BEGIN (implicit)
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.name = ?
[SQL]: ('fred',)
[SQL]: SELECT address.id AS address_id, address.email_address AS
address_email_address, address.user_id AS address_user_id FROM address
WHERE ? = address.user_id
[SQL]: (4,)
[SQL]: DELETE FROM address WHERE address.id = ?
[SQL]: (2,)
[SQL]: COMMIT |
delete-orphan
означает что дети не могут существовать без родителей.
Поэтому при удалении родителя вся связанные с ним записи тоже удалятся.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | >>> session.delete(jack)
>>> session.commit()
[SQL]: BEGIN (implicit)
[SQL]: SELECT user.id AS user_id, user.name AS user_name, user.fullname AS user_fullname
FROM user
WHERE user.id = ?
[SQL]: (5,)
[SQL]: SELECT address.id AS address_id, address.email_address AS address_email_address, address.user_id AS address_user_id
FROM address
WHERE ? = address.user_id
[SQL]: (5,)
[SQL]: DELETE FROM address WHERE address.id = ?
[SQL]: (3,)
[SQL]: DELETE FROM user WHERE user.id = ?
[SQL]: (5,)
[SQL]: COMMIT |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 | # ## slide::
# ## title:: Object Relational Mapping
# The *declarative* system is normally used to configure
# object relational mappings.
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
# ## slide::
# a basic mapping. __repr__() is optional.
from sqlalchemy import Column, Integer, String
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String)
fullname = Column(String)
def __repr__(self):
return "<User(%r, %r)>" % (self.name, self.fullname)
# ## slide::
# the User class now has a Table object associated with it.
User.__table__
# ## slide::
# The Mapper object mediates the relationship between User
# and the "user" Table object.
User.__mapper__
# ## slide::
# User has a default constructor, accepting field names
# as arguments.
ed_user = User(name='ed', fullname='Edward Jones')
# ## slide::
# The "id" field is the primary key, which starts as None
# if we didn't set it explicitly.
print(ed_user.name, ed_user.fullname)
print(ed_user.id)
# ## slide:: p
# The MetaData object is here too, available from the Base.
from sqlalchemy import create_engine
engine = create_engine('sqlite://')
Base.metadata.create_all(engine)
# ## slide::
# To persist and load User objects from the database, we
# use a Session object.
from sqlalchemy.orm import Session
session = Session(bind=engine)
# ## slide::
# new objects are placed into the Session using add().
session.add(ed_user)
# ## slide:: pi
# the Session will *flush* *pending* objects
# to the database before each Query.
our_user = session.query(User).filter_by(name='ed').first()
our_user
# ## slide::
# the User object we've inserted now has a value for ".id"
print(ed_user.id)
# ## slide::
# the Session maintains a *unique* object per identity.
# so "ed_user" and "our_user" are the *same* object
ed_user is our_user
# ## slide::
# Add more objects to be pending for flush.
session.add_all([User(name='wendy',
fullname='Wendy Weathersmith'),
User(name='mary',
fullname='Mary Contrary'),
User(name='fred',
fullname='Fred Flinstone')])
# ## slide::
# modify "ed_user" - the object is now marked as *dirty*.
ed_user.fullname = 'Ed Jones'
# ## slide::
# the Session can tell us which objects are dirty...
session.dirty
# ## slide::
# and can also tell us which objects are pending...
session.new
# ## slide:: p i
# The whole transaction is committed. Commit always triggers
# a final flush of remaining changes.
session.commit()
# ## slide:: p
# After a commit, theres no transaction. The Session
# *invalidates* all data, so that accessing them will automatically
# start a *new* transaction and re-load from the database.
ed_user.fullname
# ## slide::
# Make another "dirty" change, and another "pending" change,
# that we might change our minds about.
ed_user.name = 'Edwardo'
fake_user = User(name='fakeuser', fullname='Invalid')
session.add(fake_user)
# ## slide:: p
# run a query, our changes are flushed; results come back.
session.query(User).filter(User.name.in_(['Edwardo', 'fakeuser'])).all()
# ## slide::
# But we're inside of a transaction. Roll it back.
session.rollback()
# ## slide:: p
# ed_user's name is back to normal
ed_user.name
# ## slide::
# "fake_user" has been evicted from the session.
fake_user in session
# ## slide:: p
# and the data is gone from the database too.
session.query(User).filter(User.name.in_(['ed', 'fakeuser'])).all()
# ## slide::
# ## title:: Exercises - Basic Mapping
#
# 1. Create a class/mapping for this table, call the class Network
#
# CREATE TABLE network (
# network_id INTEGER PRIMARY KEY,
# name VARCHAR(100) NOT NULL,
# )
#
# 2. emit Base.metadata.create_all(engine) to create the table
#
# 3. commit a few Network objects to the database:
#
# Network(name='net1'), Network(name='net2')
#
#
# ## slide::
# ## title:: ORM Querying
# The attributes on our mapped class act like Column objects, and
# produce SQL expressions.
print(User.name == "ed")
# ## slide:: p
# These SQL expressions are compatible with the select() object
# we introduced earlier.
from sqlalchemy import select
sel = select([User.name, User.fullname]).\
where(User.name == 'ed').\
order_by(User.id)
session.connection().execute(sel).fetchall()
# ## slide:: p
# but when using the ORM, the Query() object provides a lot more functionality,
# here selecting the User *entity*.
query = session.query(User).filter(User.name == 'ed').order_by(User.id)
query.all()
# ## slide:: p
# Query can also return individual columns
for name, fullname in session.query(User.name, User.fullname):
print(name, fullname)
# ## slide:: p
# and can mix entities / columns together.
for row in session.query(User, User.name):
print(row.User, row.name)
# ## slide:: p
# Array indexes will OFFSET to that index and LIMIT by one...
u = session.query(User).order_by(User.id)[2]
print(u)
# ## slide:: pi
# and array slices work too.
for u in session.query(User).order_by(User.id)[1:3]:
print(u)
# ## slide:: p
# the WHERE clause is either by filter_by(), which is convenient
for name, in session.query(User.name).\
filter_by(fullname='Ed Jones'):
print(name)
# ## slide:: p
# or filter(), which is more flexible
for name, in session.query(User.name).\
filter(User.fullname == 'Ed Jones'):
print(name)
# ## slide:: p
# conjunctions can be passed to filter() as well
from sqlalchemy import or_
for name, in session.query(User.name).\
filter(or_(User.fullname == 'Ed Jones', User.id < 5)):
print(name)
# ## slide::
# multiple filter() calls join by AND just like select().where()
for user in session.query(User).\
filter(User.name == 'ed').\
filter(User.fullname == 'Ed Jones'):
print(user)
# ## slide::
# Query has some variety for returning results
query = session.query(User).filter_by(fullname='Ed Jones')
# ## slide:: p
# all() returns a list
query.all()
# ## slide:: p
# first() returns the first row, or None
query.first()
# ## slide:: p
# one() returns the first row and verifies that there's one and only one
query.one()
# ## slide:: p
# if there's not one(), you get an error
query = session.query(User).filter_by(fullname='nonexistent')
query.one()
# ## slide:: p
# if there's more than one(), you get an error
query = session.query(User)
query.one()
# ## slide::
# ## title:: Exercises - ORM Querying
# 1. Produce a Query object representing the list of "fullname" values for
# all User objects in alphabetical order.
#
# 2. call .all() on the query to make sure it works!
#
# 3. build a second Query object from the first that also selects
# only User rows with the name "mary" or "ed".
#
# 4. return only the second row of the Query from #3.
# ## slide::
# ## title:: Joins and relationships
# A new class called Address, with a *many-to-one* relationship to User.
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship
class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
email_address = Column(String, nullable=False)
user_id = Column(Integer, ForeignKey('user.id'))
user = relationship("User", backref="addresses")
def __repr__(self):
return "<Address(%r)>" % self.email_address
# ## slide:: p
# create the new table.
Base.metadata.create_all(engine)
# ## slide::
# a new User object also gains an empty "addresses" collection now.
jack = User(name='jack', fullname='Jack Bean')
jack.addresses
# ## slide::
# populate this collection with new Address objects.
jack.addresses = [Address(email_address='jack@gmail.com'),
Address(email_address='j25@yahoo.com'),
Address(email_address='jack@hotmail.com'), ]
# ## slide::
# the "backref" sets up Address.user for each User.address.
jack.addresses[1]
jack.addresses[1].user
# ## slide::
# adding User->jack will *cascade* each Address into the Session as well.
session.add(jack)
session.new
# ## slide:: p
# commit.
session.commit()
# ## slide:: p
# After expiration, jack.addresses emits a *lazy load* when first
# accessed.
jack.addresses
# ## slide:: i
# the collection stays in memory until the transaction ends.
jack.addresses
# ## slide:: p
# collections and references are updated by manipulating objects,
# not primary / foreign key values.
fred = session.query(User).filter_by(name='fred').one()
jack.addresses[1].user = fred
fred.addresses
session.commit()
# ## slide:: p
# Query can select from multiple tables at once.
# Below is an *implicit join*.
session.query(User, Address).filter(User.id == Address.user_id).all()
# ## slide:: p
# join() is used to create an explicit JOIN.
session.query(User, Address).join(Address, User.id == Address.user_id).all()
# ## slide:: p
# The most succinct and accurate way to join() is to use the
# the relationship()-bound attribute to specify ON.
session.query(User, Address).join(User.addresses).all()
# ## slide:: p
# join() will also figure out very simple joins just using entities.
session.query(User, Address).join(Address).all()
# ## slide:: p
# Either User or Address may be referred to anywhere in the query.
session.query(User.name).join(User.addresses).\
filter(Address.email_address == 'jack@gmail.com').first()
# ## slide:: p
# we can specify an explicit FROM using select_from().
session.query(User, Address).select_from(Address).join(Address.user).all()
# ## slide:: p
# A query that refers to the same entity more than once in the FROM
# clause requires *aliasing*.
from sqlalchemy.orm import aliased
a1, a2 = aliased(Address), aliased(Address)
session.query(User).\
join(a1).\
join(a2).\
filter(a1.email_address == 'jack@gmail.com').\
filter(a2.email_address == 'jack@hotmail.com').\
all()
# ## slide:: p
# We can also join with subqueries. subquery() returns
# an "alias" construct for us to use.
from sqlalchemy import func
subq = session.query(
func.count(Address.id).label('count'),
User.id.label('user_id')
).\
join(Address.user).\
group_by(User.id).\
subquery()
session.query(User.name, func.coalesce(subq.c.count, 0)).\
outerjoin(subq, User.id == subq.c.user_id).all()
# ## slide::
# ## title:: Exercises
# 1. Run this SQL JOIN:
#
# SELECT user.name, address.email_address FROM user
# JOIN address ON user.id=address.user_id WHERE
# address.email_address='j25@yahoo.com'
#
# 2. Tricky Bonus! Select all pairs of distinct user names.
# Hint: "... ON user_alias1.name < user_alias2.name"
#
# ## slide:: p
# ## title:: Eager Loading
# the "N plus one" problem refers to the many SELECT statements
# emitted when loading collections against a parent result
for user in session.query(User):
print(user, user.addresses)
# ## slide:: p
# *eager loading* solves this problem by loading *all* collections
# at once.
session.rollback() # so we can see the load happen again.
from sqlalchemy.orm import subqueryload
for user in session.query(User).options(subqueryload(User.addresses)):
print(user, user.addresses)
# ## slide:: p
# joinedload() uses a LEFT OUTER JOIN to load parent + child in one query.
session.rollback()
from sqlalchemy.orm import joinedload
for user in session.query(User).options(joinedload(User.addresses)):
print(user, user.addresses)
# ## slide:: p
# eager loading *does not* change the *result* of the Query.
# only how related collections are loaded.
for address in session.query(Address).\
join(Address.user).\
filter(User.name == 'jack').\
options(joinedload(Address.user)):
print(address, address.user)
# ## slide:: p
# to join() *and* joinedload() at the same time without using two
# JOIN clauses, use contains_eager()
from sqlalchemy.orm import contains_eager
for address in session.query(Address).\
join(Address.user).\
filter(User.name == 'jack').\
options(contains_eager(Address.user)):
print(address, address.user)
# ## slide:: p
# ## title:: Delete Cascades
# removing an Address sets its foreign key to NULL.
# We'd prefer it gets deleted.
jack = session.query(User).filter_by(name='jack').one()
del jack.addresses[0]
session.commit()
# ## slide::
# This can be configured on relationship() using
# "delete-orphan" cascade on the User->Address
# relationship.
User.addresses.property.cascade = "all, delete, delete-orphan"
# ## slide:: p
# Removing an Address from a User will now delete it.
fred = session.query(User).filter_by(name='fred').one()
del fred.addresses[0]
session.commit()
# ## slide:: p
# Deleting the User will also delete all Address objects.
session.delete(jack)
session.commit()
# ## slide::
# ## title:: Exercises - Final Exam !
# 1. Create a class called 'Account', with table "account":
#
# id = Column(Integer, primary_key=True)
# owner = Column(String(50), nullable=False)
# balance = Column(Numeric, default=0)
#
# 2. Create a class "Transaction", with table "transaction":
# * Integer primary key
# * numeric "amount" column
# * Integer "account_id" column with ForeignKey('account.id')
#
# 3. Add a relationship() on Transaction named "account", which refers
# to "Account", and has a backref called "transactions".
#
# 4. Create a database, create tables, then insert these objects:
#
# a1 = Account(owner='Jack Jones', balance=5000)
# a2 = Account(owner='Ed Rendell', balance=10000)
# Transaction(amount=500, account=a1)
# Transaction(amount=4500, account=a1)
# Transaction(amount=6000, account=a2)
# Transaction(amount=4000, account=a2)
#
# 5. Produce a report that shows:
# * account owner
# * account balance
# * summation of transaction amounts per account (should match balance)
# A column can be summed using func.sum(Transaction.amount)
#
# from sqlalchemy import Integer, String, Numeric
# ## slide:: |
SQLAlchemy находит применение в веб-фреймворках TurboGears, Pylons, Pyramid, Zope, Flask. Например, известный социальный новостной сайт Reddit построен с использованием SQLAlchemy. Список организаций, использующих SQLAlchemy, можно найти на сайте проекта.
paginate_sqlalchemy
выполняет то же, что и библиотека
https://github.com/Pylons/paginate, но гораздо быстрее для SQLAlchemy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Person(Base):
__tablename__ = 'person'
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
def __repr__(self):
return "<{}>".format(self.name)
engine = create_engine('sqlite:///sqlalchemy_example.db')
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
DBSession = sessionmaker(bind=engine)
session = DBSession()
for i in range(100):
new_person = Person(name='new person #%s' % i)
session.add(new_person)
session.commit()
query = session.query(Person)
print(query.count()) # 100
print
from paginate_sqlalchemy import SqlalchemyOrmPage
page = SqlalchemyOrmPage(query, page=5, items_per_page=8)
print(page)
print(page.items)
print(page.items[6].name)
print(page.page_count) |
Результат выполнения
100
Page:
Collection type: <class 'sqlalchemy.orm.query.Query'>
Current page: 5
First item: 33
Last item: 40
First page: 1
Last page: 13
Previous page: 4
Next page: 6
Items per page: 8
Total number of items: 100
Number of pages: 13
[<new person #32>, <new person #33>, <new person #34>, <new person #35>, <new person #36>, <new person #37>, <new person #38>, <new person #39>]
new person #38
13
См.также
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Person(Base):
__tablename__ = 'person'
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
def __repr__(self):
return "<{}>".format(self.name)
from colanderalchemy import SQLAlchemySchemaNode
person = SQLAlchemySchemaNode(Person)
from deform import Form
form = Form(person, buttons=('submit',))
print(form.render()) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | <form
id="deform"
method="POST"
enctype="multipart/form-data"
accept-charset="utf-8" class="deform"
>
<fieldset class="deformFormFieldset">
<input type="hidden" name="_charset_" />
<input type="hidden" name="__formid__" value="deform"/>
<ul>
<li class="field item-id "
title=""
id="item-deformField1">
<!-- mapping_item -->
<label
class="desc"
title=""
for="deformField1"
>Id
</label>
<input type="text" name="id" value=""
id="deformField1"/>
<!-- /mapping_item -->
</li>
</ul>
<ul>
<li class="field item-name "
title=""
id="item-deformField2">
<!-- mapping_item -->
<label
class="desc"
title=""
for="deformField2"
>Name<span class="req"
id="req-deformField2">*</span>
</label>
<input type="text" name="name" value=""
id="deformField2"/>
<!-- /mapping_item -->
</li>
</ul>
<ul>
<li class="buttons">
<button
id="deformsubmit"
name="submit"
type="submit"
class="btnText submit "
value="submit">
<span>Submit</span>
</button>
</li>
</ul>
</fieldset>
</form> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | # -*- coding: utf-8 -*-
from jinja2.utils import generate_lorem_ipsum
from sqlalchemy import Column, Integer, String, Text, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Articles(Base):
__tablename__ = 'articles'
id = Column(Integer, primary_key=True)
title = Column(String(250), nullable=False)
content = Column(Text, nullable=False)
def __repr__(self):
return "<{}>".format(self.name)
engine = create_engine('sqlite:///foo.db')
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
dbsession = Session()
for id, article in enumerate(range(100), start=1):
title = generate_lorem_ipsum(
n=1, # Одно предложение
html=False, # В виде обычного текста
min=2, # Минимум 2 слова
max=5 # Максимум 5
)
content = generate_lorem_ipsum()
article = Articles(**{'id': id, 'title': title, 'content': content})
dbsession.add(article)
dbsession.commit()
dbsession.close() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | # -*- coding: utf-8 -*-
import deform
from jinja2 import Environment, FileSystemLoader
from webob import Request, Response
from common import get_csrf_token, get_session
from models import Session, Articles
env = Environment(loader=FileSystemLoader('templates'))
def wsgify(view):
def wrapped(environ, start_response):
request = Request(environ)
app = view(request).response()
return app(environ, start_response)
return wrapped
class BaseArticle(object):
def __init__(self, request):
self.request = request
article_id = self.request.environ['wsgiorg.routing_args'][1]['id']
dbsession = Session()
self.article = dbsession.query(Articles).filter_by(id=article_id).one()
dbsession.close()
class BaseArticleForm(object):
def get_form(self):
from forms import CreateArticle
self.session = get_session(self.request)
self.session['csrf'] = get_csrf_token(self.session)
schema = CreateArticle().bind(request=self.request)
submit = deform.Button(name='submit',
css_class='blog-form__button')
self.form = deform.Form(schema, buttons=(submit,))
return self.form
@wsgify
class BlogIndex(object):
def __init__(self, request):
self.page = request.GET.get('page', '1')
from paginate import Page
dbsession = Session()
articles = dbsession.query(Articles).all()
self.paged_articles = Page(
articles,
page=self.page,
items_per_page=8,
)
dbsession.close()
def response(self):
return Response(env.get_template('index.html')
.render(articles=self.paged_articles))
@wsgify
class BlogCreate(BaseArticleForm):
def __init__(self, request):
self.request = request
def response(self):
if self.request.method == 'POST':
submitted = self.request.POST.items()
try:
self.get_form().validate(submitted)
except deform.ValidationFailure as e:
return Response(
env.get_template('create.html').render(form=e.render()))
article = Articles(**{'title': self.request.POST['title'],
'content': self.request.POST['content']
})
dbsession = Session()
dbsession.add(article)
dbsession.commit()
dbsession.close()
self.session = get_session(self.request).pop('csrf')
return Response(status=302, location='/')
return Response(env.get_template('create.html')
.render(form=self.get_form().render()))
@wsgify
class BlogRead(BaseArticle):
def response(self):
if not self.article:
return Response(status=404)
return Response(env.get_template('read.html')
.render(article=self.article))
@wsgify
class BlogUpdate(BaseArticle, BaseArticleForm):
def response(self):
if self.request.method == 'POST':
submitted = self.request.POST.items()
try:
self.get_form().validate(submitted)
except deform.ValidationFailure as e:
return Response(
env.get_template('create.html').render(form=e.render()))
self.article.title = self.request.POST['title']
self.article.content = self.request.POST['content']
dbsession = Session()
dbsession.add(self.article)
dbsession.commit()
dbsession.close()
self.session = get_session(self.request).pop('csrf')
return Response(status=302, location='/')
return Response(
env.get_template('create.html')
.render(form=self.get_form().render(
self.article.__dict__)))
@wsgify
class BlogDelete(BaseArticle):
def response(self):
dbsession = Session()
dbsession.delete(self.article)
dbsession.commit()
dbsession.close()
return Response(status=302, location='/') |
На создание Pyramid оказали влияние такие фреймворки, как Zope, Pylons и Django. Код Pyramid разрабатывался в проекте repoze.bfg, а название поменялось в результате слияния проектов BFG и Pylons, по решению встречи разработчиков в отеле Luxor (который имеет форму пирамиды, откуда и пошло название фреймворка) в Лас-Вегасе, в 2010 году.
См.также
Пирамида — самый молодой фреймворк для синхронного Веба среди популярных
Python фреймворков. Разработчики из сообщества Pylons не стали развивать тупиковую ветвь каркасных
фреймворков с жестко заданной архитектурой, к которым относятся Pylons и
например Ruby On Rails, поняли ошибки монолитных тяжелых фреймворков типа
Zope или Django и создали минималистичный, очень гибкий, но, в то же
время, легко расширяемый инструмент, сконцентрировав свои усилия на основных
задачах фреймворка, как: обработка маршрутов, простой и расширяемый конфиг,
система событий и middleware (tweens), простая система авторизации
построенная на ACL, возможность задания маршрутов динамически в виде
бинарного дерева и привязки их к ресурсам. Всеми остальными задачами занимаются
сторонние библиотеки. По требованию программиста можно выбрать любой ORM
для работы с БД, любой шаблонизатор, придумать любую схему авторизации и прочее.
Такой подход не ограничивает программиста в архитектуре проекта, в выборе инструментов и не заставляет для решения узких задач тянуть множество ненужных зависимостей и функционала.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
def hello(request):
return Response('Hello world!')
if __name__ == '__main__':
config = Configurator()
config.add_route('hello_world', '/')
config.add_view(hello, route_name='hello_world')
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8000, app)
server.serve_forever() |
Сохраните код в файл helloworld.py
и запустите его (python
helloworld.py
). Теперь приложение доступно на 8000 порту. По адресу
http://localhost:8000/ отобразится «Hello World».
Так просто, в одном файле, запустить Веб приложение не получится ни в Django, ни в Zope, это можно сравнить разве что с WSGI приложением или минималистичным фреймворком Bottle, который сильно уступает пирамиде по возможностям масштабирования.
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
Pyramid приложение начинается с конфига, который создается при помощи класса
Configurator
из модуля pyramid.config
.
В дальнейшем, экземпляр класса Configurator
используется для настройки.
Как и многие другие Веб-фреймворки на Python, Pyramid использует WSGI
протокол для связи приложения и веб-сервера. В этом примере выбран
Веб-сервер wsgiref
для удобства, т.к. он встроен в Python.
pyramid.response.Response
копия класса Response
из библиотеки webob. Используется для формирования HTTP ответа.
def hello(request):
return Response('Hello world!')
Такой тип представления в Pyramid называется view callable, принимает в
качестве аргумента объект класса pyramid.request.Request
(который
наследуется от webob.request.BaseRequest
) и возвращает объект HTTP
ответа pyramid.response.Response
.
config = Configurator()
1 2 | config.add_route('hello_world', '/')
config.add_view(hello, route_name='hello_world') |
В первой строчке вызывается метод конфигуратора
pyramid.config.Configurator.add_route()
, который регистрирует новый
маршрут (route) с названием «hello_world» и привязывает его к корневому
пути сайта «/».
Вторая строка регистрирует функцию hello(request)
как view callable
и привязывает ее к маршруту «hello_world». Теперь при обращении по URL адресу
http://localhost:8000/ будет запускаться функция hello
с переданным ей
объектом запроса request
.
1 | app = config.make_wsgi_app() |
Метод pyramid.config.Configurator.make_wsgi_app()
формирует WSGI
приложение из информации, которая хранится в конфигураторе. В дальнейшем,
благодаря спецификации WSGI (PEP 333), можно запустить это приложение на
любом совместимом Веб-сервере.
1 2 | server = make_server('0.0.0.0', 8000, app)
server.serve_forever() |
WSGI-сервер wsgiref
принимает первым параметром адрес „0.0.0.0“
(доступен извне, в отличие от „127.0.0.1“ по умолчанию), вторым — порт „8000“,
третий параметр — это WSGI-приложение, в пирамиде конечное приложение является
объектом класса pyramid.router.Router
(Router).
Функция serve_forever
запускает WSGI приложение.
Мы написали очень простое Веб-приложение используя Pyramid фреймворк и
настроив его императивно, это означает что настройки были прописаны напрямую в
объект конфигуратора (класс pyramid.config.Configurator
).
Как и обычный список настроек, конфигуратор инициализирует начальные значения (либо из файла «*.ini», либо через параметры класса). Отличительной особенностью конфигуратора является то, что по мере выполнения программы настройки могут меняться или добавляться.
В Pyramid существует 2 способа настройки приложений Императивный и Декларативный.
Императивный способ конфигурации означает что команды языка Python будут выполнены одна за другой, последовательно. Ниже пример простого приложения на Пирамиде сконфигурированного императивно.
Примечание
Пример будет доступен по адресу http://localhost:8080/hello/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
def hello_world(request):
return Response('Hello world!')
if __name__ == '__main__':
config = Configurator()
config.add_route('myHelloRoute', '/hello/')
config.add_view(hello_world, route_name='myHelloRoute')
# Создаем и запускаем WSGI приложение
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever() |
Иногда бывает сложно выполнить все настройки императивно в одном месте, т.к. приложение обычно состоит из множества файлов. В таком случае, вам придется постоянно перескакивать между файлами, чтобы посмотреть настройки для блока кода из другого файла. Чтобы этого избежать, фреймворк Pyramid позволяет настраивать приложение декларативных способом (configuration decoration), т.е. добавлять настройки как можно ближе к целевому коду, как показано в примере ниже:
1 2 3 4 5 6 | from pyramid.response import Response
from pyramid.view import view_config
@view_config(route_name='myHelloRoute')
def hello_world(request):
return Response('Hello') |
Сам по себе декоратор pyramid.view.view_config
не произведет ни какого
эффекта. Чтобы приложение нашло и применило эти настройки нужно выполнить
метод pyramid.config.Configurator.scan()
(scan). После выполнения
этот метод проходит по всем нижележащим файлам от текущей директории, ищет
декларативное описание настроек и применяет их к проекту.
1 2 3 4 5 6 7 8 9 10 11 12 13 | from wsgiref.simple_server import make_server
from pyramid.config import Configurator
if __name__ == '__main__':
config = Configurator()
config.add_route('myHelloRoute', '/hello/')
config.scan()
# Создаем и запускаем WSGI приложение
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever() |
В примере выше декоратор view_config
делает то же что
метод pyramid.config.Configurator.add_view()
но более наглядно:
config.add_view(hello_world, route_name='myHelloRoute')
Можно этот пример записать в одном файле:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.view import view_config
@view_config(route_name='myHelloRoute')
def hello_world(request):
return Response('Hello world!')
if __name__ == '__main__':
config = Configurator()
config.add_route('myHelloRoute', '/hello/')
config.scan()
# Создаем и запускаем WSGI приложение
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever() |
Вы можете выбрать любой способ конфигурации, который вам понравится. Оба способа полностью эквивалентны и часто используются совместно, решая разные задачи конфигурации приложения более удобным способом.
Хотя не составляет большой трудности написать Pyramid-приложение (проект) с нуля, Pyramid имеет инструменты для инициализации кода нового приложения по выбранному шаблону, или, в терминологии Pyramid, каркасной структуре (scaffolds). Например, в поставке имеются каркасные структуры для проектов, использующих ZODB или SQLAlchemy.
Проект — это каталог, содержащий по крайней мере один пакет на Python.
Типичная структура каталога для небольшого проекта:
MyProject/
|-- CHANGES.txt
|-- development.ini
|-- MANIFEST.in
|-- myproject
| |-- __init__.py
| |-- static
| | |-- favicon.ico
| | |-- logo.png
| | `-- pylons.css
| |-- templates
| | `-- mytemplate.pt
| |-- tests.py
| `-- views.py
|-- production.ini
|-- README.txt
|-- setup.cfg
`-- setup.py
Приведённую структуру, как следует из документации, не следует сильно изменять,
так как это может помешать другим разработчикам быстро ориентироваться в коде
проекта. Тем не менее, растущий проект может потребовать некоторых изменений.
Например, виды, модели (если они используются) и тесты можно, разбив на модули,
перенести соответственно в подкаталоги views, models и tests (не забыв снабдить
их файлом __init__.py
).
Следует отметить, что Pyramid может работать с любым WSGI-сервером. Проекты, созданные по готовым каркасным структурам, используют сервер Waitress.
Список официальных шаблонов найти по адресу https://github.com/Pylons?q=cookiecutter.
starter
zodb
alchemy
Некоторые пакеты могут дополнять этот список, например Cornice (https://github.com/Cornices/cookiecutter-cornice).
См.также
Cookiecutter - утилита позволяющая создавать проекты из шаблонов.
Установка Linux:
$ sudo apt install cookiecutter
Установка через Python:
$ pip install cookiecutter --user
Установка через Nix:
$ nix-env -i cookiecutter
$ cookiecutter gh:Pylons/pyramid-cookiecutter-starter
project_name [Pyramid Scaffold]: myproject
repo_name [myproject2]: myproject
Select template_language:
1 - jinja2
2 - chameleon
3 - mako
Choose from 1, 2, 3 [1]: 3
===============================================================================
Documentation: https://docs.pylonsproject.org/projects/pyramid/en/latest/
Tutorials: https://docs.pylonsproject.org/projects/pyramid_tutorials/en/latest/
Twitter: https://twitter.com/PylonsProject
Mailing List: https://groups.google.com/forum/#!forum/pylons-discuss
Welcome to Pyramid. Sorry for the convenience.
===============================================================================
Change directory into your newly created project.
cd myproject
Create a Python virtual environment.
python3 -m venv env
Upgrade packaging tools.
env/bin/pip install --upgrade pip setuptools
Install the project in editable mode with its testing requirements.
env/bin/pip install -e ".[testing]"
Run your project's tests.
env/bin/pytest
Run your project.
env/bin/pserve development.ini
myproject/
├── CHANGES.txt
├── development.ini
├── MANIFEST.in
├── myproject
│ ├── __init__.py
│ ├── static
│ │ ├── pyramid-16x16.png
│ │ ├── pyramid.png
│ │ └── theme.css
│ ├── templates
│ │ ├── layout.mako
│ │ └── mytemplate.mako
│ ├── tests.py
│ └── views.py
├── production.ini
├── pytest.ini
├── README.txt
└── setup.py
3 directories, 15 files
$ cd myproject
$ python setup.py develop
Часть настоек проекта, которые часто меняются, находится в файле development.ini
.
$ pserve development.ini
Starting server in PID 16601.
serving on http://0.0.0.0:6543
Ниже показан пример настроек сервера. Сервер Waitress запустит
MyProject.main
по адресу 127.0.0.1
и порту 6543
.
development.ini
¶[server:main]
use = egg:waitress#main
host = 127.0.0.1
port = 6543
Для автоматического перезапуска сервера после изменения файлов нужно указать флаг --reload
.
$ pserve development.ini --reload
Starting subprocess with file monitor
Starting server in PID 16601.
serving on http://0.0.0.0:6543
Теперь, после изменения какого-либо из файлов .py
или .ini
, сервер
перезапустится автоматически.
development.ini changed; reloading...
-------------------- Restarting --------------------
Starting server in PID 16602.
serving on http://0.0.0.0:6543
После запуска приложения через pserve
, можно открыть страницу
http://localhost:6543/ в браузере.
from pyramid.config import Configurator
if __name__ == '__main__':
settings = {
'debug_all': True,
'default_locale_name': 'ru',
}
config = Configurator(settings=settings)
При помощи настоек можно поменять поведение проекта. Некоторые настройки используются самой пирамидой, другие нужны для вашего личного использования. Настройки можно задавать в виде Python словаря или переменных окружения.
Переменная окружения | Словарь Python | Назначение |
---|---|---|
PYRAMID_RELOAD_ASSETS | pyramid.reload_assets или reload_assets | Не кэширует статику если «true» |
PYRAMID_DEBUG_AUTHORIZATION | pyramid.debug_authorization или debug_authorization | Выводит информацию об ошибках и успехах авторизации в stderr если «true» |
PYRAMID_DEBUG_NOTFOUND | pyramid.debug_notfound или debug_notfound | Выводит отладочтную информацию связанную с исключением NotFound в stderr если «true» |
PYRAMID_DEBUG_ROUTEMATCH | pyramid.debug_routematch или debug_routematch | Показывает отладочную информацию о маршрутах если «true» |
PYRAMID_PREVENT_HTTP_CACHE | pyramid.prevent_http_cache или prevent_http_cache | Не использует закешированные запросы если «true» |
PYRAMID_DEBUG_ALL | pyramid.debug_all или debug_all | Активирует все настройки начинающиеся с «debug…» |
PYRAMID_DEFAULT_LOCALE_NAME | pyramid.default_locale_name или default_locale_name | локаль по умолчанию, например pyramid.default_locale_name = ru |
Приложение на Pyramid можно дополнять при помощи сторонних модулей или
собственных функций. Делается это при помощи метода конфигуратора
pyramid.config.Configurator.include()
или настройки pyramid.includes
.
Метод include()
вызывает функцию
moreconfiguration
и передает ей в качестве параметра объект конфигуратора.
Тем самым как бы являясь продолжением этого конфига. Такой способ называется
императивное расширение приложения.
def moreconfiguration(config):
config.add_route('goodbye', '/goodbye')
config.add_view(goodbye, route_name='goodbye')
config = Configurator()
config.add_route('home', '/')
config.add_view(hello_world, route_name='home')
config.include(moreconfiguration)
app = config.make_wsgi_app()
Функции можно добавлять и из других модулей приложения.
# myapp.mysubmodule.views.py
def my_view(request):
from pyramid.response import Response
return Response('OK')
def includeme(config):
config.add_view(my_view)
Теперь в include
указывается путь до нашей функции с расширением конфига.
Причем его можно указать в виде строки.
from pyramid.config import Configurator
def main(global_config, **settings):
config = Configurator()
config.include('myapp.mysubmodule.views.includeme')
Конфигуратор по умолчанию ищет функцию includeme
поэтому ее можно опустить.
from pyramid.config import Configurator
def main(global_config, **settings):
config = Configurator()
config.include('myapp.mysubmodule.views')
В ini файл настроек приложения, помимо самих настроек, так же можно включить
расширения при помощи параметра pyramid.includes
.
[app:main]
pyramid.includes = pyramid_debugtoolbar
pyramid_tm
Этот код эквивалентен следующему:
from pyramid.config import Configurator
def main(global_config, **settings):
config = Configurator(settings=settings)
# ...
config.include('pyramid_debugtoolbar')
config.include('pyramid_tm')
# ...
Для пирамиды существует огромное множество сторонних модулей которые расширяю функционал вашего приложения. Вот список некоторых их них https://github.com/uralbash/awesome-pyramid.
Примечание
Сторонние модули устанавливаются как обычные python пакеты в ваше окружение. Например:
pip install pyramid_debugtoolbar pyramid_jinja2 pyramid_sacrud
Например добавляя config.include('pyramid_debugtoolbar')
мы получаем
отладочную консоль с Веб интерфейсом.
Или админку config.include('pyramid_sacrud')
.
config.include('pyramid_jinja2')
добавляет Jinja2 рендерер для ваших view.
@view_config(renderer='templates/mytemplate.jinja2')
def my_view(request):
return {'foo': 1, 'bar': 2}
И так далее…
Настройки в конфигуратор предаются в виде обычного Python словаря.
from pyramid.config import Configurator
if __name__ == '__main__':
settings = {
'default_locale_name': 'ru',
'pyramid.includes': 'pyramid_debugtoolbar pyramid_tm',
}
config = Configurator(settings=settings)
print(config.registry.settings)
Модули pyramid_debugtoolbar
и pyramid_tm
автоматически добавляют свои
настройки при включении их в конфиг.
{'debug_authorization': False,
'debug_notfound': False,
'debug_routematch': False,
'debug_templates': False,
'debugtoolbar.button_style': '',
'debugtoolbar.debug_notfound': False,
'debugtoolbar.debug_routematch': False,
'debugtoolbar.enabled': True,
'debugtoolbar.exclude_prefixes': [],
'debugtoolbar.extra_global_panels': [],
'debugtoolbar.extra_panels': [],
'debugtoolbar.global_panels': [<class 'pyramid_debugtoolbar.panels.introspection.IntrospectionDebugPanel'>,
<class 'pyramid_debugtoolbar.panels.routes.RoutesDebugPanel'>,
<class 'pyramid_debugtoolbar.panels.settings.SettingsDebugPanel'>,
<class 'pyramid_debugtoolbar.panels.tweens.TweensDebugPanel'>,
<class 'pyramid_debugtoolbar.panels.versions.VersionDebugPanel'>],
'debugtoolbar.hosts': ['127.0.0.1', '::1'],
'debugtoolbar.includes': (),
'debugtoolbar.intercept_exc': 'debug',
'debugtoolbar.intercept_redirects': False,
'debugtoolbar.max_request_history': 100,
'debugtoolbar.max_visible_requests': 10,
'debugtoolbar.panels': [<class 'pyramid_debugtoolbar.panels.headers.HeaderDebugPanel'>,
<class 'pyramid_debugtoolbar.panels.logger.LoggingPanel'>,
<class 'pyramid_debugtoolbar.panels.performance.PerformanceDebugPanel'>,
<class 'pyramid_debugtoolbar.panels.renderings.RenderingsDebugPanel'>,
<class 'pyramid_debugtoolbar.panels.request_vars.RequestVarsDebugPanel'>,
<class 'pyramid_debugtoolbar.panels.sqla.SQLADebugPanel'>,
<class 'pyramid_debugtoolbar.panels.traceback.TracebackPanel'>],
'debugtoolbar.prevent_http_cache': False,
'debugtoolbar.reload_assets': False,
'debugtoolbar.reload_resources': False,
'debugtoolbar.reload_templates': False,
'default_locale_name': 'ru',
'prevent_cachebust': False,
'prevent_http_cache': False,
'pyramid.debug_authorization': False,
'pyramid.debug_notfound': False,
'pyramid.debug_routematch': False,
'pyramid.debug_templates': False,
'pyramid.default_locale_name': 'ru',
'pyramid.includes': 'pyramid_debugtoolbar pyramid_tm',
'pyramid.prevent_cachebust': False,
'pyramid.prevent_http_cache': False,
'pyramid.reload_assets': False,
'pyramid.reload_resources': False,
'pyramid.reload_templates': False,
'reload_assets': False,
'reload_resources': False,
'reload_templates': False}
См.также
Настройки можно хранить в ini файле, включая туда не только настройки пирамиды, но и настройки веб сервера. Т.е. мы указывает веб серверу как запускать наше приложение на Pyramid.
В разделе [app:main]
указываются настройки Pyramid приложения.
###
# app configuration
# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
use = egg:MyProject
pyramid.reload_templates = true
pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
pyramid.includes =
pyramid_debugtoolbar
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
В разделе [server:main]
указываются настройки веб сервера совместимые с
Paste Deployment.
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
host = 0.0.0.0
port = 6543
Запуск приложения.
pserver development.ini
Утилита pserve
читает конфиг, вызывает функцию MyProject.main
с
настройками из этого конфига, получает WSGI приложение от этой функции и
запускает его, в нашем случае, при помощи веб сервера waitress.
from pyramid.config import Configurator
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config = Configurator(settings=settings)
...
return app
Pyramid предоставляет очень мощную систему настройки вашего приложения, а так
же простой и гибкий метод расширения функционала при помощи
include()
.
Сам фреймворк Pyramid не имеет встроенных возможностей работы с базами данных, в отличии от таких фреймворков как Django (Django ORM) и Ruby on Rails (Active Record). Хорошим выбором для реляционных БД будет ORM SQLAlchemy.
Организация БД в пирамиде не зависит от фреймворка, поэтому можно использовать любую структуру, которая вам удобна. Ниже я приведу один из вариантов, более подробно про SQLAlchemy можно прочитать в разделе SQLAlchemy ORM.
Вынесем модели и то что касается соединения с БД в отдельный файл models.py
.
# models.py
from sqlalchemy import Column, Integer, Text, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
engine = create_engine('sqlite:///foo.db')
Session = sessionmaker()
Base = declarative_base(bind=engine)
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(Text)
def __repr__(self):
return self.name
В представлениях мы просто создаем объект
sqlalchemy.orm.session.Session
и работаем с объектами, как описано в
документации SQLAlchemy. При этом в каждом представлении нам необходимо
создавать новую SQLAlchemy сессию, а если были изменения подтверждать их при
помощи метода sqlalchemy.orm.session.Session.commit()
.
# __init__.py
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
from models import User, Session, Base, engine
def hello(request):
DBSession = Session(bind=engine)
result = DBSession.query(User).all()
import time
timestamp = int(time.time())
new_user = User(name=str(timestamp))
DBSession.add(new_user)
DBSession.commit()
return Response(str(result))
if __name__ == '__main__':
Base.metadata.create_all()
DBSession = Session(bind=engine)
DBSession.add(User(name='Vasya'))
DBSession.add(User(name='Petya'))
DBSession.commit()
config = Configurator()
config.add_route('hello_world', '/')
config.add_view(hello, route_name='hello_world')
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8000, app)
server.serve_forever()
Данный пример при каждом обновлении делает новую запись в БД и отдает их браузеру.
См.также
ZopeTransactionExtension
это расширение для SQLAlchemy, которое привязывает
сессии к универсальному менеджеру транзакций transaction.
Добавим его в наш пример:
# models.py
from sqlalchemy import Column, Integer, Text, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from zope.sqlalchemy import ZopeTransactionExtension
engine = create_engine('sqlite:///foo.db')
Session = sessionmaker(bind=engine,
extension=ZopeTransactionExtension())
Base = declarative_base(bind=engine)
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(Text)
def __repr__(self):
return self.name
Теперь вместо DBSession.commit
, нужно использовать transaction.commit()
.
# __init__.py
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
from models import User, Session, Base, engine
import transaction
def hello(request):
DBSession = Session(bind=engine)
result = str(DBSession.query(User).all())
import time
timestamp = int(time.time())
new_user = User(name=str(timestamp))
DBSession.add(new_user)
transaction.commit()
return Response(result)
if __name__ == '__main__':
Base.metadata.create_all()
DBSession = Session(bind=engine)
DBSession.add(User(name='Vasya'))
DBSession.add(User(name='Petya'))
transaction.commit()
config = Configurator()
config.add_route('hello_world', '/')
config.add_view(hello, route_name='hello_world')
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8000, app)
server.serve_forever()
Теперь мы используем общий, глобальный менеджер транзакций, который работает не только с SQLAlchemy но и со всеми модулями которые его поддерживают. Ниже пример сессии в которой одновременно участвуют SQLAlchemy и pyramid_mailer.
Внимание
Пример ниже работает с версией repoze.sendmail==4.1
. Установить ее можно
припомощи команды:
pip install repoze.sendmail==4.1
Если возникает ошибка raise ValueError("TPC in progress")
, cмотри
https://github.com/repoze/repoze.sendmail/issues/31
# __init__.py
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
from models import User, Session, Base, engine
import transaction
from pyramid_mailer.message import Message
message = Message(subject="hello world",
sender="example@yandex.ru",
recipients=["me@uralbash.ru"],
body="hello, uralbash")
def hello(request):
DBSession = Session(bind=engine)
result = str(DBSession.query(User).all())
import time
timestamp = int(time.time())
new_user = User(name=str(timestamp),
id=100500)
DBSession.add(new_user)
from pyramid_mailer import get_mailer
mailer = get_mailer(request)
mailer.send(message)
try:
transaction.commit()
except Exception as e:
transaction.abort()
return Response(str(e))
return Response(result)
if __name__ == '__main__':
Base.metadata.create_all()
DBSession = Session(bind=engine)
DBSession.add(User(name='Vasya'))
DBSession.add(User(name='Petya'))
transaction.commit()
settings = {'mail.host': 'smtp.yandex.ru',
'mail.port': '465',
'mail.ssl': True,
'pyramid_mailer.prefix': 'mail.',
'mail.username': 'example@yandex.ru',
'mail.password': 'example password'}
config = Configurator(settings=settings)
config.include('pyramid_mailer')
config.add_route('hello_world', '/')
config.add_view(hello, route_name='hello_world')
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8000, app)
server.serve_forever()
В этом примере вьюха hello
записывает нового пользователя с id=100500
в
БД и отправляет письмо на адрес me@uralbash.ru
. При первом обновлении
страницы пользователь добавится в БД и отправится письмо. При последующих
обновлениях произойдет ошибка т.к. пользователь с таким id
уже существует,
при этом transaction.abort()
откатит изменения как в сессии
SQLAlchemy, так и в сессии pyramid_mailer, поэтому письмо не
отправится.
pyramid_tm автоматически подтверждает транзакцию в каждом запросе. Т.е.
если мы забыли написать transaction.commit()
, то он все равно вызовется,
при этом мы также можем вызывать его явно.
# __init__.py
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
from models import User, Session, Base, engine
def hello(request):
DBSession = Session(bind=engine)
result = str(DBSession.query(User).all())
import time
timestamp = int(time.time())
new_user = User(name=str(timestamp))
DBSession.add(new_user)
return Response(result)
if __name__ == '__main__':
Base.metadata.create_all()
DBSession = Session(bind=engine)
DBSession.add(User(name='Vasya'))
DBSession.add(User(name='Petya'))
config = Configurator()
config.include('pyramid_tm')
config.add_route('hello_world', '/')
config.add_view(hello, route_name='hello_world')
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8000, app)
server.serve_forever()
pyramid_sqlalchemy создает объект базового класса Base
и сессии
Session
автоматически. Мы просто указываем строку подключения к БД в
настройках и включаем модуль pyramid_sqlalchemy
в проект.
# __init__.py
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
from models import User
from pyramid_sqlalchemy import BaseObject, Session
import transaction
def hello(request):
result = str(Session.query(User).all())
import time
timestamp = int(time.time())
new_user = User(name=str(timestamp))
Session.add(new_user)
return Response(result)
if __name__ == '__main__':
settings = {'sqlalchemy.url': 'sqlite:///:memory:'}
config = Configurator(settings=settings)
config.include('pyramid_tm')
config.include('pyramid_sqlalchemy')
BaseObject.metadata.create_all()
Session.add(User(name='Vasya'))
Session.add(User(name='Petya'))
transaction.commit()
config.add_route('hello_world', '/')
config.add_view(hello, route_name='hello_world')
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8000, app)
server.serve_forever()
Файл с моделями теперь выглядит значительно проще.
# models.py
from sqlalchemy import Column, Integer, Text
from pyramid_sqlalchemy import BaseObject
class User(BaseObject):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(Text)
def __repr__(self):
return self.name
Несмотря на то, что фреймворк Pyramid не предоставляет инструментов для работы с базами дынных, есть большое количество сторонних модулей и расширений (написанных специально для Pyramid) которые реализуют этот функционал.
Для быстрого старта существует шаблон проекта alchemy
, по которому можно
быстро начать использовать пирамиду вместе с SQLAlchemy.
$ pcreate --scaffold alchemy sqla_demo
$ cd sqla_demo
$ python setup.py develop
Дополнительную информацию можно найти в Pyramid Cookbook.
См.также
Каждый поступающий на сервер приложений Pyramid запрос (request) должен найти вид (view), который и будет его обрабатывать.
В Pyramid имеется два базовых подхода к поиску нужного вида для обрабатываемого запроса: на основе сопоставления (matching), как в большинстве подобных фреймворков, и обхода (traversal), как в Zope. Кроме того, в одном приложении можно с успехом сочетать оба подхода.
Простейший пример с заданием маршрута (заимствован из документации):
# Здесь config - экземпляр pyramid.config.Configurator
config.add_route('idea', 'site/{id}')
config.add_view('mypackage.views.site_view', route_name='idea')
Использование обхода лучше проиллюстрировать на небольшом примере:
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
# Класс некоторого ресурса
class Resource(dict):
pass
# Дерево ресурсов (жёстко закодированное) в фабрике корня
def get_root(request):
return Resource({'a': Resource({'b': Resource({'c': Resource()})})})
# Вид-для-вызова, который умеет показывать ресурс Resource (в context)
def hello_world_of_resources(context, request):
output = "Ресурс и его дети: %s" % context
return Response(output)
if __name__ == '__main__':
config = Configurator(root_factory=get_root)
config.add_view(hello_world_of_resources, context=Resource)
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever()
В этом примере иерархия для обхода жестко задана в методе get_root
с
помощью вложенных словарей, тогда как реальные приложения должны сами
определять необходимый доступ по ключам (метод __getitem__
помогает
организовать такой доступ). В коде также присутствует корневая фабрика, с
которой собственно и начинается обход узлов (node) дерева ресурсов.
Вид-для-вызова (view callable) представлен функцией
hello_world_of_resources
. Говоря несколько упрощённо, на основе URL запроса
в результате обхода иерархии Pyramid находит ресурс и применяет к нему
«наилучший» вид-для-вызова (в нашем примере — он единственный).
Предупреждение
В примерах используется Python3
Метод обхода дерева (traversal), позволяет находить ресурсы во вложенных структурах. Такой механизм хорошо применим для некоторых практических задач, например список всех URL маршрутов сайта является деревом, в котором каждый конечный URL это отдельная ветка дерева. Поэтому всю структуру сайта можно поместить в словарь.
К примеру, известно, что смерь сказочного персонажа «кащея» находится в яйце, которое в свою очередь в утке, которая в зайце и т.д. В сумме получается вложенная структура, которую можно описать так:
остров -> дуб -> сундук -> заяц -> утка -> яйцо -> игла -> СмертьКощея
Мы можем такую, плоскую, вложенную структуру легко представить в виде URL:
http://localhost:8080/mytraversal/остров/дуб/сундук/заяц/утка/яйцо/игла
А так-как любой URL является веткой дерева, то несложно описать это в Python:
{
'остров': {
'дуб': {
'сундук': {
'заяц': {
'утка': {
'яйцо': {
'игла': СмертьКощея()
}
}
}
}
}
}
}
СмертьКощея()
- это объект класса СмертьКощея
, который может выглядеть к примеру так:
class СмертьКощея(object):
def __json__(self, request):
return {
'имя': 'кощей',
'статус': request.context == self and 'мертв' or 'жив ещё',
}
В принципе, этого достаточно чтобы наш сайт-убийца «кощея» заработал. Осталось лишь прописать пути и добавить представление (view).
View будет просто возвращать объект, например, если мы ввели:
URL: 'остров'
объект: {'дуб': {
'сундук': {
'заяц': {
'утка': {
'яйцо': {
'игла': СмертьКощея()
}
}
}
}
}}
URL: 'остров/дуб'
объект: {'сундук': {
'заяц': {
'утка': {
'яйцо': {
'игла': СмертьКощея()
}
}
}
}}
URL: 'остров/дуб/сундук'
объект: {'заяц': {
'утка': {
'яйцо': {
'игла': СмертьКощея()
}
}
}}
URL: 'остров/дуб/сундук/заяц'
объект: {'утка': {
'яйцо': {
'игла': СмертьКощея()
}
}}
URL: 'остров/дуб/сундук/заяц/утка'
объект: {'яйцо': {
'игла': СмертьКощея()
}}
URL: 'остров/дуб/сундук/заяц/утка/яйцо'
объект: {'игла': СмертьКощея()}
URL: 'остров/дуб/сундук/заяц/утка/яйцо/игла'
объект: СмертьКощея()
Такие функции-представления (View) должны принимать 2 параметра, где первый
параметр будет являться объектом, обычно именуемым context
, а второй
параметр request
:
def traverse_koshey(context, request):
return context # Наш объект
Роуты создаются почти так же как в pattern matching
, за исключением того,
что структура путей передается в виде «фабрики», которая возвращает словарь или
ему подобный (dict-like) объект. Путь указывается в виде статической и
динамической части, например /fixedpath/*traverse
:
config.add_route('koshey', '/mytraversal/*traverse', factory=my_factory)
Фабрика, которая возвращает структуру сайта:
def my_factory(request):
return {
'остров': {
'дуб': {
'сундук': {
'заяц': {
'утка': {
'яйцо': {
'игла': СмертьКощея()
}
}
}
}
}
}
}
View добавляется стандартно:
config.add_view(traverse_koshey, route_name='koshey', renderer='json')
Все готово, можно перемещаться по объектам:
Полный пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | from wsgiref.simple_server import make_server
from pyramid.config import Configurator
def traverse_koshey(context, request):
return context
class СмертьКощея(object):
def __json__(self, request):
return {
'имя': 'кощей',
'статус': request.context == self and 'мертв' or 'жив ещё',
}
def my_factory(request):
return {
'остров': {
'дуб': {
'сундук': {
'заяц': {
'утка': {
'яйцо': {
'игла': СмертьКощея()
}
}
}
}
}
}
}
if __name__ == '__main__':
config = Configurator()
# Traversal routing
config.add_route('koshey', '/mytraversal/*traverse', factory=my_factory)
config.add_view(traverse_koshey, route_name='koshey', renderer='json')
# Make app and serve
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever() |
Есть один нюанс, json renderer, по умолчанию, все не латинские символы отображает
как UTF коды \uxxxx
, поэтому мы увидим следующий вывод:
{"\u043e\u0441\u0442\u0440\u043e\u0432": {"\u0434\u0443\u0431": {"\u0441\u0443\u043d\u0434\u0443\u043a": {"\u0437\u0430\u044f\u0446": {"\u0443\u0442\u043a\u0430": {"\u044f\u0439\u0446\u043e": {"\u0438\u0433\u043b\u0430": {"\u0441\u0442\u0430\u0442\u0443\u0441": "\u0436\u0438\u0432 \u0435\u0449\u0451", "\u0438\u043c\u044f": "\u043a\u043e\u0449\u0435\u0439"}}}}}}}}
Но можно изменить его поведение следующим образом:
from pyramid.renderers import JSON
...
config.add_renderer('myjson', JSON(indent=4, ensure_ascii=False))
config.add_view(traverse_koshey, route_name='koshey', renderer='myjson')
Результат:
http://localhost:8080/mytraversal/
{
"остров": {
"дуб": {
"сундук": {
"заяц": {
"утка": {
"яйцо": {
"игла": {
"имя": "кощей",
"статус": "жив ещё"
}
}
}
}
}
}
}
}
http://localhost:8080/mytraversal/остров/дуб/сундук/заяц/утка/яйцо/игла
{
"имя": "кощей",
"статус": "мертв"
}
Полный код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.renderers import JSON
def traverse_koshey(context, request):
return context
class СмертьКощея(object):
def __json__(self, request):
return {
'имя': 'кощей',
'статус': request.context == self and 'мертв' or 'жив ещё',
}
def my_factory(request):
return {
'остров': {
'дуб': {
'сундук': {
'заяц': {
'утка': {
'яйцо': {
'игла': СмертьКощея()
}
}
}
}
}
}
}
if __name__ == '__main__':
config = Configurator()
# ensure_ascii JSON renderer
config.add_renderer('myjson', JSON(indent=4, ensure_ascii=False))
# Traversal routing
config.add_route('koshey', '/mytraversal/*traverse', factory=my_factory)
config.add_view(traverse_koshey, route_name='koshey', renderer='myjson')
# Make app and serve
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever() |
В Пирамиде объект (context) который передается во вью, именуют еще как «ресурс».
Есть возможность жестко привязать View к типу ресурса. Например, наше
представление traverse_koshey
должно вызываться, только когда пришел объект
класса СмертьКощея
:
config.add_view(traverse_koshey, route_name='koshey_context',
renderer='myjson',
context=СмертьКощея)
Параметр context
указывает на то, что это View принадлежит ТОЛЬКО объектам
класса СмертьКащея
.
Все пути, кроме полного (который возвращает нужный объект), вернут 404 код ответа. Полный путь http://localhost:8080/mytraversal/остров/дуб/сундук/заяц/утка/яйцо/игла.
Добавим в нашу структуру еще ресурсов:
def my_factory(request):
return {
'превед': Человек(),
'остров': {
'ясень': {
'что то здесь': 'не так!'
},
'дуб': {
'сундук': {
'заяц': {
'утка': {
'яйцо': {
'игла': СмертьКощея()
}
}
}
}
}
}
}
Здесь Человек()
это новый тип ресурса, который имеет метод __getitem__
как у словаря и при обращении по ключу возвращает другой ресурс:
class Человек(object):
name = 'Человек'
def __getitem__(self, name):
return Имя(name)
class Имя(object):
__parent__ = Человек()
def __init__(self, name):
self.name = name
Например мы обращаемся по URL http://localhost:8080/mytraversal/превед/Пирамид.
превед
вернет ресурс Человек
, а Пирамид
вызовет метод
__getitem__
, который вернет ресурс Имя('Пирамид')
. Таким образом мы
можем строить дерево динамически при помощи dict-like объектов.
Для ресурса Имя
мы можем создать отдельное представление и жестко привязать
его к этому типу.
def traverse_hello(context, request):
"""
http://localhost:8080/mytraversal/первед/Пирамид
"""
return Response('Превед ' + context.__parent__.name + ' ' + context.name)
...
config.add_view(traverse_hello, route_name='koshey_context',
renderer='text',
context=Имя)
Результат вывода по адресу
http://localhost:8080/mytraversal/превед/Пирамид, будет обычный текст
(Content-Type: plain/text
):
Превед Человек Пирамид
Полный пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.renderers import JSON
def traverse_koshey(context, request):
"""
http://localhost:8080/mytraversal/остров/дуб/сундук/заяц/утка/яйцо/игла
"""
return context
def traverse_hello(context, request):
"""
http://localhost:8080/mytraversal/первед/Пирамид
"""
return Response('Превед ' + context.__parent__.name + ' ' + context.name)
class Человек(object):
name = 'Человек'
def __getitem__(self, name):
return Имя(name)
def __json__(self, request):
return {'name': self.name}
class Имя(object):
__parent__ = Человек()
def __init__(self, name):
self.name = name
def __json__(self, request):
return {'name': self.name}
class СмертьКощея(object):
def __json__(self, request):
return {
'имя': 'кощей',
'статус': request.context == self and 'мертв' or 'жив ещё',
}
def my_factory(request):
return {
'превед': Человек(),
'остров': {
'ясень': {
'что то здесь': 'не так!'
},
'дуб': {
'сундук': {
'заяц': {
'утка': {
'яйцо': {
'игла': СмертьКощея()
}
}
}
}
}
}
}
if __name__ == '__main__':
config = Configurator()
# ensure_ascii JSON renderer
config.add_renderer('myjson', JSON(indent=4, ensure_ascii=False))
# Traversal routing
config.add_route('koshey', '/mytraversal/*traverse', factory=my_factory)
config.add_view(traverse_koshey, route_name='koshey', renderer='myjson')
# Traversal routing with context constraint
config.add_route('koshey_context', '/mytraversal_context/*traverse',
factory=my_factory)
config.add_view(traverse_koshey, route_name='koshey_context',
renderer='myjson',
context=СмертьКощея)
config.add_view(traverse_hello, route_name='koshey_context',
renderer='text',
context=Имя)
# Make app and serve
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever() |
Фреймворк Pyramid
позволяет использовать оба способа URL маршрутизации
одновременно.
Добавим к примеру с «кащеем» hello world с использованием pattern matching:
def hello_world(request):
return Response('Hello %(name)s!' % request.matchdict)
...
# Pattern matching routes
config.add_route('hello', '/hello/{name}')
config.add_view(hello_world, route_name='hello')
Полный пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.renderers import JSON
def hello_world(request):
return Response('Hello %(name)s!' % request.matchdict)
def traverse_koshey(context, request):
"""
http://localhost:8080/mytraversal/остров/дуб/сундук/заяц/утка/яйцо/игла
"""
return context
def traverse_hello(context, request):
"""
http://localhost:8080/mytraversal/первед/Пирамид
"""
return Response('Превед ' + context.__parent__.name + ' ' + context.name)
class Человек(object):
name = 'Человек'
def __getitem__(self, name):
return Имя(name)
def __json__(self, request):
return {'name': self.name}
class Имя(object):
__parent__ = Человек()
def __init__(self, name):
self.name = name
def __json__(self, request):
return {'name': self.name}
class СмертьКощея(object):
def __json__(self, request):
return {
'имя': 'кощей',
'статус': request.context == self and 'мертв' or 'жив ещё',
}
def my_factory(request):
return {
'превед': Человек(),
'остров': {
'ясень': {
'что то здесь': 'не так!'
},
'дуб': {
'сундук': {
'заяц': {
'утка': {
'яйцо': {
'игла': СмертьКощея()
}
}
}
}
}
}
}
if __name__ == '__main__':
config = Configurator()
# Pattern matching routes
config.add_route('hello', '/hello/{name}')
config.add_view(hello_world, route_name='hello')
# ensure_ascii JSON renderer
config.add_renderer('myjson', JSON(indent=4, ensure_ascii=False))
# Traversal routing
config.add_route('koshey', '/mytraversal/*traverse', factory=my_factory)
config.add_view(traverse_koshey, route_name='koshey', renderer='myjson')
# Traversal routing with context constraint
config.add_route('koshey_context', '/mytraversal_context/*traverse',
factory=my_factory)
config.add_view(traverse_koshey, route_name='koshey_context',
renderer='myjson',
context=СмертьКощея)
config.add_view(traverse_hello, route_name='koshey_context',
renderer='text',
context=Имя)
# Make app and serve
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever() |
См.также
REST API
подразумевает под собой простые правила:
GET
возвращается описание этого
ресурсаPOST
добавляет новый ресурсPUT
изменяет ресурсDELETE
удаляет ресурсЭти правила предоставляют простой CRUD
интерфейс для других приложений,
взаимодействие с которым происходит через протокол HTTP
.
Соответствие CRUD
операций и HTTP
методов:
POST
GET
PUT
DELETE
REST API
интерфейс очень удобен для межпрограммного взаимодействия,
например мобильное приложение может выступать в роли клиента, который
манипулирует данными посредством REST
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | from wsgiref.simple_server import make_server
from pyramid.view import view_config, view_defaults
from pyramid.config import Configurator
@view_defaults(
route_name='rest_people',
renderer='json'
)
class RESTViewPeople(object):
def __init__(self, request):
self.request = request
@view_config(request_method='GET')
def get(self):
return {
'id': self.request.matchdict['id'],
'method': self.request.method,
'get': dict(self.request.GET)
}
@view_config(request_method='POST')
def post(self):
return {
'id': self.request.matchdict['id'],
'method': self.request.method,
'post': dict(self.request.POST)
}
@view_config(request_method='DELETE')
def delete(self):
return {'status': 'success'}
if __name__ == '__main__':
config = Configurator()
config.add_route('rest_people', '/api/v1/people/{id:\d+}')
config.add_view(RESTViewPeople, route_name='rest_people')
config.scan('.')
# make wsgi app
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever() |
Пример выше добавляет View с тремя методами, каждый из которых вызывается при
соответствующем GET
, POST
, DELETE
запросе.
Ресурсом здесь является конкретный человек, получить которого можно по URL
http://localhost:8080/api/v1/people/123
Результатом запроса будет:
{"get": {}, "id": "123", "method": "GET"}
Для отправки POST
запроса воспользуемся консольной утилитой curl:
$ curl -X POST -d 'param1=value1¶m2=value2' http://localhost:8080/api/v1/people/1
Результат запроса:
{"id": "1", "post": {"param1": "value1", "param2": "value2"}, "method": "POST"}
DELETE
запрос выполняется по аналогии:
$ curl -X DELETE http://localhost:8080/api/v1/people/1
Результат запроса:
{"status": "success"}
См.также
Метод URL диспетчеризации Traversal
В предыдущем примере показан только один ресурс - конкретный человек и в принципе все выглядит неплохо, пока не появится другой смежный ресурс, например список всех людей по адресу http://localhost:8080/api/v1/people
В этом случае, придется добавлять новый путь (rout), привязывать его к представлению (View) и самое неприятное менять само представление, или еще хуже писать новое. Таким образом с увеличением ресурсов, сложность REST API растет не пропорционально и в какой то момент код становится не читаемым из-за больших размеров и постоянно меняющейся логики во View.
Выход из данной ситуации - отделить ресурсы от представлений, тем самым вынести часть логики и сделать представления более универсальными.
Ресурсы могут выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 | class PeopleResource(object):
def __getitem__(self, people_id):
if str(people_id).isdigit():
return PersonResource(people_id)
def __json__(self, request):
return {
'params': request.matchdict,
'method': request.method,
} |
PeopleResource
представляет список всех людей и будет доступен по
адресу http://localhost:8080/api/v1/people.
PeopleResource
имеет метод __getitem__
, что делает его похожим на
словарь. При обращении к объекту ресурса как к словарю, он вызовет
эту функцию и передаст ключ в параметр people_id
, например:
foo = PeopleResource()
bar = foo[123] # Вернет объект PersonResource(123)
Метод __json__
определяет каким образом преобразовывать ресурс в json.
PersonResource
представляет конкретного человека и будет доступен по
адресу http://localhost:8080/api/v1/people/{id}. Здесь отличительной
особенностью является то, что метод __json__
наследует часть словаря из
класса PeopleResource
, при помощи конструкции super
:
1 2 3 4 5 6 7 8 9 10 | class PersonResource(PeopleResource):
def __init__(self, people_id):
self.id = people_id
def __json__(self, request):
return {
'id': self.id,
**super().__json__(request)
} |
Перепишем View таким образом, чтобы она возвращала только ресурс, а так-как
ресурс уже содержит в себе информацию как отдавать json, то это представление
будет универсальным как для PeopleResource
, так и для PersonResource
и возможно подойдет другим ресурсам которые мы будем писать в будущем.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @view_defaults(
route_name='rest_api',
renderer='json',
context=PeopleResource
)
class RESTViewPeople(object):
def __init__(self, context, request):
self.context = context
self.request = request
@view_config(request_method='GET')
def get(self):
return self.context
@view_config(request_method='POST')
def post(self):
return self.context
@view_config(request_method='DELETE')
def delete(self):
return {'status': 'success'} |
Рендерер json
по умолчанию ищет метод __json__
и если он есть то
возвращает его результат вызова.
Путь, в нашем случае, будет один, так-как вся структура вынесена в ресурсы
(метод __getitem__
).
config.add_route('rest_api', '/api/v1/*traverse', factory=rest_factory)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | from wsgiref.simple_server import make_server
from pyramid.view import view_config, view_defaults
from pyramid.config import Configurator
class PeopleResource(object):
def __getitem__(self, people_id):
if str(people_id).isdigit():
return PersonResource(people_id)
def __json__(self, request):
return {
'params': request.matchdict,
'method': request.method,
}
class PersonResource(PeopleResource):
def __init__(self, people_id):
self.id = people_id
def __json__(self, request):
return {
'id': self.id,
**super().__json__(request)
}
class AnimalsResource(object):
pass
@view_defaults(
route_name='rest_api',
renderer='json',
context=PeopleResource
)
class RESTViewPeople(object):
def __init__(self, context, request):
self.context = context
self.request = request
@view_config(request_method='GET')
def get(self):
return self.context
@view_config(request_method='POST')
def post(self):
return self.context
@view_config(request_method='DELETE')
def delete(self):
return {'status': 'success'}
def rest_factory(request):
return {
'people': PeopleResource(),
'animals': AnimalsResource(),
}
if __name__ == '__main__':
config = Configurator()
config.add_route('rest_api', '/api/v1/*traverse', factory=rest_factory)
config.add_view(RESTViewPeople, route_name='rest_api')
config.scan('.')
# make wsgi app
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever() |
См.также
Представления (views) создаются в виде функций или методов и могут находится в
любом месте проекта. В качестве аргумента функция принимает объект request
,
а возвращает объект response
:
from pyramid.response import Response
def my_view(request):
return Response("Hello, world!")
В классе, который содержит представления-методы, объект request
передается в конструктор:
class MyHandler(object):
def __init__(self, request):
self.request = request
def my_view(self):
return Response("Hello, classy world!")
route_name
Имя для привязки к роуту. Нужно если используется URL диспетчеризация.
renderer
Имя обработчика (string, json) или шаблон (index.jinja2, index.pt, index.mako).
permission
Строка и именем права доступа, которое текущий пользователь должен иметь чтобы вызвать это представление.
request_method
“GET”, “POST”, “PUT”, “DELETE’, “HEAD”.
request_param
Проверяет наличие параметров в запросе, например «foo» означает что в запросе должен быть параметр с именем «foo». «foo=1» означает что этот параметр должен быть равен 1.
match_param
Тоже что иrequest_param
но проверяет все параметры, в том числе которые пришли от URL диспетчеризации.
Декларативный способ задания представлений осуществляется при помощи
декораторов pyramid.view.view_config
и
pyramid.view.view_defaults
.
from pyramid.view import view_config
class Handler(object):
def __init__(self, request):
self.request = request
class Main(Handler):
@view_config(route_name="home", renderer="index.mako")
def index(self):
return {"project": "Akhet Demo"}
Функция или метод может быть привязана к нескольким представлениям.
class Main(Handler):
@view_config(route_name="home", renderer="index.mako")
@view_config(route_name="home_json", renderer="json")
def index(self):
return {"project": "Akhet Demo"}
Пример REST
from pyramid.view import view_defaults
from pyramid.view import view_config
from pyramid.response import Response
@view_defaults(route_name='rest')
class RESTView(object):
def __init__(self, request):
self.request = request
@view_config(request_method='GET')
def get(self):
return Response('get')
@view_config(request_method='POST')
def post(self):
return Response('post')
@view_config(request_method='DELETE')
def delete(self):
return Response('delete')
from pyramid.config import not_
...
config.add_view(Main.index, route_name="home", request_method=not_('POST'))
Пример REST.
from pyramid.response import Response
from pyramid.config import Configurator
class RESTView(object):
def __init__(self, request):
self.request = request
def get(self):
return Response('get')
def post(self):
return Response('post')
def delete(self):
return Response('delete')
def main(global_config, **settings):
config = Configurator()
config.add_route('rest', '/rest')
config.add_view(RESTView, route_name='rest', attr='get', request_method='GET')
config.add_view(RESTView, route_name='rest', attr='post', request_method='POST')
config.add_view(RESTView, route_name='rest', attr='delete', request_method='DELETE')
return config.make_wsgi_app()
from pyramid.view import view_defaults
from pyramid.response import Response
from pyramid.config import Configurator
@view_defaults(route_name='rest')
class RESTView(object):
def __init__(self, request):
self.request = request
def get(self):
return Response('get')
def post(self):
return Response('post')
def delete(self):
return Response('delete')
def main(global_config, **settings):
config = Configurator()
config.add_route('rest', '/rest')
config.add_view(RESTView, attr='get', request_method='GET')
config.add_view(RESTView, attr='post', request_method='POST')
config.add_view(RESTView, attr='delete', request_method='DELETE')
return config.make_wsgi_app()
См.также
В пирамиде нет встроенного шаблонизатора. Представления (view callable)
всегда отдают объект response
. Этот объект может формироваться напрямую,
например Response("Hello, world!")
. При помощи встроенных обработчиков
(string, json, jsonp), самописных или сторонних. Или через специальные функции,
например pyramid.renderers.render_to_response()
.
Дополнительные обработчики могут поставляться сторонними модулями:
config.include('pyramid_chameleon') # Chameleon - template engine
config.include('pyramid_jinja2') # Jinja2 -template engine
config.include('pyramid_mako') # Mako -template engine
Обработка напрямую происходит при помощи функции
pyramid.renderers.render_to_response()
.
Примечание
Предварительно нужно добавить расширение, которое знает как обрабатывать шаблоны Chameleon.
config.include('pyramid_chameleon')
from pyramid.renderers import render_to_response
def sample_view(request):
return render_to_response('mypackage:templates/foo.pt',
{'foo':1, 'bar':2},
request=request)
Функция pyramid.renderers.render()
вернет только текст, а не объект response
.
from pyramid.renderers import render
from pyramid.response import Response
def sample_view(request):
result = render('mypackage:templates/foo.pt',
{'foo':1, 'bar':2},
request=request)
response = Response(result)
return response
Такой подход позволяет, например, использовать возможности самого шаблонизатора напрямую.
from mako.template import Template
from pyramid.response import Response
def make_view(request):
template = Template(filename='/templates/template.mak')
result = template.render(name=request.params['name'])
response = Response(result)
return response
Альтернативный способ функции render_to_response()
,
это привязывать к представлению свой обработчик. При этом представление
возвращает только словарь, который в последующем будет обработан этим
рендерером.
from pyramid.view import view_config
@view_config(renderer='mypackage:templates/foo.jinja2')
def my_view(request):
return {'foo':1, 'bar':2}
Этот код идентичен:
from pyramid.renderers import render
from pyramid.response import Response
def sample_view(request):
result = render('mypackage:templates/foo.jinja2',
{'foo':1, 'bar':2},
request=request)
response = Response(result)
return response
См.также
pip install pyramid_jinja2
Добавляется стандартными средствами:
config.Configurator()
config.include('pyramid_jinja2')
или
pyramid.includes=
pyramid_jinja2
@view_config(renderer='mypackage:templates/mytemplate.jinja2')
def my_view(request):
return {'foo': 1, 'bar': 2}
По умолчанию pyramid_jinja2
ищет директорию с шаблонами относительно вашего
проекта, поэтому можно опустить название проекта.
@view_config(renderer='templates/mytemplate.jinja2')
def my_view(request):
return {'foo': 1, 'bar': 2}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello World!</title>
</head>
<body>
<h1>{{ foo }}</h1>
<h1>{{ bar }}</h1>
</body>
</html>
Фреймворк Pyramid не ограничивает вас в использовании какого-либо определенного шаблонизатора. Вы можете выбрать любой который вам понравится или пользоваться несколькими, при этом можно написать собственные обработчики запросов, даже не привязанные к движкам шаблонов и даже написать свой собственный шаблонизатор с расширением для пирамиды, как например Tonnikala.
См.также
Фреймворк Pyramid имеет модуль pyramid.session
, который содержит в
себе несколько методов организации сессий.
Для того, чтобы использовать сессии, необходимо задать session factory во время конфигурации.
Очень простой, небезопасный способ создания сессии реализуется при помощи
функции pyramid.session.UnencryptedCookieSessionFactoryConfig()
. Он
использует куки для хранения информации сеанса. Эта реализация имеет следующие
ограничения:
Функция pyramid.session.SignedCookieSessionFactory()
шифрует
данные, поэтому их тяжело подделать.
Добавление сессий в конфиг происходит следующим образом:
from pyramid.session import SignedCookieSessionFactory
my_session_factory = SignedCookieSessionFactory('itsaseekreet')
from pyramid.config import Configurator
config = Configurator()
config.set_session_factory(my_session_factory)
или через атрибут конструктора:
from pyramid.session import SignedCookieSessionFactory
my_session_factory = SignedCookieSessionFactory('itsaseekreet')
from pyramid.config import Configurator
config = Configurator(session_factory=my_session_factory)
После добавления сессий в конфиг, вы можете получить доступ к объектам сессии из любого запроса. Например:
from pyramid.response import Response
def myview(request):
session = request.session
if 'counter' in session:
session['counter'] += 1
else:
session['counter'] = 1
return Response(session['counter'])
В самом простом случае достаточно включить модуль в проект:
config = Configurator()
config.include('pyramid_beaker')
Теперь можно использовать сессии:
from pyramid.response import Response
def myview(request):
session = request.session
if 'counter' in session:
session['counter'] += 1
else:
session['counter'] = 1
return Response(session['counter'])
«Всплывающие сообщения» — это очередь сообщений, хранящихся в сессии. Они показываются только один раз и при последующем обновлении удаляются или формируются новые.
Чтобы добавить сообщение, достаточно выполнить метод сессии flash()
.
request.session.flash('Congratulations "rm -rf /" successful')
Чтобы извлечь сообщение, нужно вызвать метод сессии pop_flash()
.
>>> request.session.flash('info message')
>>> request.session.pop_flash()
['info message']
>>> request.session.pop_flash()
[]
Для получения очереди, не извлекая сообщения из нее, нужно использовать метод peek_flash()
.
>>> request.session.flash('info message')
>>> request.session.peek_flash()
['info message']
>>> request.session.peek_flash()
['info message']
>>> request.session.pop_flash()
['info message']
>>> request.session.peek_flash()
[]
Например, всплывающие сообщения используются в модуле pyramid_sacrud. Это простой CRUD веб-интерфейс который выводит сообщения после какой-либо операции.
Пример всплывающего сообщения после добавления записи в админке pyramid_sacrud
Для получения токена используется метод session.get_csrf_token()
.
token = request.session.get_csrf_token()
Для создание нового токена:
token = request.session.new_csrf_token()
Пример добавления CSRF токена из текущей сессии в форму:
<form method="post" action="/myview">
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}">
<input type="submit" value="Delete Everything">
</form>
Проверка токена:
from pyramid.session import check_csrf_token
def myview(request):
# Require CSRF Token
check_csrf_token(request)
# ...
или
@view_config(request_method='POST', check_csrf=True, ...)
def myview(request):
...
Фреймворк Pyramid не имеет CRUD веб-интерфейса или встроенной админки, как у фреймворков Django и web2py. Но за счет стороннего модуля pyramid_sacrud этот функционал можно добавить.
from .models import (Model1, Model2, Model3,)
# add sacrud and project models
config.include('pyramid_sacrud')
settings = config.registry.settings
settings['pyramid_sacrud.models'] = (('Group1', [Model1, Model2]),
('Group2', [Model3]))
CRUD интерфейс для фреймворка Pyramid
pip install pyramid_sacrud
pyramid_sacrud предоставляет CRUD интерфейс для моделей SQLAlchemy. Создадим 3 простых таблицы (Car, Manufacturer, User) для примера:
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import backref, relationship
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String(30))
def __repr__(self):
return self.name
class Manufacturer(Base):
__tablename__ = 'manufacturers'
id = Column(Integer, primary_key=True)
name = Column(String(30))
class Car(Base):
__tablename__ = 'cars'
id = Column(Integer, primary_key=True)
name = Column(String(30))
manufacturer_id = Column(Integer, ForeignKey('manufacturers.id'))
manufacturer = relationship('Manufacturer',
backref=backref('cars', lazy='dynamic'))
Далее создадим Pyramid приложение и добавляем настройки БД.
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
# ...
def database_settings(config):
from sqlalchemy import create_engine
config.registry.settings['sqlalchemy.url'] = db_url = "sqlite:///example.db"
engine = create_engine(db_url)
Base.metadata.bind = engine
Base.metadata.create_all()
if __name__ == '__main__':
config = Configurator()
config.include(database_settings)
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 6543, app)
server.serve_forever()
Теперь опишем настройки нашего CRUD интерфейса:
1 2 3 4 5 6 | def sacrud_settings(config):
config.include('pyramid_sacrud', route_prefix='admin')
config.registry.settings['pyramid_sacrud.models'] = (
('Vehicle', [Manufacturer, Car]),
('Group2', [User])
) |
route_prefix='admin'
означает что интерфейс будет доступен по адресу
http://localhost:6543/admin/ (по умолчанию http://localhost:6543/sacrud/).
В настройках (settings) параметр pyramid_sacrud.models
отвечает за список моделей
которые будут отображаться в интерфейсе. В нашем случае это 3 модели,
поделенные на 2 группы (Vehicle и Group2).
Осталось включить эти настройки в проект:
# ...
if __name__ == '__main__':
from pyramid.session import SignedCookieSessionFactory
my_session_factory = SignedCookieSessionFactory('itsaseekreet')
config = Configurator(session_factory=my_session_factory)
config.include(database_settings)
config.include(sacrud_settings)
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 6543, app)
server.serve_forever()
И запустить:
python __init__.py
По адресу http://localhost:6543/admin/ будет доступна наша админка!
Чтобы после CRUD операций появлялись всплывающие сообщение необходимо добавить в проект поддержку сессий.
# ...
if __name__ == '__main__':
from pyramid.session import SignedCookieSessionFactory
my_session_factory = SignedCookieSessionFactory('itsaseekreet')
config = Configurator(session_factory=my_session_factory)
# ...
Полный исходный код:
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import backref, relationship
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String(30))
def __repr__(self):
return self.name
class Manufacturer(Base):
__tablename__ = 'manufacturers'
id = Column(Integer, primary_key=True)
name = Column(String(30))
class Car(Base):
__tablename__ = 'cars'
id = Column(Integer, primary_key=True)
name = Column(String(30))
manufacturer_id = Column(Integer, ForeignKey('manufacturers.id'))
manufacturer = relationship('Manufacturer',
backref=backref('cars', lazy='dynamic'))
def sacrud_settings(config):
config.include('pyramid_sacrud', route_prefix='admin')
config.registry.settings['pyramid_sacrud.models'] = (
('Vehicle', [Manufacturer, Car]),
('Group2', [User])
)
def database_settings(config):
from sqlalchemy import create_engine
config.registry.settings['sqlalchemy.url'] = db_url =\
"sqlite:///example.db"
engine = create_engine(db_url)
Base.metadata.bind = engine
Base.metadata.create_all()
if __name__ == '__main__':
from pyramid.session import SignedCookieSessionFactory
my_session_factory = SignedCookieSessionFactory('itsaseekreet')
config = Configurator(session_factory=my_session_factory)
config.include(database_settings)
config.include(sacrud_settings)
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 6543, app)
server.serve_forever()
CRUD интерфейс вряд ли может использоваться как основной инструмент клиента, но он может помочь визуализировать данные при разработке и выполнять простые операции связанные с администрированием приложения.
pyramid_sacrud довольно простой способ добавить в ваше приложение веб CRUD интерфейс, больше информации о настройке можно найти по адресу http://pyramid-sacrud.readthedocs.org/en/latest/pages/configuration.html, также pyramid_sacrud полностью совместим с настройками ColanderAlchemy.
Особенностью pyramid_sacrud является то что он не накладывает ограничений на структуру БД, а наоборот отталкивается от уже существующей. Ниже приведен пример как подключить его к БД не зная ее структуры:
"""
Funny application demonstrates the capabilities of SQLAlchemy and Pyramid.
It is something between phpMyAdmin and django.contrib.admin. SQLAlchemy with
Pyramid mapped on existing Django generated database but not vice versa.
Requirements
------------
pip install pyramid, sqlalchemy
pip install git+https://github.com/sacrud/pyramid_sacrud.git@develop
Demonstration
-------------
python SQLAlchemyMyAdmin.py
goto http://localhost:8080/sacrud/
"""
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from sqlalchemy import engine_from_config, MetaData
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker
from zope.sqlalchemy import ZopeTransactionExtension
DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
Base = declarative_base()
def get_metadata(engine):
# produce our own MetaData object
metadata = MetaData()
metadata.reflect(engine)
# we can then produce a set of mappings from this MetaData.
Base = automap_base(metadata=metadata)
# calling prepare() just sets up mapped classes and relationships.
Base.prepare()
return metadata
def quick_mapper(table):
class GenericMapper(Base):
__table__ = table
__tablename__ = table.name
return GenericMapper
def get_app():
config = Configurator()
settings = config.registry.settings
settings['sqlalchemy.url'] = "postgresql://login:password@localhost/your_database_name"
# Database
engine = engine_from_config(settings)
DBSession.configure(bind=engine)
metadata = get_metadata(engine)
tables = [quick_mapper(table) for table in metadata.sorted_tables]
# SACRUD
settings['pyramid_sacrud.models'] = (
('', tables),
)
config.include('pyramid_sacrud')
return config.make_wsgi_app()
if __name__ == '__main__':
app = get_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever()
См.также
Видео:
См.также
В пирамиде система безопасности поделена на 2 части. Первая это аутентификация, которая производит идентификацию пользователя, его проверку (например что он есть в БД и он не заблокирован) и определяет какими правами он наделен. Второе это авторизация, система которая проверяет имеет ли этот пользователь доступ к запрошенному ресурсу.
Примечание
Фреймворк Repoze.bfg имеет расширение repoze.who, которое отвечает за идентификацию и аутентификацию пользователя.
Who? т.е. Кто? ты.
Для авторизации используется расширение repoze.what, которое проверяет какие ресурсы тебе доступны.
What? т.е. Что? доступно тебе.
Несмотря на то, что фреймворк Pyramid это по сути переименованный repoze.bfg, в нем есть собственный механизм авторизации и аутентификации из коробки.
Определение текущего пользователя при поступлении HTTP запроса, это задача аутентификации (authentication policy). Производится она в 3 этапа:
Идентифицируем пользователя путем проверки токенов/заголовков/итд в HTTP
запросе. (см. pyramid.request.Request.unauthenticated_userid
)
Например: ищем auth_token
в куках запроса, проверяем что токен правильно
подписан, и возвращаем id
пользователя.
Подтверждаем статус идентифицированного пользователя. (authenticated_userid
)
Например: проверяем что id
этого пользователя все еще в базе данных и
пользователь еще активен. Пользователя могли удалить из БД, но при этом
в куках браузера хранится валидный токен auth_token
.
Ищем группы (principal) которые принадлежат пользователю и добавляем
их в список. (effective_principals
)
Например: берем из БД группы пользователя и добавляем в список. Для текущего идентифицированного пользователя это может быть: «vasya», «user_admin», «editor».
Каждый ресурс пирамиды может быть защищен правами доступа (permission). Задача авторизации определение того, какие пользователи имеют доступ к ресурсам.
После аутентификации создается список групп пользователя (principal). Политика авторизации (authorization policy) запрещает или разрешает доступ к ресурсу на основании этого списка групп, сверяя его с правами ресурса.
В пирамиде по умолчанию авторизация отключена. Все представления (views
)
полностью доступны анонимным пользователям. Для того чтобы их защитить нужно
добавить в настройки политику безопасности.
Для включения политики авторизации используется метод конфигуратора
pyramid.config.Configurator.set_authorization_policy()
. Для
аутентификации pyramid.config.Configurator.set_authentication_policy()
соответственно. Так-как авторизация не может существовать без аутентификации,
необходимо указывать обе политики в проекте.
from pyramid.config import Configurator
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512')
authz_policy = ACLAuthorizationPolicy()
config = Configurator()
config.set_authentication_policy(authn_policy)
config.set_authorization_policy(authz_policy)
Здесь pyramid.authentication.AuthTktAuthenticationPolicy
это механизм
аутентификации пользователя, который ищет его из «auth ticket» cookie.
pyramid.authorization.ACLAuthorizationPolicy
механизм авторизации
по аксесс листам (ACL).
Императивно:
config.add_view('mypackage.views.blog_entry_add_view',
name='add_entry.html',
permission='add')
Декларативно:
from pyramid.view import view_config
from resources import Blog
@view_config(name='add_entry.html', permission='add')
def blog_entry_add_view(request):
""" Add blog entry code goes here """
# ...
Если ресурсу не присвоены права доступа, то используются права по умолчанию.
В пирамиде права по умолчанию (pyramid.security.NO_PERMISSION_REQUIRED
)
подразумевают что ресурсы доступны всем, даже анонимным пользователям.
Это поведение возможно изменить при помощи метода
pyramid.config.Configurator.set_default_permission()
.
config.set_default_permission('my_default_permission')
См.также
Access Control List или ACL — список контроля доступа, который определяет, кто или что может получать доступ к конкретному объекту, и какие именно операции разрешено или запрещено этому субъекту проводить над объектом.
В пирамиде аксесс лист это список содержащий записи, определяющие права индивидуального пользователя или группы на ресурсы проекта. Элементы ACL также еще называют Access Control Entry или ACE.
Например:
from pyramid.security import Allow, Deny
from pyramid.security import Everyone
__acl__ = [
(Deny, 'vasya', 'move'),
(Deny, 'group:blacklist', ('add', 'delete', 'edit')),
(Allow, Everyone, 'view'),
(Allow, 'group:editors', ('add', 'edit')),
(Allow, 'group:editors', 'move'),
(Allow, 'group:deleter', 'delete'),
]
__acl__
из примера выше, это список контроля доступа (ACL).
(Allow, Everyone, 'delete')
это ACE, т.е. запись в ACL.
pyramid.security.Allow
и pyramid.security.Deny
.Также существую специальные группы (principal):
pyramid.security.Everyone
- для всех.pyramid.security.Authenticated
- для аутентифицированнных
пользователей.Если мы захотим запретить все, кроме тех ACE которые в списке, мы можем написать это так:
from pyramid.security import Allow
from pyramid.security import ALL_PERMISSIONS
__acl__ = [(Allow, 'fred', 'view'),
(Deny, Everyone, ALL_PERMISSIONS)]
или воспользоваться встроенным в пирамиду ACE:
from pyramid.security import Allow
from pyramid.security import DENY_ALL
__acl__ = [(Allow, 'fred', 'view'),
DENY_ALL]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.security import Allow
from pyramid.security import Everyone
class HelloFactory(object):
def __init__(self, request):
self.__acl__ = [
(Allow, Everyone, 'view'),
(Allow, 'group:editors', 'add'),
(Allow, 'group:editors', 'edit'),
]
def hello_world(request):
return Response('Hello %(name)s!' % request.matchdict)
if __name__ == '__main__':
authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512')
authz_policy = ACLAuthorizationPolicy()
config = Configurator()
config.set_authentication_policy(authn_policy)
config.set_authorization_policy(authz_policy)
config.add_route('hello', '/hello/{name}',
factory=HelloFactory)
config.add_view(hello_world,
route_name='hello',
permission='view')
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever() |
См.также
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.security import Allow
from pyramid.security import Everyone
class HelloFactory(object):
def __init__(self, request):
self.__acl__ = [
(Allow, Everyone, 'view'),
(Allow, 'group:editors', 'add'),
(Allow, 'group:editors', 'edit'),
]
def hello_world(request):
return Response('Hello %(name)s!' % request.matchdict)
if __name__ == '__main__':
authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512')
authz_policy = ACLAuthorizationPolicy()
config = Configurator(root_factory=HelloFactory)
config.set_authentication_policy(authn_policy)
config.set_authorization_policy(authz_policy)
config.add_route('hello', '/hello/{name}')
config.add_view(hello_world,
route_name='hello',
permission='view')
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | from wsgiref.simple_server import make_server
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.config import Configurator
from pyramid.httpexceptions import HTTPFound
from pyramid.response import Response
from pyramid.security import Allow, forget, remember
class HelloFactory(object):
def __init__(self, request):
self.__acl__ = [
(Allow, 'vasya', 'view'),
(Allow, 'group:editors', 'add'),
(Allow, 'group:editors', 'edit'),
]
def hello_world(request):
return Response('Hello %(name)s!' % request.matchdict)
def login(request):
headers = remember(request, 'vasya')
return HTTPFound(location=request.route_url('hello', name='vasya'),
headers=headers)
def logout(request):
headers = forget(request)
return HTTPFound(location=request.route_url('hello', name='log out!!!'),
headers=headers)
if __name__ == '__main__':
authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512')
authz_policy = ACLAuthorizationPolicy()
config = Configurator(root_factory=HelloFactory)
config.set_authentication_policy(authn_policy)
config.set_authorization_policy(authz_policy)
config.add_route('hello', '/hello/{name}')
config.add_view(hello_world,
route_name='hello',
permission='view')
# login form
config.add_route('login', '/login')
config.add_route('logout', '/logout')
config.add_view(login, route_name='login')
config.add_view(logout, route_name='logout')
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever() |
from __future__ import absolute_import
from waitress import serve
from pyramid.config import Configurator
from pyramid.response import Response
from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
from pyramid.security import Authenticated, Allow, Everyone
from pyramid.authorization import ACLAuthorizationPolicy
class Root(object):
__acl__ = [
(Allow, Authenticated, 'view'),
]
def __init__(self, request):
self.request = request
def checkauth(username, password):
return username == 'pyramid' and password == 'aliens'
class BasicAuthenticationPolicy(object):
def authenticated_userid(self, request):
authorization = AUTHORIZATION(request.environ)
if not authorization:
return None
(authmeth, auth) = authorization.split(' ', 1)
if 'basic' != authmeth.lower():
return None
auth = auth.strip().decode('base64')
username, password = auth.split(':', 1)
if not checkauth(username, password):
return None
return username
def effective_principals(self, request):
ep = [Everyone]
username = self.authenticated_userid(request)
if username is not None:
ep.append(Authenticated)
ep.append(username)
ep.append('g:admin')
return ep
def unauthenticated_userid(self, request):
authorization = AUTHORIZATION(request.environ)
if not authorization:
return None
(authmeth, auth) = authorization.split(' ', 1)
if 'basic' != authmeth.lower():
return None
auth = auth.strip().decode('base64')
username, password = auth.split(':', 1)
return username
def remember(self, request, principal, **kw):
return []
def forget(self, request):
return []
def forbidden_view(request):
head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % 'fnord')
return Response('Not Authorized', status='401 Not Authorized', headers=head)
def hello_world(request):
return Response('Hello {!r}!'.format(request.effective_principals))
if __name__ == '__main__':
config = Configurator(root_factory=Root)
config.add_route('hello', '/hello')
config.add_view(hello_world, route_name='hello', permission='view')
config.set_authentication_policy(BasicAuthenticationPolicy())
config.set_authorization_policy(ACLAuthorizationPolicy())
config.add_forbidden_view(forbidden_view)
app = config.make_wsgi_app()
serve(app, host='0.0.0.0', port=8080)
Создадим структуру будущего блога.
$ pcreate -t alchemy pyramid_blogr
$ cd pyramid_blogr
$ tree
.
├── CHANGES.txt
├── development.ini <- файл с настройками проекта
├── MANIFEST.in
├── production.ini
├── pyramid_blogr
│ ├── __init__.py <- точка входа нашего приложения, функция main.
│ │ Создает конфиг и возвращает WSGI-приложение.
│ ├── models.py <- описание схемы БД при помощи ORM SQLAlchemy
│ ├── scripts
│ │ ├── initializedb.py <- скрипт инициализации проекта
│ │ └── __init__.py
│ ├── static/ <- статические файлы (картинки, стили, javascript, ...)
│ ├── templates/ <- шаблоны
│ ├── tests.py
│ └── views.py <- вьюхи (бизнес-логика приложения)
├── README.txt
└── setup.py
В скаффолде alchemy
, который мы использовали для создания блога, уже существуют минимальные настройки для работы с БД.
Подключение к БД прописано в файле development.ini
.
[app:main]
use = egg:pyramid_blogr
pyramid.reload_templates = true
pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
pyramid.includes =
pyramid_debugtoolbar
pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/pyramid_blogr.sqlite
Объект сессии создается в файле pyramid_blogr/models.py
.
Там же находится базовый класс для моделей.
from sqlalchemy import (
Column,
Index,
Integer,
Text,
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import (
scoped_session,
sessionmaker,
)
from zope.sqlalchemy import ZopeTransactionExtension
DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
Base = declarative_base()
class MyModel(Base):
__tablename__ = 'models'
id = Column(Integer, primary_key=True)
name = Column(Text)
value = Column(Integer)
Index('my_index', MyModel.name, unique=True, mysql_length=255)
В главном файле проекта pyramid_blogr/__init__.py
находится функция main
, которая вызывается при запуске команды pserve development.ini
. Причем, настройки из файла development.ini
передаются в эту функцию через атрибут settings
(def main(global_config, **settings):
).
pserve
знает что нужно запустить функцию main
, потому что это указанно в самом файле настроек development.ini
.
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
host = 0.0.0.0
port = 6543
Подключение к БД берется из настроек при помощи функции sqlalchemy.engine_from_config()
. Далее объекту сессии и базовому классу указывается строка подключения.
from pyramid.config import Configurator
from sqlalchemy import engine_from_config
from .models import (
DBSession,
Base,
)
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
engine = engine_from_config(settings, 'sqlalchemy.')
DBSession.configure(bind=engine)
Base.metadata.bind = engine
config = Configurator(settings=settings)
config.include('pyramid_chameleon')
config.add_static_view('static', 'static', cache_max_age=3600)
config.add_route('home', '/')
config.scan()
return config.make_wsgi_app()
pyramid_sqlalchemy - расширение для Pyramid которое делает многие настройки БД за вас.
Установка:
$ pip install pyramid_sqlalchemy
Файл __init__.py
стал значительно проще.
from pyramid.config import Configurator
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config = Configurator(settings=settings)
config.include('pyramid_sqlalchemy')
config.include('pyramid_chameleon')
config.add_static_view('static', 'static', cache_max_age=3600)
config.add_route('home', '/')
config.scan()
return config.make_wsgi_app()
Базовый класс и сессия импортируются прямо из библиотеки.
pyramid_sqlalchemy.BaseObject
pyramid_sqlalchemy.Session
Поэтому можно удалить Base
и DBSession
из файла models.py
.
from sqlalchemy import (
Column,
Index,
Integer,
Text,
)
from pyramid_sqlalchemy import BaseObject
class MyModel(BaseObject):
__tablename__ = 'models'
id = Column(Integer, primary_key=True)
name = Column(Text)
value = Column(Integer)
Index('my_index', MyModel.name, unique=True, mysql_length=255)
Сессии работаю аналогично. Пример views.py
.
from pyramid.response import Response
from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
from pyramid_sqlalchemy import Session as DBSession
from .models import MyModel
@view_config(route_name='home', renderer='templates/mytemplate.pt')
def my_view(request):
try:
one = DBSession.query(MyModel).filter(MyModel.name == 'one').first()
except DBAPIError:
return Response(conn_err_msg, content_type='text/plain', status_int=500)
return {'one': one, 'project': 'pyramid_blogr'}
conn_err_msg = """\
Pyramid is having a problem using your SQL database. The problem
might be caused by one of the following things:
A. You may need to run the "initialize_pyramid_blogr_db" script
to initialize your database tables. Check your virtual
environment's "bin" directory for this script and try to run it.
B. Your database server may not be running. Check that the
database server referred to by the "sqlalchemy.url" setting in
your "development.ini" file is running.
After you fix the problem, please restart the Pyramid application to
try it again.
"""
В файле models.py
заменим MyModel
на таблицы блога:
import datetime
from pyramid_sqlalchemy import BaseObject
from sqlalchemy import Column, DateTime, Integer, Unicode, UnicodeText
class User(BaseObject):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(Unicode(255), unique=True, nullable=False)
password = Column(Unicode(255), nullable=False)
last_logged = Column(DateTime, default=datetime.datetime.utcnow)
class Article(BaseObject):
__tablename__ = 'articles'
id = Column(Integer, primary_key=True)
title = Column(Unicode(255), unique=True, nullable=False)
content = Column(UnicodeText, default=u'')
created = Column(DateTime, default=datetime.datetime.utcnow)
edited = Column(DateTime, default=datetime.datetime.utcnow)
В скаффорлде существует файл инициализации проекта pyramid_blogr/scripts/initializedb.py
. Его можно выполнить как команду окружения:
$ initialize_pyramid_blogr_db development.ini
В окружение эта команда попадает после установки (python setup.py develop
) пакета, т.к. прописана в настройках setup.py
.
# ...
setup(name='pyramid_blogr',
version='0.0',
description='pyramid_blogr',
long_description=README + '\n\n' + CHANGES,
classifiers=[
"Programming Language :: Python",
"Framework :: Pyramid",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
],
author='',
author_email='',
url='',
keywords='web wsgi bfg pylons pyramid',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
test_suite='pyramid_blogr',
install_requires=requires,
entry_points="""\
[paste.app_factory]
main = pyramid_blogr:main
[console_scripts]
initialize_pyramid_blogr_db = pyramid_blogr.scripts.initializedb:main
""",
)
Добавим в этот скрипт инициализации, создание новых таблиц, добавление пользователя «admin» и статей.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | # -*- coding: utf-8 -*-
import os
import sys
import transaction
from pyramid.paster import get_appsettings, setup_logging
from pyramid.scripts.common import parse_vars
from pyramid_sqlalchemy import BaseObject as Base
from pyramid_sqlalchemy import Session as DBSession
from sqlalchemy import engine_from_config
from ..models import Article, User
def usage(argv):
cmd = os.path.basename(argv[0])
print('usage: %s <config_uri> [var=value]\n'
'(example: "%s development.ini")' % (cmd, cmd))
sys.exit(1)
def main(argv=sys.argv):
if len(argv) < 2:
usage(argv)
config_uri = argv[1]
options = parse_vars(argv[2:])
setup_logging(config_uri)
settings = get_appsettings(config_uri, options=options)
engine = engine_from_config(settings, 'sqlalchemy.')
DBSession.configure(bind=engine)
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
with transaction.manager:
model = User(name=u'admin', password=u'admin')
DBSession.add(model)
from jinja2.utils import generate_lorem_ipsum
for id, article in enumerate(range(100), start=1):
title = generate_lorem_ipsum(
n=1, # Одно предложение
html=False, # В виде обычного текста
min=2, # Минимум 2 слова
max=5 # Максимум 5
)
content = generate_lorem_ipsum()
article = Article(**{'title': title, 'content': content})
DBSession.add(article) |
Теперь при выполнении этого скрипта, наша БД будет пересоздаваться.
$ initialize_pyramid_blogr_db development.ini
CREATE TABLE articles (
id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT,
created DATETIME,
edited DATETIME,
PRIMARY KEY (id),
UNIQUE (title)
)
2015-05-05 12:49:59,749 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
2015-05-05 12:49:59,755 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT
2015-05-05 12:49:59,755 INFO [sqlalchemy.engine.base.Engine][MainThread]
CREATE TABLE users (
id INTEGER NOT NULL,
name VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
last_logged DATETIME,
PRIMARY KEY (id),
UNIQUE (name)
)
2015-05-05 12:49:59,755 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
2015-05-05 12:49:59,761 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT
2015-05-05 12:49:59,764 INFO [sqlalchemy.engine.base.Engine][MainThread] BEGIN (implicit)
2015-05-05 12:49:59,766 INFO [sqlalchemy.engine.base.Engine][MainThread] INSERT INTO users (name, password, last_logged) VALUES (?, ?, ?)
2015-05-05 12:49:59,767 INFO [sqlalchemy.engine.base.Engine][MainThread] (u'admin', u'admin', '2015-05-05 12:49:59.766198')
2015-05-05 12:49:59,769 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT
URL | Назначение |
---|---|
/ | Главная страница со списком статей |
/static/jquery.js | Статические файлы |
/sign/in | Вход под своей учетной записью |
/sign/out | Выход |
/add | Добавление новой статьи |
/article/13 | Просмотр статьи с id=13 |
/article/13/edit | Редактирование статьи с id=13 |
/article/13/delete | Удаление статьи с id=13 |
Добавим пути в кофигуратор в файле __init__.py
.
from pyramid.config import Configurator
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config = Configurator(settings=settings)
config.include('pyramid_sqlalchemy')
config.include('pyramid_chameleon')
config.add_static_view('static', 'static', cache_max_age=3600)
config.add_route('blog', '/')
config.add_route('blog_article', '/article/{id:\d+}')
config.add_route('blog_action', '/article/{id:\d+}/{action}')
config.add_route('auth', '/sign/{action}')
config.scan()
return config.make_wsgi_app()
Создадим представления для нашего блога. Пока в виде «заглушек».
from pyramid.view import view_config
@view_config(route_name='blog',
renderer='blog/index.jinja2')
def index_page(request):
return {}
@view_config(route_name='blog_article', renderer='blog/read.jinja2')
def blog_view(request):
return {}
@view_config(route_name='blog_action', match_param='action=create',
renderer='blog/edit.jinja2')
def blog_create(request):
return {}
@view_config(route_name='blog_action', match_param='action=edit',
renderer='blog/edit.jinja2')
def blog_update(request):
return {}
@view_config(route_name='auth', match_param='action=in', renderer='string',
request_method='POST')
@view_config(route_name='auth', match_param='action=out', renderer='string')
def sign_in_out(request):
return {}
views.py
@view_config(route_name='blog',
renderer='blog/index.jinja2')
def index_page(request):
page = int(request.params.get('page', 1))
paginator = Article.get_paginator(request, page)
return {'paginator': paginator}
models.py
Article
@classmethod
def get_paginator(cls, request, page=1):
query = Session.query(Article).order_by(desc(Article.created))
query_params = request.GET.mixed()
def url_maker(link_page):
query_params['page'] = link_page
return request.current_route_url(_query=query_params)
return SqlalchemyOrmPage(query, page, items_per_page=5,
url_maker=url_maker)
views.py
@view_config(route_name='blog_article', renderer='blog/read.jinja2')
def blog_view(request):
id = int(request.matchdict.get('id', -1))
article = Article.by_id(id)
if not article:
return HTTPNotFound()
return {'article': article}
models.py
Article
@classmethod
def by_id(cls, id):
return Session.query(Article).filter(Article.id == id).first()
views.py
@view_config(route_name='blog_create',
renderer='blog/edit.jinja2')
@view_config(route_name='blog_action', match_param='action=edit',
renderer='blog/edit.jinja2')
def blog_create(request):
form = get_form(request)
if request.method == 'POST':
try:
values = form.validate(request.POST.items())
except deform.ValidationFailure as e:
return {'form': e.render(),
'action': request.matchdict.get('action')}
if request.matchdict['action'] == 'edit':
article = Session.query(Article)\
.filter_by(id=request.matchdict['id']).one()
article.title = request.POST['title']
article.content = request.POST['content']
else:
article = Article(**values)
Session.add(article)
return HTTPFound(location=request.route_url('blog'))
values = {}
if request.matchdict['action'] == 'edit':
values = Session.query(Article)\
.filter_by(id=request.matchdict['id']).one().__dict__
return {'form': form.render(values),
'action': request.matchdict.get('action')}
import deform
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
from pyramid.view import view_config
from pyramid_sqlalchemy import Session
from .forms import get_form
from .models import Article
@view_config(route_name='blog',
renderer='blog/index.jinja2')
def index_page(request):
page = int(request.params.get('page', 1))
paginator = Article.get_paginator(request, page)
return {'paginator': paginator}
@view_config(route_name='blog_article', renderer='blog/read.jinja2')
def blog_view(request):
id = int(request.matchdict.get('id', -1))
article = Article.by_id(id)
if not article:
return HTTPNotFound()
return {'article': article}
@view_config(route_name='blog_create',
renderer='blog/edit.jinja2')
@view_config(route_name='blog_action', match_param='action=edit',
renderer='blog/edit.jinja2')
def blog_create(request):
form = get_form(request)
if request.method == 'POST':
try:
values = form.validate(request.POST.items())
except deform.ValidationFailure as e:
return {'form': e.render(),
'action': request.matchdict.get('action')}
if request.matchdict.get('action', '') == 'edit':
article = Session.query(Article)\
.filter_by(id=request.matchdict['id']).one()
article.title = request.POST['title']
article.content = request.POST['content']
else:
article = Article(**values)
Session.add(article)
return HTTPFound(location=request.route_url('blog'))
values = {}
if request.matchdict.get('action', '') == 'edit':
values = Session.query(Article)\
.filter_by(id=request.matchdict['id']).one().__dict__
return {'form': form.render(values),
'action': request.matchdict.get('action')}
@view_config(route_name='blog_action', match_param='action=delete')
def blog_delete(request):
article = Session.query(Article)\
.filter_by(id=request.matchdict['id']).one()
Session.delete(article)
return HTTPFound(location=request.route_url('blog'))
@view_config(route_name='auth', match_param='action=in', renderer='string',
request_method='POST')
@view_config(route_name='auth', match_param='action=out', renderer='string')
def sign_in_out(request):
return {}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
def hello(request):
return Response('Hello world! See you blog <a href="blog/">there</a>!')
from pyramid.wsgi import wsgiapp
@wsgiapp
def hello_world(environ, start_response):
body = 'Hello world'
start_response('200 OK', [('Content-Type', 'text/plain'),
('Content-Length', str(len(body)))])
return [body]
if __name__ == '__main__':
config = Configurator()
config.add_route('hello_world', '/')
config.add_route('hello_world_wsgi', '/hello_wsgi')
config.add_view(hello, route_name='hello_world')
config.add_view(hello_world, route_name='hello_world_wsgi')
from my_wsgi_blog import make_wsgi_app
blog_app = make_wsgi_app()
from paste import urlmap
mapping = urlmap.URLMap()
mapping['/blog'] = blog_app
from paste.cascade import Cascade
pyramid_app = config.make_wsgi_app()
app = Cascade([mapping, pyramid_app])
server = make_server('0.0.0.0', 8000, app)
server.serve_forever() |
Allow
or
Deny
), a principal (a string describing a user or
group), and a permission. For example the ACE, (Allow,
'bob', 'read')
is a member of an ACL that indicates that the
principal bob
is allowed the permission read
against the
resource the ACL is attached to.[
(Allow, 'bob', 'read'), (Deny, 'fred', 'write')]
. If an ACL is
attached to a resource instance, and that resource is findable via the
context resource, it will be consulted any active security policy to
determine wither a particular request can be fulfilled given the
authentication information in the request.pyramid.config.Configurator.commit()
is
called.A consulting organization formed by Paul Everitt, Tres Seaver, and Chris McDonough.
См.также
See also Agendaless Consulting.
pyramid.path.AssetResolver.resolve()
method. It supports the
methods and attributes documented in
pyramid.interfaces.IAssetDescriptor
.my.package:static/baz.css
identifies the file named
baz.css
in the static
subdirectory of the my.package
Python package. See Understanding Asset Specifications for more
info.${name}
syntax. It is also much faster than the reference
implementation of ZPT. Pyramid offers Chameleon templating out
of the box in ZPT and text flavors.add_view()
method of the
configurator) or route configuration (via the
add_route()
method of the
configurator). A set of configuration declarations is also implied by
the configuration decoration detected by a scan of code
in a package.pyramid.view.view_config
, aka
@view_config
.pyramid.config.Configurator.add_view()
is a
configuration directive, and application developers can add their own
directives as necessary (see Adding Methods to the Configurator via add_directive).pyramid.config.Configurator
class.bin
(on UNIX, or Scripts
on Windows)
directory of a Python installation or virtualenv as the result of
running setup.py install
or setup.py develop
.A wrapper around a Python function or class which accepts the function or class as its first argument and which returns an arbitrary object. Pyramid provides several decorators, used for configuration and return value modification purposes.
См.также
See also PEP 318.
A permission which is registered as the default for an
entire application. When a default permission is in effect,
every view configuration registered with the system will
be effectively amended with a permission
argument that will
require that the executing user possess the default permission in
order to successfully execute the associated view
callable.
См.также
See also Setting a Default Permission.
''
). This is the case when
traversal exhausts the path elements in the PATH_INFO of a
request before it returns a context resource.settings
argument. These are later accessible via a
request.registry.settings
dictionary in views or as
config.registry.settings
in configuration code. Deployment settings
can be used as global application values..egg
, .tar.gz
, or .zip
.
Distributions are the target of Setuptools-related commands such as
easy_install
.path.to.modulename:attributename
. Often used in Pyramid and
setuptools configurations. A variant is used in dotted names within
configurator method arguments that name objects (such as the «add_view»
method’s «view» and «context» attributes): the colon (:
) is not
used; in its place is a dot.pyramid.httpexceptions.HTTPForbidden
exception from within view code or root factory code,
or when the view configuration and authorization policy
found for a request disallows a particular view invocation.
Pyramid provides a default implementation of a forbidden view;
it can be overridden. See Changing the Forbidden View.gunicorn
, a fast WSGI server that runs on UNIX under
Python 2.6+ or Python 3.1+. See http://gunicorn.org/ for detailed
information.The set of exception classes defined in pyramid.httpexceptions
.
These can be used to generate responses with various status codes when
raised or returned from a view callable.
См.также
See also HTTP Exceptions.
context
argument, the request_type
argument and the containment
argument. Interfaces are also
exposed to application developers when they make use of the
event system. Fundamentally, Pyramid
programmers can think of an interface as something that they can
attach to an object that stamps it with a «type» unrelated to its
underlying Python type. Interfaces can also be used to describe
the behavior of an object (its methods and attributes), but
unless they choose to, Pyramid programmers do not need
to understand or use this feature of interfaces.The act of creating software with a user interface that can potentially be displayed in more than one language or cultural context. Often shortened to «i18n» (because the word «internationalization» is I, 18 letters, then N).
См.также
See also Localization.
pyramid.interfaces.IIntrospectable
. Introspectables are used
by the introspector to display configuration information about
a running Pyramid application. An introspectable is associated with a
action by virtue of the
pyramid.config.Configurator.action()
method.pyramid.interfaces.IIntrospector
that is available in both
configuration code (for registration) and at runtime (for querying) that
allows a developer to introspect configuration statements and
relationships between those statements.__parent__
attribute.pot-create
command to extract translateable messages from Python sources
and Chameleon ZPT template files.en
, en_US
, de
, or de_AT
which
uniquely identifies a particular locale.pyramid.i18n.get_locale_name()
, and
pyramid.i18n.negotiate_locale_name()
functions, and
indirectly by pyramid.i18n.get_localizer()
. The
pyramid.i18n.default_locale_negotiator()
function
is an example of a locale negotiator.The process of displaying the user interface of an internationalized application in a particular language or cultural context. Often shortened to «l10» (because the word «localization» is L, 10 letters, then N).
См.также
See also Internationalization.
pyramid.i18n.Localizer
which
provides translation and pluralization services to an
application. It is retrieved via the
pyramid.i18n.get_localizer()
function.request.matchdict
when a URL dispatch route has been matched.
Its keys are names as identified within the route pattern; its values are
the values matched by each pattern name..mo
file containing translations.msgid
argument to a translation string is a
message identifier. Message identifiers are also present in a
message catalog..py
or .pyc
. Modules often live in a
package.getall
, getone
, mixed
, add
and
dict_of_lists
to the normal dictionary interface. See
Multidict and pyramid.interfaces.IMultiDict
.pyramid.httpexceptions.HTTPNotFound
exception from within view code or root factory code,
or when the current request doesn’t match any view
configuration. Pyramid provides a default implementation of a
Not Found View; it can be overridden. See
Changing the Not Found View.__init__.py
file, making
it recognizable to Python as a location which can be import
-ed.
A package exists to contain module files..ini
file. It was developed by Ian Bicking.read
, or view_blog_entries
.abc
subobject of the physical root object is /abc
. Physical paths
can also be specified as tuples where the first element is the empty
string (representing the root), and every other element is a Unicode
object, e.g. ('', 'abc')
. Physical paths are also sometimes called
«traversal paths».A module which ships with setuptools and distribute that provides an API for addressing «asset files» within a Python package. Asset files are static files, template files, etc; basically anything non-Python-source that lives in a Python package can be considered a asset file.
См.также
See also PkgResources.
True
or False
. Two different types
of predicates exist in Pyramid: a view predicate
and a route predicate. View predicates are attached to
view configuration and route predicates are attached to
route configuration.route_url()
in order to adjust the set
of arguments passed to it by the user for special purposes. It will
influence the URL returned by
route_url()
. See
pyramid.interfaces.IRoutePregenerator
for more information.setup.py
file and one or more Python packages. The
setup.py
file contains code that allows the package(s) to be
installed, distributed, and tested.pyramid_debugtoolbar
is configured into
the development.ini
of all applications which use a Pyramid
scaffold. For more information, see
http://docs.pylonsproject.org/projects/pyramid_debugtoolbar/en/latest/.pyramid_jinja2_starter
, which creates an application package based
on the Jinja2 templating system.pyramid_zcml
, you can use ZCML as an alternative to
imperative configuration or configuration decoration.pyramid.event.BeforeRender
event.repoze.bfg
.pyramid.request.Request
class. See Request and Response Objects
(narrative) and pyramid.request (API documentation) for
information about request objects.pyramid.interfaces.IRequest
.pyramid.interfaces.IResponse
interface. A response object is
typically an instance of the pyramid.response.Response
class or
a subclass such as pyramid.httpexceptions.HTTPFound
. See
Request and Response Objects for information about response objects.pyramid.response.Response
object. See Changing How Pyramid Treats View Responses
for more information.A user-defined callback executed by the router at a point after a response object is successfully created.
См.также
See also Using Response Callbacks.
pyramid.interfaces.IResponseFactory
.traverse=
argument
is used in route configuration).get_root
. An
application may supply a root factory to Pyramid during the
construction of a Configurator. If a root factory is not
supplied, the application creates a default root object using the
default root factory.A single pattern matched by the url dispatch subsystem, which generally resolves to a root factory (and then ultimately a view).
См.также
See also url dispatch.
True
or False
for a given
request. All predicates attached to a route
configuration must evaluate to True
for the associated route
to «match» the current request. If a route does not match the
current request, the next route (in definition order) is
attempted.pcreate
command.request
(a request object), returns a session object. See
Using the Default Session Factory,
Using Alternate Session Factories and
pyramid.config.Configurator.set_session_factory()
for more
information.distutils
to provide easier building,
distribution, and installation of libraries and applications. As of
this writing, setuptools runs under Python 2, but not under Python 3.
You can use distribute under Python 3 instead.['left', 'over', 'names']
. Within
Pyramid applications that use URL dispatch rather than traversal, you
can use *subpath
in the route pattern to influence the
subpath. See Using *subpath in a Route Pattern for more information.A thread-local variable is one which is essentially a global variable in terms of how it is accessed and treated, however, each thread used by the application may have a different value for this same «global» variable. Pyramid uses a small number of thread local variables, as described in Thread Locals.
См.также
See also the stdlib documentation
for more information.
LC_MESSAGES
folders, which contain .mo
files.
Each .mo
file represents a set of translations for a language
in a translation domain. The name of the .mo
file
(minus the .mo extension) is the translation domain name..mo
files
within one or more translation directory directories.pyramid.i18n.TranslationString
, which
is a class that behaves like a Unicode string, but has several
extra attributes such as domain
, msgid
, and mapping
for use during translation. Translation strings are usually
created by hand within software, but are sometimes created on the
behalf of the system for automatic template translation. For
more information, see Internationalization and Localization.translate
method.request
,
which will be an instance of a request object. An
alternate calling convention allows a view to be defined as a
callable which accepts a pair of arguments: context
and
request
: this calling convention is useful for
traversal-based applications in which a context is always
very important. A view callable is the primary mechanism by
which a developer writes user interface code within
Pyramid. See Views for more information
about Pyramid view callables.@view_config
decorator coupled with a scan. See
View Configuration for more information about view
configuration.pyramid.config.Configurator.add_route()
and
pyramid.config.Configurator.add_view()
to make it more convenient
to register a collection of views as a single class when using
url dispatch. View handlers ship as part of the
pyramid_handlers add-on package.pyramid.interfaces.IViewMapperFactory
interface, which performs
view argument and return value mapping. This is a plug point for
extension builders, not normally used by «civilians».index.html
. If a view is
configured without a name, its name is considered to be the empty
string (which implies the default view).True
or False
for a given request. All predicates
attached to a view configuration must evaluate to true for the
associated view to be considered as a possible callable for a
given request.A term referring both to an isolated Python environment, or the leading tool that allows one to create such environments.
Note: whenever you encounter commands prefixed with $VENV
(Unix)
or %VENV
(Windows), know that that is the environment variable whose
value is the root of the virtual environment in question.
Предупреждение
Comet:
WS:
Concurency:
Asyncio:
Работа с периферией:
Установка:
$ npm install nw
Примечание
Структура файлов:
.
├── index.html
└── package.json
0 directories, 2 files
1 2 3 4 5 6 7 8 9 10 | <!DOCTYPE html>
<html>
<head>
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
We are using node.js <script>document.write(process.version)</script>.
</body>
</html> |
1 2 3 4 5 6 7 8 9 10 11 | {
"name": "Hello",
"version": "1.0.0",
"description": "",
"main": "index.html",
"window": {
"toolbar": false,
"width": 200,
"height": 180
}
} |
Запуск:
$ nw
См.также
Пример online-переводчика, который использует класс WebRequest для обращения к API Яндекс.Переводчика.
См.также
Для удобства в программе используется простой GUI интерфейс.
Онлайн переводчик, в первую очередь, это программа, позволяющая переводить текст с иностранного языка и наоборот, в режиме онлайн. Перевод может быть абсолютно разным, будь-то это фраза, словосочетание, либо большой и длинный литературный текст. Также онлайн переводчик может служить помощником в переписке с зарубежными друзьями или коллегами, в переводе каких-либо иностранных статей. В конце концов, переводчик может быть полезен в том случае, когда Вы пытаетесь что-то кому-то сказать на иностранном языке.
Для создания программы-переводчика использовалось приложение Visual Studio 2013, а также дополнительный фреймворк Json.NET.
Приложение написано на WindowsForms, на языке C#. Для создания дизайна приложения была использована программа Adobe Photoshop CS5.
С помощью данного приложения можно перевести текст на 9 различных языков: Английский, Русский, Иврит, Испанский, Итальянский, Китайский, Немецкий, Французский, Японский. Перевод осуществляется с помощью веб-запросов к API Яндекс.Переводчика.
Для перевода текста достаточно ввести исходный текст в первое окно, указать начальный язык, а также требуемый язык перевода во всплывающих боксах. Для быстрой смены выбранных языков между собой можно воспользоваться центральной кнопкой в виде двух стрелок. Как только Вы выбрали нужные языки и ввели необходимый текст, нажмите кнопку «Перевод». Переведенный текст отобразится во втором окне.
Чтобы работать с API Яндекс Переводчика, потребовалось получить API-ключ(OAuth токен доступа).
OAuth-токен ― это специальный код, разрешающий доступ к данным конкретного пользователя. Для каждого пользователя (логина в Яндекс.Директе, от имени которого осуществляются запросы к API) необходимо получить отдельный токен, который следует указывать при вызове методов.
Ключ был получен на сайте Яндекс https://tech.yandex.ru/translate/
Расположение объектов приложения было произведено при помощи WindowsForms, а затем оформлено при помощи изображений, созданных в Adobe Photoshop CS5. Код приложения написан на языке программирования C#.
Приложение состоит из трех классов: Form1, Translate и Translation.
В классе Form1 описаны объекты приложения и действия над ними. Приложение состоит из двух кнопок, реализованных через pictureBox; Двух текстбоксов, а также трех комбобоксов (всплывающие списки). В двух комбобоксах пользователь осуществляет выбор необходимого языка для перевода. Еще один комбобокс не отображается на экране, а служит лишь для осуществления быстрой смены двух выбранных языков между собой.
При нажатии на кнопку «Перевод» (pictureBox1) происходит вызов функции TranslateText, которая посылает веб-запрос. Однако, при отправлении запроса, требуется указать коды языков для перевода, например(“ru-en” или “it-ru”). Для этого была написана структура switch-case, которая, исходя из выбранных языков в комбобоксах, составляет необходимую нам строку с кодами языков.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TranslatorForWeb
{
public partial class Form1 : Form
{
ComboBox forSwap;//Для свапа выбранных языков
Translate YandexAPI;
public Form1()
{
InitializeComponent();
//---Первоначальные языки в боксах---
comboBox1.SelectedItem = "Английский";
comboBox2.SelectedItem = "Русский";
//---
//---Для свапа выбранных языков---
forSwap = new ComboBox();
forSwap.Items.AddRange(new object[] {
#region Бокс для свапа
"Английский",
"Иврит",
"Испанский",
"Итальянский",
"Китайский",
"Немецкий",
"Русский",
"Французский",
"Японский"});
#endregion
//---
YandexAPI = new Translate();
}
private void Form1_Load(object sender, EventArgs e)
{
}
//===================Кнопка свапа языков========================
private void pictureBox2_Click(object sender, EventArgs e)
{
forSwap.SelectedItem = comboBox2.SelectedItem;
comboBox2.SelectedItem = comboBox1.SelectedItem;
comboBox1.SelectedItem = forSwap.SelectedItem;
}
//==============================================================
private void pictureBox1_Click(object sender, EventArgs e)
{
#region Преобразование языка в код
string inputlang;//Начальный язык
inputlang = (String)Convert.ChangeType(comboBox1.SelectedItem, typeof(String));//Значение из первого бокса
string outputlang;//Язык перевода
outputlang = (String)Convert.ChangeType(comboBox2.SelectedItem, typeof(String));//Значение из второго бокса
//===Преобразование названия языка в его кодовое значение===
//---Для начального языка
switch (inputlang)
{
case "Английский": inputlang = "en"; break;
case "Русский": inputlang = "ru"; break;
case "Иврит": inputlang = "he"; break;
case "Испанский": inputlang = "es"; break;
case "Итальянский": inputlang = "it"; break;
case "Китайский": inputlang = "zh"; break;
case "Немецкий": inputlang = "de"; break;
case "Французский": inputlang = "fr"; break;
case "Японский": inputlang = "ja"; break;
}
//---
//---Для языка перевода
switch (outputlang)
{
case "Английский": outputlang = "en"; break;
case "Русский": outputlang = "ru"; break;
case "Иврит": outputlang = "he"; break;
case "Испанский": outputlang = "es"; break;
case "Итальянский": outputlang = "it"; break;
case "Китайский": outputlang = "zh"; break;
case "Немецкий": outputlang = "de"; break;
case "Французский": outputlang = "fr"; break;
case "Японский": outputlang = "ja"; break;
}
//---
//==========================================================
#endregion
string language = string.Format("{0}-{1}", inputlang, outputlang);//Итоговая строка языков с кодами
richTextBox2.Text = YandexAPI.TranslateText(richTextBox1.Text, language);
}
}
} |
Класс Translate описывает веб-запрос к серверам Яндекс.Переводчика и десериализацию JSON полученного ответа. Запрос к API переводчика содержит ряд обязательных параметров – это:
Необязательные параметры, такие как формат и опции перевода, в приложении не используются.
Так как ответ приходит в виде структуры данных JSON, требуется его
распарсить. Для этого был установлен дополнительный фреймворк
Json.NET
и подключена библиотека Newtonsoft.Json
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;//Пакет JSON
namespace TranslatorForWeb
{
class Translate
{
public string TranslateText(string s, string language)
{
if (s.Length > 0)//Проверка на непустую строку
{
//---Запрос---
WebRequest request = WebRequest.Create("https://translate.yandex.net/api/v1.5/tr.json/translate?"
+ "key=trnsl.1.1.20170125T084253Z.cc366274cc3474e9.68d49c802348b39b5d677c856e0805c433b7618c"//Ключ
+ "&text=" + s//Текст
+ "&lang=" + language);//Язык
WebResponse response = request.GetResponse();
//--------------------
//---Распарсить JSON ответ. Я скачал фреймворк Json.NET
using (StreamReader stream = new StreamReader(response.GetResponseStream()))
{
string line;
if ((line = stream.ReadLine()) != null)
{
Translation translation = JsonConvert.DeserializeObject<Translation>(line);
s = "";
foreach (string str in translation.text)
{
s += str;
}
}
}
//------------------
return s;
}
else
return "";
}
}
class Translation
{
public string code { get; set; }
public string lang { get; set; }
public string[] text { get; set; }
}
} |
Данных класс описывает структуру JSON-ответа на запрос к API переводчика.
Получить практические навыки по работе со спецификацией WSGI
.
Пример WSGI middleware, которое вставляет в тело HTML-страниц строки следующим образом:
<html>
<head>
...
</head>
<body>
<div class='top'>Middleware TOP</div>
...
<div class='botton'>Middleware BOTTOM</div>
</body>
</html>
Пример реализации:
from paste.httpserver import serve
TOP = "<div class='top'>Middleware TOP</div>"
BOTTOM = "<div class='botton'>Middleware BOTTOM</div>"
class WsgiTopBottomMiddleware(object):
'''
WSGI Middleware, которое добавляет TOP, BOTTOM в HTML документ
'''
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
response = self.app(environ, start_response).decode() # bytes to str
if response.find('<body>') > -1:
header, body = response.split('<body>')
data, htmlend = body.split('</body>')
data = '<body>' + TOP + data + BOTTOM+'</body>'
yield (header + data + htmlend).encode() # str to bytes
else:
yield (TOP + response + BOTTOM).encode() # str to bytes
def app(environ, start_response):
'''
WSGI приложение, которое отдает HTML документ
'''
response_code = '200 OK'
response_type = ('Content-Type', 'text/HTML')
start_response(response_code, [response_type])
return '''
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>HTML Document</title>
</head>
<body>
<p>
<b>
Этот текст будет полужирным,
<i>а этот — ещё и курсивным</i>
</b>
</p>
</body>
</html>
'''.encode() # str to bytes
# Оборачиваем WSGI приложение в middleware
app = WsgiTopBottomMiddleware(app)
# Запускаем сервер
serve(app, host='localhost', port=8000)
Описание заданий находится в разделе Работа с протоколом HTTP через telnet.
Написать WSGI приложение, которое отдает статикой файлы index.html и about.html.
Написать WSGI middleware, которое будет вставлять в HTML документ JavaScript и CSS файлы из списка типа:
includes = [
'app.js',
'react.js',
'leaflet.js',
'D3.js',
'moment.js',
'math.js',
'main.css',
'bootstrap.css',
'normalize.css',
]
Следующим образом:
<html>
<head>
...
<link rel="stylesheet" href="/_static/main.css"/>
<link rel="stylesheet" href="/_static/bootstrap.css"/>
<link rel="stylesheet" href="/_static/normalize.css"/>
</head>
<body>
...
<script src="/_static/app.js"></script>
<script src="/_static/react.js"></script>
<script src="/_static/leaflet.js"></script>
<script src="/_static/D3.js"></script>
<script src="/_static/moment.js"></script>
<script src="/_static/math.js"></script>
</body>
</html>
Делать не надо.
Изучить возможности шаблонизаторов на языке программирования Python. Получить практические навыки по обработке HTTP запросов/ответов при помощи библиотеки WebOb.
Пример создания HTTP запроса при помощи библиотеки WebOb.
1 2 3 4 5 6 7 8 | from webob import Request
req = Request.blank('http://en.wikipedia.org/wiki/HTTP')
from pprint import pprint
pprint(req)
print
print(req.get_response()) |
aboutme.html
и index.html
должны наследоваться от
base.html
.get_response()
).Научиться работать с БД используя ORM SQLAlchemy.
Выполнить упражнения из презентации https://bitbucket.org/zzzeek/pycon2013_student_package.
См.также
Полезные ссылки http://wiht.co/python-guide
Примечание
В оф. документации предлагают скачать ртутью с фирменного сайта:
$ hg clone https://hg.python.org/cpython
$ hg update 3.5
Скачиваем с гитхаба python/cpython:
git clone https://github.com/python/cpython.git
Выбираем ветку 3.5
(cpython версии 3.5):
git checkout 3.5
Укажем локальную директорию для сборки:
./configure --prefix=$HOME/Projects/bin/python3.5
Скомпилируем:
make && make install
Теперь можно запускать:
$ $HOME/Projects/bin/python3.5/bin/python3
Python 3.5.0+ (default, Oct 10 2015, 13:35:25)
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> {*range(4), 4, *(5, 6, 7)}
{0, 1, 2, 3, 4, 5, 6, 7}
>>> import asyncio
>>> async def foo(bar): await asyncio.sleep(42)
Укажем виртуальному окружению где находится интерпретатор cpython:
$ mkvirtualenv --python=$HOME/Projects/bin/python3.5/bin/python3 python35_env
Running virtualenv with interpreter /home/uralbash/Projects/bin/python3.5/bin/python3
Using base prefix '/home/uralbash/Projects/bin/python3.5'
New python executable in aiohttp/bin/python3
Also creating executable in aiohttp/bin/python
Installing setuptools, pip, wheel...done.
$ sudo apt-get install python
$ sudo apt-get install python-setuptools python-dev build-essential
$ sudo easy_install pip
$ sudo pip install virtualenv virtualenvwrapper
$ source /usr/local/bin/virtualenvwrapper.sh
Некоторые Python пакеты написаны с использование языка программирования Си, поэтому при установке они требуют компиляции. Если у вас не установлен компилятор, пакет не будет установлен.
$ sudo apt-get install gcc python-dev
$ sudo apt-get intall git
Склонируем репозитарий админки https://github.com/sacrud/pyramid_sacrud.git в
директорию /home/user/Projects
.
$ cd /home/user/Projects/
$ git clone https://github.com/sacrud/pyramid_sacrud.git
Установим pyramid_sacrud из исходных кодов.
$ cd /home/user/Projects/pyramid_sacrud
$ mkvirtualenv pyramid_sacrud
$ python setup.py develop
Далее установим пример pyramid_sacrud/example
$ cd /home/user/Projects/pyramid_sacrud/example
$ workon pyramid_sacrud
$ python setup.py develop
Пакеты устанавливаются в виртуальное окружение с названием pyramid_sacrud
.
Теперь можно запустить пример:
$ cd /home/user/Projects/pyramid_sacrud/example
$ workon pyramid_sacrud
$ pserve development.ini
Заходим на http://localhost:6543/admin/
Homebrew
Homebrew является очень удобным пакетным менеджером для MacOS. Все дальнейшие манипуляции по установке пакетов будут осуществлены с его использованием (где это возможно, конечно).
Установка
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew install python
При использовании Homebrew для установки python’а pip поставится автоматически.
$ sudo pip install virtualenv virtualenvwrapper
$ source /usr/local/bin/virtualenvwrapper.sh
Некоторые Python пакеты написаны с использование языка программирования Си, поэтому при установке они требуют компиляции. Если у вас не установлен компилятор, пакет не будет установлен.
$ brew install gcc
Для успешной установки GCC необходимо наличие установленного XCode в системе.
Примечание
Для старых версий MacOS необходимо установить старую же версию XCode с диска, который поставляется вместе с Вашей операционной системой.
$ brew intall git
Склонируем репозитарий админки https://github.com/sacrud/pyramid_sacrud.git в
директорию /home/user/Projects
.
$ cd /home/user/Projects/
$ git clone https://github.com/sacrud/pyramid_sacrud.git
Установим pyramid_sacrud из исходных кодов.
$ cd /home/user/Projects/pyramid_sacrud
$ mkvirtualenv pyramid_sacrud
$ python setup.py develop
Далее установим пример pyramid_sacrud/example
$ cd /home/user/Projects/pyramid_sacrud/example
$ workon pyramid_sacrud
$ python setup.py develop
Пакеты устанавливаются в виртуальное окружение с названием pyramid_sacrud
.
Теперь можно запустить пример:
$ cd /home/user/Projects/pyramid_sacrud/example
$ workon pyramid_sacrud
$ pserve development.ini
Заходим на http://localhost:6543/admin/
Все версии CPython можно найти по адресу https://www.python.org/downloads/
Выберем, например, версию 2.7.10 для 32 битной операционной системы.
Запускаем инсталятор:
По умолчанию Python устанавливается в директорию C:\Python27\
.
Выбираем опцию «добавить python.exe в окружение».
Теперь интерпретатор Python доступен из консоли.
Пример Hello Word!.
После установки CPython в окружении появится утилита easy_install
. С
помощью нее можно установит pip, следующим образом:
$ easy_install pip
Или при помощи скрипта get-pip.py
.
Скрипт можно скачать по прямой ссылке
https://raw.github.com/pypa/pip/master/contrib/get-pip.py
Запускается скрипт как обычная Python программа.
Теперь можно устанавливать Python пакеты.
Зададим переменную окружения WORKON_HOME
которая указывает где будут
хранится изолированные окружения.
Теперь можно создавать изолированные окружения для каждого проекта.
Некоторые Python пакеты написаны с использование языка программирования Си, поэтому при установке они требуют компиляции. Если у вас не установлен компилятор, пакет не будет установлен.
Попробуем установить NumPy без компилятора.
$ pip install numpy
После установки следующих приложений для Windows:
Компиляция пройдет успешно:
Склонируем репозитарий админки https://github.com/sacrud/pyramid_sacrud.git в
директорию C:\Projects
.
$ git clone https://github.com/sacrud/pyramid_sacrud.git
Установим pyramid_sacrud из исходных кодов.
$ cd C:\Projects\pyramid_sacrud
$ mkvirtualenv pyramid_sacrud
$ python setup.py develop
Далее установим пример pyramid_sacrud/example
$ cd C:\Projects\pyramid_sacrud\example
$ workon pyramid_sacrud
$ python setup.py develop
Пакеты устанавливаются в виртуальное окружение с названием pyramid_sacrud
.
Установим дополнительные пакеты six
, pyramid_jinja2==1.10
и iso8601
:
$ pip install six iso8601 pyramid_jinja2==1.10
Теперь можно запустить пример:
$ cd C:\Projects\pyramid_sacrud\example
$ workon pyramid_sacrud
$ pserve development.ini
Заходим на http://localhost:6543/admin/
См.также
Anaconda – свободный open source дистрибутив для языков программирования Python и R с открытым кодом для обработки данных большого объема, построения аналитических прогнозов и научных вычислений. Разработчики дистрибутива имеют цель упростить управление и использование пакетов. Версии пакетов контролируются системой управления пакетами conda. По умолчанию, вместе с Anaconda устанавливается также:
После установки дистрибутива Anaconda в командной строке (cmd.exe) должна появится команда пакетного менеджера conda.
Проверим версию выполнив команду в терминале:
C:\Users\user>conda --version
conda 4.3.30
Неплохо было бы обновится до последней версии, делается это командой update:
C:\Users\user>conda update conda
Fetching package metadata ...............
Solving package specifications: .
Package plan for installation in environment C:\Users\user\Anaconda3:
The following packages will be UPDATED:
conda: 4.3.30-py36h404fb56_0 --> 4.5.11-py36_0
pycosat: 0.6.1-py36_1 --> 0.6.3-py36hfa6e2cd_0
Proceed ([y]/n)? y
pycosat-0.6.3- 100% |###############################| Time: 0:00:00 1.40 MB/s
conda-4.5.11-p 100% |###############################| Time: 0:00:00 5.15 MB/s
Anaconda дополнительно устанавливает множество различных python пакетов для того, что бы узнать, что у нас установлено необходимо выполнить команду list:
C:\Users\user\Project\pyramid_test>conda list
# packages in environment at C:\Users\user\Anaconda3:
#
# Name Version Build Channel
_license 1.1 py36_1
alabaster 0.7.9 py36_0
anaconda custom py36_0
anaconda-client 1.6.0 py36_0
anaconda-navigator 1.5.0 py36_0
anaconda-project 0.4.1 py36_0
anyqt 0.0.8 py36_0
astroid 1.4.9 py36_0
astropy 1.3 np111py36_0
babel 2.3.4 py36_0
backports 1.0 py36_0
beautifulsoup4 4.5.3 py36_0
bitarray 0.8.1 py36_1
blas 1.0 mkl
blaze 0.10.1 py36_0
bokeh 0.12.4 py36_0
boto 2.45.0 py36_0
bottleneck 1.2.0 np111py36_0
bzip2 1.0.6 vc14_3 [vc14]
cffi 1.9.1 py36_0
chardet 2.3.0 py36_0
chest 0.2.3 py36_0
click 6.7 py36_0
cloudpickle 0.2.2 py36_0
clyent 1.2.2 py36_0
colorama 0.3.7 py36_0
comtypes 1.1.2 py36_0
conda 4.5.11 py36_0
conda-env 2.6.0 0
configobj 5.0.6 py36_0
console_shortcut 0.1.1 py36_1
contextlib2 0.5.4 py36_0
cryptography 1.7.1 py36_0
curl 7.52.1 vc14_0 [vc14]
...
Conda позволяет создавать виртуальные окружения для изолированной разработки программ. Делается это при помощи команды create:
C:\Users\user>conda create --name myenv sqlite
Solving environment: done
## Package Plan ##
environment location: C:\Users\user\Anaconda3\envs\myenv
added / updated specs:
- sqlite
The following packages will be downloaded:
package | build
---------------------------|-----------------
vc-14.1 | h0510ff6_4 6 KB
sqlite-3.25.2 | hfa6e2cd_0 897 KB
vs2015_runtime-14.15.26706 | h3a45250_0 2.1 MB
------------------------------------------------------------
Total: 2.9 MB
The following NEW packages will be INSTALLED:
sqlite: 3.25.2-hfa6e2cd_0
vc: 14.1-h0510ff6_4
vs2015_runtime: 14.15.26706-h3a45250_0
Proceed ([y]/n)? y
Downloading and Extracting Packages
vc-14.1 | 6 KB | ############################################################################ | 100%
sqlite-3.25.2 | 897 KB | ############################################################################ | 100%
vs2015_runtime-14.15 | 2.1 MB | ############################################################################ | 100%
Preparing transaction: done
Verifying transaction: done
Executing transaction: done
#
# To activate this environment, use:
# > activate myenv
#
# To deactivate an active environment, use:
# > deactivate
#
# * for power-users using bash, you must source
#
C:\Users\user>
Активация виртуального окружения осуществляется при помощи команды activate:
C:\Users\user>activate myenv
(myenv) C:\Users\user>conda list
# packages in environment at C:\Users\user\Anaconda3\envs\myenv:
#
# Name Version Build Channel
sqlite 3.25.2 hfa6e2cd_0
vc 14.1 h0510ff6_4
vs2015_runtime 14.15.26706 h3a45250_0
После активации, мы как бы находимся внутри изолированного окружения, подтверждением этого является пригласительная надпись в круглых скобках в начале строки с именем нашего окружения (myenv). Теперь если запустить команду list (список установленных пакетов) мы получим намного меньший список только того, что установлено в наше новое виртуальное окружение.
Пакетный менеджер pip это универсальный инструмент установки пакетов в мире python, он устанавливает официальные пакеты из общего хранилища пакетов PyPi. Поэтому pip незаменимый инструмент для разработки на Python. Установим его при помощи команды install.
(myenv) C:\Users\user>conda install pip
Solving environment: done
==> WARNING: A newer version of conda exists. <==
current version: 4.4.6
latest version: 4.5.11
Please update conda by running
$ conda update -n base conda
## Package Plan ##
environment location: C:\Users\user\Anaconda3\envs\myenv
added / updated specs:
- pip
The following packages will be downloaded:
package | build
---------------------------|-----------------
setuptools-40.4.3 | py37_0 575 KB
wincertstore-0.2 | py37_0 13 KB
pip-10.0.1 | py37_0 1.7 MB
python-3.7.1 | h33f27b4_3 18.6 MB
wheel-0.32.2 | py37_0 51 KB
sqlite-3.20.1 | vc14hf772eac_1 796 KB
certifi-2018.10.15 | py37_0 138 KB
------------------------------------------------------------
Total: 21.9 MB
The following NEW packages will be INSTALLED:
certifi: 2018.10.15-py37_0
pip: 10.0.1-py37_0
python: 3.7.1-h33f27b4_3
setuptools: 40.4.3-py37_0
wheel: 0.32.2-py37_0
wincertstore: 0.2-py37_0
The following packages will be DOWNGRADED:
sqlite: 3.25.2-hfa6e2cd_0 --> 3.20.1-vc14hf772eac_1
Proceed ([y]/n)? y
Downloading and Extracting Packages
setuptools 40.4.3: ############################################################################################ | 100%
wincertstore 0.2: ############################################################################################# | 100%
pip 10.0.1: ################################################################################################### | 100%
python 3.7.1: ################################################################################################# | 100%
wheel 0.32.2: ################################################################################################# | 100%
sqlite 3.20.1: ################################################################################################ | 100%
certifi 2018.10.15: ########################################################################################### | 100%
Preparing transaction: done
Verifying transaction: done
Executing transaction: done
Теперь нам доступны все пакеты с PyPi, установим фреймворк Pyramid:
(myenv) C:\Users\user>pip install pyramid
Collecting pyramid
Downloading https://files.pythonhosted.org/packages/85/c7/0a14873ef7bbb6d30e38678334d5b5faee1ccae2f5a59f093d104a3cc5ee/pyramid-1.9.2-py2.py3-none-any.whl (582kB)
100% |████████████████████████████████| 583kB 4.0MB/s
Collecting zope.deprecation>=3.5.0 (from pyramid)
Downloading https://files.pythonhosted.org/packages/ee/33/625098914ec59b3006adf2cdf44a721e9671f4836af9eeb8cbe14e485954/zope.deprecation-4.3.0-py2.py3-none-any.whl
Collecting zope.interface>=3.8.0 (from pyramid)
Downloading https://files.pythonhosted.org/packages/55/99/f728599ef08137889cacc58c08e3b1affe974fcd029528a822ec7b7efffa/zope.interface-4.6.0-cp37-cp37m-win32.whl (132kB)
100% |████████████████████████████████| 133kB 2.0MB/s
Collecting plaster-pastedeploy (from pyramid)
Downloading https://files.pythonhosted.org/packages/d9/e2/de7cd499923dbf6aacc9b243f262817bfea3ffbbd4dcc5847e1aaec817a7/plaster_pastedeploy-0.6-py2.py3-none-any.whl
Collecting translationstring>=0.4 (from pyramid)
Downloading https://files.pythonhosted.org/packages/26/e7/9dcf5bcd32b3ad16db542845ad129c06927821ded434ae88f458e6190626/translationstring-1.3-py2.py3-none-any.whl
Requirement already satisfied: setuptools in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from pyramid) (40.4.3)
Collecting PasteDeploy>=1.5.0 (from pyramid)
Downloading https://files.pythonhosted.org/packages/31/28/51201a54aeecbd02eff767d17050b302f6fd98fdfecb4e3f4c9301ba6ef8/PasteDeploy-1.5.2-py2.py3-none-any.whl
Collecting plaster (from pyramid)
Downloading https://files.pythonhosted.org/packages/61/29/3ac8a5d03b2d9e6b876385066676472ba4acf93677acfc7360b035503d49/plaster-1.0-py2.py3-none-any.whl
Collecting WebOb>=1.7.0 (from pyramid)
Downloading https://files.pythonhosted.org/packages/b5/74/a9aaec7ca6c94a58e379a9c95255a2b2017514948054c72c0d1a25953348/WebOb-1.8.3-py2.py3-none-any.whl (113kB)
100% |████████████████████████████████| 122kB 3.8MB/s
Collecting repoze.lru>=0.4 (from pyramid)
Downloading https://files.pythonhosted.org/packages/b0/30/6cc0c95f0b59ad4b3b9163bff7cdcf793cc96fac64cf398ff26271f5cf5e/repoze.lru-0.7-py3-none-any.whl
Collecting hupper (from pyramid)
Downloading https://files.pythonhosted.org/packages/70/b7/4013ae11e977d4a38141ecba1c754f8b0a826b182de0c5c6fb780ede9834/hupper-1.3.1-py2.py3-none-any.whl
Collecting venusian>=1.0a3 (from pyramid)
Downloading https://files.pythonhosted.org/packages/2f/c2/3d122e19287ed7d73f03821cef87e53673f27d41cae54ee3a46e92b147e2/venusian-1.1.0-py2.py3-none-any.whl
Installing collected packages: zope.deprecation, zope.interface, PasteDeploy, plaster, plaster-pastedeploy, translationstring, WebOb, repoze.lru, hupper, venusian, pyramid
Successfully installed PasteDeploy-1.5.2 WebOb-1.8.3 hupper-1.3.1 plaster-1.0 plaster-pastedeploy-0.6 pyramid-1.9.2 repoze.lru-0.7 translationstring-1.3 venusian-1.1.0 zope.deprecation-4.3.0 zope.interface-4.6.0
You are using pip version 10.0.1, however version 18.1 is available.
You should consider upgrading via the 'python -m pip install --upgrade pip' command.
Проверим что пакет установился командой list:
(myenv) C:\Users\user>conda list
# packages in environment at C:\Users\user\Anaconda3\envs\myenv:
#
certifi 2018.10.15 py37_0
hupper 1.3.1 <pip>
PasteDeploy 1.5.2 <pip>
pip 10.0.1 py37_0
plaster 1.0 <pip>
plaster-pastedeploy 0.6 <pip>
pyramid 1.9.2 <pip>
python 3.7.1 h33f27b4_3
repoze.lru 0.7 <pip>
setuptools 40.4.3 py37_0
sqlite 3.20.1 vc14hf772eac_1 []
translationstring 1.3 <pip>
vc 14.1 h0510ff6_4 []
venusian 1.1.0 <pip>
vs2015_runtime 14.15.26706 h3a45250_0 []
WebOb 1.8.3 <pip>
wheel 0.32.2 py37_0
wincertstore 0.2 py37_0
zope.deprecation 4.3.0 <pip>
zope.interface 4.6.0 <pip>
См.также
Для примера создадим стартовую Веб-страницу при помощи фреймворка Pyramid. Для её создания будем использовать готовый шаблон проекта, который можно установить при помощи программы cookiecutter. Скачаем cookiecutter:
(myenv) C:\Users\user\Project\pyramid_test>pip install cookiecutter
Collecting cookiecutter
Downloading https://files.pythonhosted.org/packages/16/99/1ca3a75978270288354f419e9166666801cf7e7d8df984de44a7d5d8b8d0/cookiecutter-1.6.0-py2.py3-none-any.whl (50kB)
100% |████████████████████████████████| 51kB 584kB/s
Collecting requests>=2.18.0 (from cookiecutter)
Downloading https://files.pythonhosted.org/packages/f1/ca/10332a30cb25b627192b4ea272c351bce3ca1091e541245cccbace6051d8/requests-2.20.0-py2.py3-none-any.whl (60kB)
100% |████████████████████████████████| 61kB 1.5MB/s
Collecting poyo>=0.1.0 (from cookiecutter)
Downloading https://files.pythonhosted.org/packages/e0/16/e00e3001007a5e416ca6a51def6f9e4be6a774bf1c8486d20466f834d113/poyo-0.4.2-py2.py3-none-any.whl
Collecting click>=5.0 (from cookiecutter)
Downloading https://files.pythonhosted.org/packages/fa/37/45185cb5abbc30d7257104c434fe0b07e5a195a6847506c074527aa599ec/Click-7.0-py2.py3-none-any.whl (81kB)
100% |████████████████████████████████| 81kB 6.8MB/s
Collecting jinja2>=2.7 (from cookiecutter)
Downloading https://files.pythonhosted.org/packages/7f/ff/ae64bacdfc95f27a016a7bed8e8686763ba4d277a78ca76f32659220a731/Jinja2-2.10-py2.py3-none-any.whl (126kB)
100% |████████████████████████████████| 133kB 8.9MB/s
Collecting future>=0.15.2 (from cookiecutter)
Downloading https://files.pythonhosted.org/packages/85/aa/ba2e24dcb889d7e98733f87515d80b3512418b80ba79d82d2ddcd43fadf3/future-0.17.0.tar.gz (827kB)
100% |████████████████████████████████| 829kB 3.1MB/s
Collecting whichcraft>=0.4.0 (from cookiecutter)
Downloading https://files.pythonhosted.org/packages/ab/c6/eb4d1dfbb68168bb01c4394420e5e71d5851e64b910838aa0f14ebd5c7a0/whichcraft-0.5.2-py2.py3-none-any.whl
Collecting jinja2-time>=0.1.0 (from cookiecutter)
Downloading https://files.pythonhosted.org/packages/6a/a1/d44fa38306ffa34a7e1af09632b158e13ec89670ce491f8a15af3ebcb4e4/jinja2_time-0.2.0-py2.py3-none-any.whl
Collecting binaryornot>=0.2.0 (from cookiecutter)
Downloading https://files.pythonhosted.org/packages/24/7e/f7b6f453e6481d1e233540262ccbfcf89adcd43606f44a028d7f5fae5eb2/binaryornot-0.4.4-py2.py3-none-any.whl
Collecting urllib3<1.25,>=1.21.1 (from requests>=2.18.0->cookiecutter)
Downloading https://files.pythonhosted.org/packages/8c/4b/5cbc4cb46095f369117dcb751821e1bef9dd86a07c968d8757e9204c324c/urllib3-1.24-py2.py3-none-any.whl (117kB)
100% |████████████████████████████████| 122kB 4.1MB/s
Collecting idna<2.8,>=2.5 (from requests>=2.18.0->cookiecutter)
Downloading https://files.pythonhosted.org/packages/4b/2a/0276479a4b3caeb8a8c1af2f8e4355746a97fab05a372e4a2c6a6b876165/idna-2.7-py2.py3-none-any.whl (58kB)
100% |████████████████████████████████| 61kB 4.7MB/s
Collecting chardet<3.1.0,>=3.0.2 (from requests>=2.18.0->cookiecutter)
Downloading https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl (133kB)
100% |████████████████████████████████| 143kB 7.6MB/s
Requirement already satisfied: certifi>=2017.4.17 in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from requests>=2.18.0->cookiecutter) (2018.10.15)
Collecting MarkupSafe>=0.23 (from jinja2>=2.7->cookiecutter)
Downloading https://files.pythonhosted.org/packages/4d/de/32d741db316d8fdb7680822dd37001ef7a448255de9699ab4bfcbdf4172b/MarkupSafe-1.0.tar.gz
Collecting arrow (from jinja2-time>=0.1.0->cookiecutter)
Downloading https://files.pythonhosted.org/packages/e0/86/4eb5228a43042e9a80fe8c84093a8a36f5db34a3767ebd5e1e7729864e7b/arrow-0.12.1.tar.gz (65kB)
100% |████████████████████████████████| 71kB 2.0MB/s
Collecting python-dateutil (from arrow->jinja2-time>=0.1.0->cookiecutter)
Downloading https://files.pythonhosted.org/packages/2f/e9/b02e8a1a8c53a55a4f37df1e8e111539d0a3e76828bcd252947a5200b797/python_dateutil-2.7.4-py2.py3-none-any.whl (211kB)
100% |████████████████████████████████| 215kB 2.9MB/s
Collecting six>=1.5 (from python-dateutil->arrow->jinja2-time>=0.1.0->cookiecutter)
Downloading https://files.pythonhosted.org/packages/67/4b/141a581104b1f6397bfa78ac9d43d8ad29a7ca43ea90a2d863fe3056e86a/six-1.11.0-py2.py3-none-any.whl
Building wheels for collected packages: future, MarkupSafe, arrow
Running setup.py bdist_wheel for future ... done
Stored in directory: C:\Users\user\AppData\Local\pip\Cache\wheels\fc\5b\ec\2983c4a6e3692d1315f44d6480c6abdd8585d96471b431d6b4
Running setup.py bdist_wheel for MarkupSafe ... done
Stored in directory: C:\Users\user\AppData\Local\pip\Cache\wheels\33\56\20\ebe49a5c612fffe1c5a632146b16596f9e64676768661e4e46
Running setup.py bdist_wheel for arrow ... done
Stored in directory: C:\Users\user\AppData\Local\pip\Cache\wheels\a3\dd\b2\d3b8d22e8136164c2e2c36ed42392531957cdf9c717065b28b
Successfully built future MarkupSafe arrow
Installing collected packages: urllib3, idna, chardet, requests, poyo, click, MarkupSafe, jinja2, future, whichcraft, six, python-dateutil, arrow, jinja2-time, binaryornot, cookiecutter
Successfully installed MarkupSafe-1.0 arrow-0.12.1 binaryornot-0.4.4 chardet-3.0.4 click-7.0 cookiecutter-1.6.0 future-0.17.0 idna-2.7 jinja2-2.10 jinja2-time-0.2.0 poyo-0.4.2 python-dateutil-2.7.4 requests-2.20.0 six-1.11.0 urllib3-1.24 whichcraft-0.5.2
You are using pip version 10.0.1, however version 18.1 is available.
You should consider upgrading via the 'python -m pip install --upgrade pip' command.
При помощи cookiecutter развернем самый простой шаблон Веб-сайта который имеется в фреймворке Pyramid:
(myenv) C:\Users\user\Project\pyramid_test>cookiecutter gh:Pylons/pyramid-cookiecutter-starter
project_name [Pyramid Scaffold]: myfirstapp
repo_name [myfirstapp]:
Select template_language:
1 - jinja2
2 - chameleon
3 - mako
Choose from 1, 2, 3 (1, 2, 3) [1]: 1
===============================================================================
Documentation: https://docs.pylonsproject.org/projects/pyramid/en/latest/
Tutorials: https://docs.pylonsproject.org/projects/pyramid_tutorials/en/latest/
Twitter: https://twitter.com/PylonsProject
Mailing List: https://groups.google.com/forum/#!forum/pylons-discuss
Welcome to Pyramid. Sorry for the convenience.
===============================================================================
Change directory into your newly created project.
cd myfirstapp
Create a Python virtual environment.
py -3 -m venv env
Upgrade packaging tools.
env\Scripts\pip install --upgrade pip setuptools
Install the project in editable mode with its testing requirements.
env\Scripts\pip install -e ".[testing]"
Run your project's tests.
env\Scripts\pytest
Run your project.
env\Scripts\pserve development.ini
Проект создается в отдельной директории myfirstapp
.
(myenv) C:\Users\user\Project\pyramid_test>dir
Том в устройстве C не имеет метки.
Серийный номер тома: 480D-DE95
Содержимое папки C:\Users\user\Project\pyramid_test
26.10.2018 16:30 <DIR> .
26.10.2018 16:30 <DIR> ..
26.10.2018 16:30 <DIR> myfirstapp
0 файлов 0 байт
3 папок 31 729 090 560 байт свободно
Что бы запустить проект необходимо прежде установить его в окружение, перейдем в директорию проекта и запустим стандартную команду установки пакетов из исходников.
(myenv) C:\Users\user\Project\pyramid_test>cd myfirstapp
(myenv) C:\Users\user\Project\pyramid_test\myfirstapp>pip install -e .
Obtaining file:///C:/Users/user/Project/pyramid_test/myfirstapp
Requirement already satisfied: plaster_pastedeploy in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from myfirstapp==0.0) (0.6)
Requirement already satisfied: pyramid in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from myfirstapp==0.0) (1.9.2)
Collecting pyramid_jinja2 (from myfirstapp==0.0)
Downloading https://files.pythonhosted.org/packages/21/30/fdd0b9a365a60c9e56ae4730c8839eae603f7a87696df14dbd4f980acf35/pyramid_jinja2-2.7-py2.py3-none-any.whl (70kB)
100% |████████████████████████████████| 71kB 421kB/s
Collecting pyramid_debugtoolbar (from myfirstapp==0.0)
Downloading https://files.pythonhosted.org/packages/6f/9a/933267076461c1fd6f4f8b0715ecf037dbe622180d0b77e7ea605a32b51b/pyramid_debugtoolbar-4.5-py2.py3-none-any.whl (345kB)
100% |████████████████████████████████| 348kB 2.3MB/s
Collecting waitress (from myfirstapp==0.0)
Downloading https://files.pythonhosted.org/packages/ee/af/ac32a716d64e56561ee9c23ce45ee2865d7ac4e0678b737d2f5ee49b5fd6/waitress-1.1.0-py2.py3-none-any.whl (114kB)
100% |████████████████████████████████| 122kB 3.7MB/s
Requirement already satisfied: PasteDeploy>=1.5.0 in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from plaster_pastedeploy->myfirstapp==0.0) (1.5.2)
Requirement already satisfied: plaster>=0.5 in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from plaster_pastedeploy->myfirstapp==0.0) (1.0)
Requirement already satisfied: zope.interface>=3.8.0 in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from pyramid->myfirstapp==0.0) (4.6.0)
Requirement already satisfied: translationstring>=0.4 in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from pyramid->myfirstapp==0.0) (1.3)
Requirement already satisfied: zope.deprecation>=3.5.0 in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from pyramid->myfirstapp==0.0) (4.3.0)
Requirement already satisfied: setuptools in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from pyramid->myfirstapp==0.0) (40.4.3)
Requirement already satisfied: WebOb>=1.7.0 in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from pyramid->myfirstapp==0.0) (1.8.3)
Requirement already satisfied: hupper in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from pyramid->myfirstapp==0.0) (1.3.1)
Requirement already satisfied: repoze.lru>=0.4 in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from pyramid->myfirstapp==0.0) (0.7)
Requirement already satisfied: venusian>=1.0a3 in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from pyramid->myfirstapp==0.0) (1.1.0)
Requirement already satisfied: MarkupSafe in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from pyramid_jinja2->myfirstapp==0.0) (1.0)
Requirement already satisfied: Jinja2>=2.5.0 in c:\users\user\anaconda3\envs\myenv\lib\site-packages (from pyramid_jinja2->myfirstapp==0.0) (2.10)
Collecting Pygments (from pyramid_debugtoolbar->myfirstapp==0.0)
Downloading https://files.pythonhosted.org/packages/02/ee/b6e02dc6529e82b75bb06823ff7d005b141037cb1416b10c6f00fc419dca/Pygments-2.2.0-py2.py3-none-any.whl (841kB)
100% |████████████████████████████████| 849kB 1.9MB/s
Collecting pyramid-mako>=0.3.1 (from pyramid_debugtoolbar->myfirstapp==0.0)
Downloading https://files.pythonhosted.org/packages/f1/92/7e69bcf09676d286a71cb3bbb887b16595b96f9ba7adbdc239ffdd4b1eb9/pyramid_mako-1.0.2.tar.gz
Collecting Mako>=0.8 (from pyramid-mako>=0.3.1->pyramid_debugtoolbar->myfirstapp==0.0)
Downloading https://files.pythonhosted.org/packages/eb/f3/67579bb486517c0d49547f9697e36582cd19dafb5df9e687ed8e22de57fa/Mako-1.0.7.tar.gz (564kB)
100% |████████████████████████████████| 573kB 1.5MB/s
Building wheels for collected packages: pyramid-mako, Mako
Running setup.py bdist_wheel for pyramid-mako ... done
Stored in directory: C:\Users\user\AppData\Local\pip\Cache\wheels\08\5f\98\3dfc5a39bcb3fd094897db7f394eb13768cdf472bdf2a89a2f
Running setup.py bdist_wheel for Mako ... done
Stored in directory: C:\Users\user\AppData\Local\pip\Cache\wheels\15\35\25\dbcb848832ccb1a4b4ad23f529badfd3bce9bf88017f7ca510
Successfully built pyramid-mako Mako
Installing collected packages: pyramid-jinja2, Pygments, Mako, pyramid-mako, pyramid-debugtoolbar, waitress, myfirstapp
Running setup.py develop for myfirstapp
Successfully installed Mako-1.0.7 Pygments-2.2.0 myfirstapp pyramid-debugtoolbar-4.5 pyramid-jinja2-2.7 pyramid-mako-1.0.2 waitress-1.1.0
You are using pip version 10.0.1, however version 18.1 is available.
You should consider upgrading via the 'python -m pip install --upgrade pip' command.
Проверяем что все поставилось:
(myenv) C:\Users\user\Project\pyramid_test\myfirstapp>conda list
# packages in environment at C:\Users\user\Anaconda3\envs\myenv:
#
arrow 0.12.1 <pip>
binaryornot 0.4.4 <pip>
certifi 2018.10.15 py37_0
chardet 3.0.4 <pip>
Click 7.0 <pip>
cookiecutter 1.6.0 <pip>
future 0.17.0 <pip>
hupper 1.3.1 <pip>
idna 2.7 <pip>
Jinja2 2.10 <pip>
jinja2-time 0.2.0 <pip>
Mako 1.0.7 <pip>
MarkupSafe 1.0 <pip>
myfirstapp 0.0 <pip>
numpy 1.15.3 <pip>
PasteDeploy 1.5.2 <pip>
pip 10.0.1 py37_0
plaster 1.0 <pip>
plaster-pastedeploy 0.6 <pip>
poyo 0.4.2 <pip>
Pygments 2.2.0 <pip>
pyramid 1.9.2 <pip>
pyramid-debugtoolbar 4.5 <pip>
pyramid-jinja2 2.7 <pip>
pyramid-mako 1.0.2 <pip>
python 3.7.1 h33f27b4_3
python-dateutil 2.7.4 <pip>
repoze.lru 0.7 <pip>
requests 2.20.0 <pip>
setuptools 40.4.3 py37_0
six 1.11.0 <pip>
sqlite 3.20.1 vc14hf772eac_1 []
translationstring 1.3 <pip>
urllib3 1.24 <pip>
vc 14.1 h0510ff6_4 []
venusian 1.1.0 <pip>
vs2015_runtime 14.15.26706 h3a45250_0 []
waitress 1.1.0 <pip>
WebOb 1.8.3 <pip>
wheel 0.32.2 py37_0
whichcraft 0.5.2 <pip>
wincertstore 0.2 py37_0
zope.deprecation 4.3.0 <pip>
zope.interface 4.6.0 <pip>
Последний шаг это запуск самого Веб-приложения, после его установки в окружение
должна появиться команда pserve
она позволяет запускать WSGI
приложения
которым и является наш проект. Давайте попробуем это сделать:
(myenv) C:\Users\user\Project\pyramid_test\myfirstapp>pserve development.ini --reload
Starting monitor for PID 1144.
Starting server in PID 1144.
Serving on http://DESKTOP-9JPISDO:6543
Serving on http://DESKTOP-9JPISDO:6543
Заходим на http://localhost:6543/
$ python3 -m venv .env
$ source .env/bin/activate
$ which python
См.также
$ sudo apt-get install python-pip python-dev build-essential
$ sudo pip install --upgrade pip
$ sudo pip install pyramid
$ pcreate -t alchemy MyProgect
$ sudo apt-get install python-setuptools python-dev build-essential
$ sudo easy_install pip
$ sudo pip install --upgrade pip
$ sudo pip install pyramid
$ pcreate -t alchemy MyProgect
$ pip uninstall django # Удаление пакета
$ pip install pyramid # Установка пакета
$ pip install pyramid -U # Обновление
$ pip install pyramid --upgrade
$ pip install pip -U # Обновление самого pip
$ pip install pyramid --user # Установка локально, для этого пользователя
$ pip install -r requirements.txt # Установка из файла
$ pip install git+https://github.com/pylons/pyramid # Установка по ссылке
$ pip install git+https://bitbucket.org/zzzeek/sqlalchemy # Установка по ссылке
Копирует проект в PYTHONPAH
$ git clone git@github.com:myint/rstcheck.git
$ cd rstcheck
$ pip install .
Симлинк на директорию. Требуется для разработки, чтобы не устанавливать заново, после каждого изменения в проекте.
$ git clone git@github.com:myint/rstcheck.git
$ cd rstcheck
$ pip install -e .
См.также
1 2 | with file("/tmp/foo", "w") as foo:
print >> foo, "Hello!" |
Эквивалентно
1 2 3 4 5 | foo = file("/tmp/foo", "w")
try:
print >> foo, "Hello!"
finally:
foo.close() |
Декоратор подменяет функцию, например мы можем подменить функцию foo
на
ноль.
def zero(func):
return 0
@zero
def foo():
return "Hi"
print(foo) # 0
print(foo()) # Вызовет ошибку как-будто мы хотим вызвать ноль 0()
Это равносильно следующему коду:
def foo():
return "Hi"
foo = 0
print(foo) # 0
print(foo()) # Вызовет ошибку как-будто мы хотим вызвать ноль 0()
Подменим функцию на другую:
def zero(func):
return lambda: 0
@zero
def foo():
return "Hi"
print(foo()) # 0
Теперь foo
это lambda: 0
, а foo()
соответственно 0
.
Это равносильно следующему коду:
def foo():
return "Hi"
foo = lambda: 0
print(foo()) # 0
И более практичный пример, дополним нашу функцию:
def world(func):
return lambda: func() + " World!"
@world
def foo():
return "Hi"
print(foo()) # Hi World!
@world
def hello():
return "Hello"
print(hello()) # Hello World!
Этот пример уже сложнее переписать:
def foo():
return "Hi"
foo = lambda: foo() + " World!"
print(foo()) # RuntimeError: maximum recursion depth exceeded
def foo():
return "Hi"
hello_world = lambda: foo() + " World!"
print(bar()) # Hello World!
См.также
Декораторы для асинхронных функций пишутся как и для обычных только возвращать нужно корутину, а не функцию.
Для примера обычная функция:
def plusplus(func):
def wrapped():
return func() + 1
return wrapped
@plusplus
def one():
return 1
print(one()) # return 2
@plusplus
@plusplus
@plusplus
@plusplus
def one():
return 1
print(one()) # now return 5
В декораторе наша обертка над функцией (wrapped
) стала корутиной:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import timeit
import asyncio
def plusplus(func):
async def wrapped():
await asyncio.sleep(1)
return await func() + 1
return wrapped
@plusplus
async def one():
await asyncio.sleep(2)
return 1
loop = asyncio.get_event_loop()
start = timeit.default_timer()
print(loop.run_until_complete(one())) # return 2
stop = timeit.default_timer()
print(stop - start) # minimum 3 seconds
@plusplus
@plusplus
@plusplus
@plusplus
async def one():
await asyncio.sleep(2)
return 1
start = timeit.default_timer()
print(loop.run_until_complete(one())) # return 5
stop = timeit.default_timer()
print(stop - start) # minimum 6 seconds |
Более практичный пример это функция json_response
для вьюх в
aiohttp. Идея взята из презентации
(http://igordavydenko.com/talks/lvivpy-4/#slide-31).
import ujson
import asyncio
from aiohttp import web
def json_response(data, **kwargs):
kwargs.setdefault('content_type', 'application/json')
return web.Response(text=ujson.dumps(data), **kwargs)
async def index(request):
return json_response({"Hello": "World"})
Все хорошо но ретурнов во вьюхе может быть много и тогда оборачивать каждый в
json_response
довольно неудобно. Чтобы решить эту проблему создадим
декоратор json_view
.
def json_view(func):
async def wrapped(request):
return json_response(await func(request))
return wrapped
Теперь можно писать так:
@json_view
async def index(request):
if somethink:
return {"Somethink": "happens"}
else:
return {"else": "happens"}
return {"Hello": "World"}
Класс aiohttp.web.Response
позволяет задавать различные параметры типа
заголовков и статуса ответа. Перепишем наш декоратор таким образом чтобы он
умел принимать эти параметры:
def json_view_arg(**kwargs):
def wrap(func):
async def wrapped(request):
return json_response(await func(request), **kwargs)
return wrapped
return wrap
Теперь можно задать, например, кастомный заголовок ответа Server
:
@json_view_arg(headers={"Server": "Nginx"})
async def index(request):
return {"Hello": "World"}
И в заключение то же в виде класса-декоратора:
class JsonView(object):
def __init__(self, **kwargs):
self.kwargs = kwargs
def __call__(self, func):
async def wrapped(request):
return json_response(await func(request), **self.kwargs)
return wrapped
@JsonView(headers={"Server": "Nginx"})
async def index(request):
return {"Hello": "World"}
См.также
См.также
1 2 3 4 5 6 7 8 9 10 11 | def id_maker():
index = 0
while True:
yield index
index += 1
gen = id_maker()
print(next(gen))
print(next(gen))
print(next(gen)) |
Запуск:
$ python gen.py
0
1
2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class CSVFile(object):
def __init__(self, path, sep=','):
self.path = path
self.sep = sep
def __iter__(self):
with open(self.path) as f:
for l in f:
yield l.split(self.sep)
csv_generator = CSVFile('sample.csv')
for row in csv_generator:
print(row) |
1 2 3 | 1997,Ford,E350,"ac, abs, moon",3000.00
1999,Chevy,"Venture ""Extended Edition""","",4900.00
1996,Jeep,Grand Cherokee,"MUST SELL! air, moon roof, loaded",4799.00 |
$ python csv_gen.py
['1997', 'Ford', 'E350', '"ac', ' abs', ' moon"', '3000.00\n']
['1999', 'Chevy', '"Venture ""Extended Edition"""', '""', '4900.00\n']
['1996', 'Jeep', 'Grand Cherokee', '"MUST SELL! air', ' moon roof', ' loaded"', '4799.00\n']
1 2 3 4 5 6 7 8 9 10 11 | function* idMaker(){
var index = 0;
while(true)
yield index++;
}
var gen = idMaker();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2 |
Запуск:
$ iojs gen.js
0
1
2
Visual Studio Code отличный выбор для начинающего программиста, имеет необходимый минимум:
Также редактор адаптирован для Веб-разработки и вполне подойдет для серьезных проектов как основной инструмент редактирования кода.
Скачиваем дистрибутив для своей ОС https://code.visualstudio.com/download
Для Linux существуют два типа пакетов, самых популярных форматов, rpm и deb.
Установка в Ubuntu/Debian:
$ sudo dpkg -i <file>.deb
CentOS/Fedora:
$ sudo yum install <file>.rpm
Fedora > 22 версии:
$ sudo dnf install <file>.rpm
После установки можно запустить редактор следующей командой:
$ code
Пакетный менеджер Nix работает на любом Linux дистрибутиве, содержит богатую базу уже готовых пакетов, в том числе и vscode.
Установка пакетного менеджера:
$ curl https://nixos.org/nix/install | sh
Установка Visual Studio Code:
$ nix-env -i vscode
Редактор имеет возможность расширения функционала за счет плагинов и удобный интерфейс их установки, доступный по нажатию кнопки:
Из списка можно выбрать любой плагин и установить, после чего он применит свои настройки к редактору.
Расширения можно искать введя название или ключевые слова в строке поиска, например Python.
Существует огромное количество расширений для Go, C#, C/C++, Nix, Haskell, Python, JS, TypeScript и др.
После установки плагина Python нам становятся доступны многие функции:
Работает при наборе по нажатию Ctrl + Space.
Показывает ошибки в коде:
Работает если установлены Python пакеты Pylint, Pep8 или Flake8.
Совет
$ pip install -U --user pylint pep8 flake8
Встроенный в редактор отладчик позволяет отлаживать код визуально, устанавливать точки останова мышкой и просматривать переменные в отдельном окне. Это похоже на отладку в различных IDE, таких как QtCreator или Wingware.
Также избавляет программиста писать мучительные строки типа printf или import pdb;pdb.set_trace();.
Настройки хранятся в формате JSON и доступны из меню
File->Preferences->User Settings
.
Шрифт задается в настройках File->Preferences->User Settings
:
// Place your settings in this file to overwrite the default settings
{
// Controls the font size.
"editor.fontSize": 16
}
Более привычно дополнять код по клавише <Tab>. Для этого необходимо
открыть настройки пользователя File->Preferences->User Settings
и прописать
опцию editor.tabCompletion
:
// Place your settings in this file to overwrite the default settings
{
// Controls the font size.
"editor.fontSize": 16,
// Insert snippets when their prefix matches. Works best when 'quickSuggestions' aren't enabled.
"editor.tabCompletion": true
}
Цветовое оформление задается в настройках File->Preferences->Color Theme
.
Умеет подсвечивать изменения в файлах с предыдущего коммита, выполнять команды git и отслеживать состояние, например какая текущая ветка.
См.также
Visual Studio Code требует для отладки открывать не просто файл, а директорию. Это необходимо, чтобы в этом каталоге сохранить локальные настройки редактора. Такая директория будет считаться проектом для редактора.
Для примера, создадим директорию hello1 и откроем в редакторе File->Open
Folder...
.
Создадим в этой директории файл myapp.py:
Добавим в файл пример с сайта http://trypyramid.com
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
def hello_world(request):
return Response('Hello %(name)s!' % request.matchdict)
config = Configurator()
config.add_route('hello', '/hello/{name}')
config.add_view(hello_world, route_name='hello')
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever()
Для запуска приложения, заходим в режим отладки по нажатию на кнопку:
.
Пока у нас нет никаких настроек отладки/запуска проекта, но при первом запуске редактор предложит их выбрать из существующих шаблонов.
Шаблон Python создает настройки в файле launch.json в локальной директории, которые выглядят примерно так:
{
"version": "0.2.0",
"configurations": [
{
"name": "Python",
"type": "python",
"request": "launch",
"stopOnEntry": true,
"pythonPath": "${config.python.pythonPath}",
"program": "${file}",
"debugOptions": [
"WaitOnAbnormalExit",
"WaitOnNormalExit",
"RedirectOutput"
]
},
{
"name": "Python Console App",
"type": "python",
"request": "launch",
"stopOnEntry": true,
"pythonPath": "${config.python.pythonPath}",
"program": "${file}",
"externalConsole": true,
"debugOptions": [
"WaitOnAbnormalExit",
"WaitOnNormalExit"
]
},
{
"name": "Django",
"type": "python",
"request": "launch",
"stopOnEntry": true,
"pythonPath": "${config.python.pythonPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"runserver",
"--noreload"
],
"debugOptions": [
"WaitOnAbnormalExit",
"WaitOnNormalExit",
"RedirectOutput",
"DjangoDebugging"
]
},
{
"name": "Watson",
"type": "python",
"request": "launch",
"stopOnEntry": true,
"pythonPath": "${config.python.pythonPath}",
"program": "${workspaceRoot}/console.py",
"args": [
"dev",
"runserver",
"--noreload=True"
],
"debugOptions": [
"WaitOnAbnormalExit",
"WaitOnNormalExit",
"RedirectOutput"
]
},
{
"name": "Attach",
"type": "python",
"request": "attach",
"localRoot": "${workspaceRoot}",
"remoteRoot": "${workspaceRoot}",
"port": 3000,
"secret": "my_secret",
"host": "localhost"
}
]
}
Это универсальный шаблон, который добавляет несколько вариантов запуска
приложений. Нас будет интересовать первый вариант Python
, просто
запускающий python файл.
Запущенное приложение останавливается на первой строчке, что позволяет нам продолжать выполнение программы по шагам.
После выполнения второй строки, интерпретатор выдаст ошибку ImportError: No
module named pyramid.config
. Это происходит из-за того что в нашем Python
окружении не установлен модуль pyramid.
Решить эту проблему можно двумя способами:
Установить Pyramid в глобальное окружение.
$ pip install --user pyramid
Создать виртуальное окружение, установить в нем Pyramid и прописать его в настройках Visual Studio Code.
См.также
Как создать Виртуальное окружение
Создаем виртуальное окружение:
$ cd /path/to/hello1/
$ pyvenv hello1_env
$ source ./hello1_env/bin/activate
Устанавливаем Pyramid:
(hello1_env)$ pip install pyramid
Прописываем путь до виртуального окружения в настройках проекта Visual Studio Code (файл launch.json):
{
"name": "PythonVenv",
"type": "python",
"request": "launch",
"stopOnEntry": true,
"pythonPath": "${workspaceRoot}/hello1_env/bin/python",
"program": "${file}",
"debugOptions": [
"WaitOnAbnormalExit",
"WaitOnNormalExit",
"RedirectOutput"
]
}
После этого появится возможность запускать наш скрипт в локальном виртуальном окружении. Запущенная программа будет доступна по адресу http://localhost:8080/hello/foo. В консоле отладчика можно наблюдать ее вывод.
Поставим точку останова внутри функции hello_world
, в строке 6. Это
позволит нам остановить программу при запуске этой функции. После запуска,
программа будет нормально работать, пока мы не зайдем по адресу
http://localhost:8080/hello/foo, в этом случае запустится функция
hello_world
и выполнение программы прервется, до тех пор пока мы ее не
продолжим вручную.
Примерно так выглядит процесс разработки и отладки программ на Python. Осталось только инициализировать git репозиторий и выложить проект на https://github.com.
Инициализируем репозиторий:
Добавим файл .gitignore
:
Для этого нам потребуется скопировать содержимое
https://www.gitignore.io/api/visualstudiocode,python в файл .gitignore
и добавить туда директорию hello1_env
, чтобы она не участвовала в
процессе создания версий.
# Created by https://www.gitignore.io/api/visualstudiocode,python
hello1_env
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
...
Создаем первый коммит
Для создания коммита требуется ввести комментарий и нажать на кнопку в виде галочки.
Отправляем изменения на https://github.com
Прописываем путь до гитхаба в нашем проекте, при помощи команды Git
Easy:Add Orign
Отправляем изменения на GitHub, при помощи команды
Git Easy:Push Current Branch to Origin
При успешном выполнении команды, мы должны увидеть сообщение типа:
To github.com:uralbash/hello1.git
* [new branch] master -> master
Файлы будут доступны по адресу https://github.com/uralbash/hello1
Для того чтобы проверка синтаксиса заработала, необходимо создать файл
.vscode/settings.json
и переопределить в нем глобальные настройки для
нашего проекта:
{
"editor.fontSize": 18,
//Python
"python.pythonPath": "${workspaceRoot}/hello1_env/bin/python",
// Whether to lint Python files using pylint.
"python.linting.pylintEnabled": true,
// Whether to lint Python files using pep8
"python.linting.pep8Enabled": true,
// Whether to lint Python files using flake8
"python.linting.flake8Enabled": true
}
Фреймворк Pyramid имеет несколько стартовых шаблонов, которые нужны для того, чтобы не начинать писать код с нуля. Рассмотрим как создать шаблон с БД sqlite + SQLAlchemy и настроить его в Visual Studio Code.
Для начала создадим директорию hello2 и виртуальное окружение hello2_env:
$ mkdir hello2
$ cd hello2/
$ pyvenv hello2_env
$ source hello2_env/bin/activate
$ pip install pyramid
После установки Pyramid, в окружении появляется команда pcreate
. С ее
помощью создадим проект по шаблону:
$ pcreate -t alchemy .
$ ls
CHANGES.txt development.ini hello2 hello2_env MANIFEST.in production.ini pytest.ini README.txt setup.py
Устанавливаем его как Python пакет:
$ pip install -e .
$ pserve development.ini
Starting server in PID 17311.
Serving on http://localhost:6543
После запуска, становится доступен адрес http://localhost:6543
Но так-как БД еще не создана, отображается страница с подсказкой как ее инициализировать:
$ initialize_hello2_db development.ini
Теперь мы увидим стартовую страницу шаблона alchemy.
Проект на пирамиде запускается при помощи утилиты pserve
. Добавим
конфигурацию для Pyramid в файл настроек launch.json
, чтобы можно было
запускать/отлаживать приложение из редактора:
{
"version": "0.2.0",
"configurations": [{
"name": "Pyramid",
"type": "python",
"request": "launch",
"stopOnEntry": true,
"pythonPath": "${workspaceRoot}/hello2_env/bin/python",
"program": "${workspaceRoot}/hello2_env/bin/pserve",
"args": ["${workspaceRoot}/development.ini"],
"debugOptions": [
"WaitOnNormalExit",
"RedirectOutput"
]
}]
}
Попробуем запустить:
Поставим точку останова в функции my_view
в файле
hello2/views/default.py
.
После обновления страницы http://localhost:6543 в браузере, программа остановит свое выполнение в этой точке, а браузер будет ждать пока мы не закончим отладку и не продолжим выполнение вручную.
Скачайте инсталятор https://notepad-plus-plus.org/download/ и установите редактор.
При наборе текста вы столкнетесь с проблемой, что нажатие на клавишу <Tab> вставляет символ табуляции заместо 4 пробелов, как это принято при написании Python программ.
По умолчанию в Notepad++ клавиша <Tab> вставляет символ табуляции.
Поменять это поведение можно в пункте меню Опции->Настройки->Настройки Табуляции
.
В результате нажатие клавиши <Tab> будет подменяться четырьмя пробелами.