Делаем mash-up приложение на трэндах

Разделы

воскресенье, 29 мая 2011 г.

OAuth для твиттера (продолжение)

Буду краток. Вчера времени не хватило, не успел выход сделать:
@route('/exit')
@route('/exit/')
def exit_view():
    del_cookie()
    redirect("/")
И кстати заодно файлик для работы с куками:
Листинг cookie.py:
@route('/login')
#!/usr/bin/env python
# encoding: utf-8

from bottle import route, run, view, request, redirect, response, post, abort, debug, error
import logging
import hmac
import uuid
import base64
from google.appengine.ext import db
from datetime import datetime
from datetime import timedelta

from models import Users
from oauth import TwitterClient


def generate_rnd_hash(uid):
    h = hmac.new(str(uid))
    h.update(uuid.uuid4().bytes)
    return base64.b64encode(h.digest() + uuid.uuid4().bytes, ['+', '-']).replace('=', '').replace('+', '')

    
def set_cookie(uid, user_info = {}):
    last_error = "ok"
    hash = generate_rnd_hash(uid)
    dt = datetime.utcnow() + timedelta(seconds = 60*60*24*7)
    exp_gmt = dt.strftime("%a, %d-%b-%Y %H:%M:%S GMT")
    user = Users.get_by_key_name(str(uid))
    if not user:
        user = Users(key_name=str(uid))
    if user_info:
        user.uid = user_info["uid"]
        user.username = user_info["username"]
        user.name = user_info["name"]
        user.image = user_info["image"]
        user.token = user_info["token"]
        user.secret = user_info["secret"]
    user.hash = hash
    user.hashex = dt
    user.put()
    try:
        response.set_cookie('sid', hash, secret = None, \
                                            **{'expires' : exp_gmt, 'path' : '/'})
    except Exception, msg:
        logging.error(msg)

def del_cookie():
    uid = get_cookie()
    user = Users.get_by_key_name(str(uid)) if uid else None
    if user:
        user.delete()
    try:
        response.set_cookie('sid', "", secret = None, \
                                            **{'path' : '/'})
    except Exception, msg:
        logging.error(msg)
        
    
def get_cookie():
    hash = request.get_cookie("sid")
    logging.debug(hash)
    key = Users.all(keys_only=True).filter("hash = ", hash).fetch(1)
    key = long(key[0].name()) if key else 0
    return key

OAuth для твиттера

Этот интерфейс авторизации придумали для того что бы свой пароль пользователь не оставлял на третьих сайтах, а авторизовался непосредственно там он создал аккаунт - у провайдера. В данном примере речь пойдёт о Twitter.
Итак мы зарегистрировали приложение в предыдущем посте. И получили два пин-кода: ключ, и секрет. С помощью их мы можем теперь получить, если пользователь разрешит, пользовательский токен, и секрет - пользовательские пин-коды. Ключей везде по двое, что бы обеспечьте защиту канала связи.
По большому счёту если нет желания со всем этим заморачиваться - то и не надо, для это есть уже готовые библиотеки, сделанные прямо для авторизации в твиттере через GAE-приложение, и даже не одна. Я взял AppEngine-OAuth-Library by Mike Knapp, которая состоит из одного файла и маленьким примерчиком.
Собственно весь процесс авторизации выглядит следующим образом:
@route('/login')
@route('/login/')
def login_view():
    callback_url = "http://%s/verify" % request.environ["HTTP_HOST"]
    client = TwitterClient(TWITTER_KEY, TWITTER_SECRET, callback_url)
    redirect(client.get_authorization_url())
    
@route('/verify')
@route('/verify/')
def verify_view():
    callback_url = "http://%s/verify" % request.environ["HTTP_HOST"]
    auth_token = request.GET.get('oauth_token', "")
    auth_verifier = request.GET.get('oauth_verifier', "")
    client = TwitterClient(TWITTER_KEY, TWITTER_SECRET, callback_url)
    try:
        user_info = client.get_user_info(auth_token, auth_verifier=auth_verifier)
    except Exception, msg:
        redirect("/")
    set_cookie(user_info["uid"], user_info)
    redirect("/")
Собственно запрос на авторизацию и её проверка. В случаее успеха имеем словарик с
service, token, secret, id, username, name, picture. Теперь в твиттер можно отправлять авторизованные запросы:
timeline_url = "http://twitter.com/statuses/user_timeline.xml"
    result = client.make_request(url=timeline_url, token=user_token, 
          secret=user_secret)

суббота, 28 мая 2011 г.

Твиттер

Как я уже сообщил что одну неделю приложение было не работоспособно из за бага. За это время пришло понимание каким наконец приложение должно быть. И будет оно таким:
  • твиттер-ориентированным, или дословно твиттер-приложением.
  • больше ориентированно на сами твиты, чем на фолловеров.
  • ну и новости, тредны тоже должны быть.
Как это удастся связать - это для меня тоже вопрос, но пока осваиваем твиттер. Причём с самого начала (*sick*). Для этого надо завести аккаунт. После подтверждения по емэйлу можно зарегистрировать свое приложение. Приблизительно форма регистрации приложения выглядит так:
  • Application Name - уникальное название приложения
  • Description - описание.
  • Application Website - домашняя страница приложения.
  • Organization - можно указать себя.
  • Application Type - либо вэб, либо десктопное.
  • Callback URL - если десктопное, то надо указать куда будет перенаправлять твиттер авторизованных, через OAuth пользователей (подробней об этом уже в следующем посте).
  • Application Icon - иконка.

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

Индексы

Как я уже писал с индексами была проблема, из за бага самого сервиса. Эта проблема решалась целую неделю. Сначала в issue трэкере были единичные просьбы пофиксить этот баг с индексами. И только после того как это приняло лавинообразный характер, баг в течении 2 часов был исправлен.
Естественно, за эту неделю я время не терял но ою этом уже в следующем посте.

суббота, 21 мая 2011 г.

Певая страница

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

Но что б статья не была совсем уж голой, я скажу два слова о шаблонах. После того как вчера была проделана огромная работа по адаптации архитектуры к App Engine. Под вечер провозился с глупой ошибкой в шаблоне, в связи с этим перелопатил все доступные шаблоны. Я пользуюсь стандартным из Bottle и на нём пока останусь. Интересным из всех показался только Mako. Не смотря на обилие файлов в библиотеке по скорости он не уступает стандартному (но всё равно это гораздо медленней чем чистый питон), а возможности у него сопоставимы с самим языком. т.е. он фактически обеспечивает прозрачность HTML-кода для питона. И прям в HTML объявлять функции, обрабатывать переменные. Вообщем, при разработке большого проекта вещь вполне достойная.

Видео

Что нужно для mash-up приложения? Конечно видео. Ведь как же без него, с релевантным подбором могут быть проблемы, но это уже время покажет. Вообщем примерчик как это сделать располагается здесь. Одно не понятно зачем ключ? Немного полазив понял что он используется при защищенном соединении и с пользовательским данными. Нам это пока не надо, но ключик я получил.

Чуть подробней о том же уже сюда. Что ж делаем HTTP-запрос, на получить одно какое-нибудь видео, получаем JSON размером в "Войну и мир", если учесть то что от видео мы хотели получить только его идентификатор. По поводу хедера ещё были сомнения: слать его или нет. Решил так раз ключ получил буду слать. Получилось вот что.

Листинг videos.py

#!/usr/bin/env python
# encoding: utf-8

from google.appengine.api import urlfetch
from urllib import quote
from settings import YOUTUBE_KEY
try:
  import json
except ImportError:
  import simplejson as json

def get_videos(key):
  videos = []
  video_url = 'http://gdata.youtube.com/feeds/api/videos?v=2&alt=json&q=%s&orderby=relevance&max-results=10&format=5&time=today' % \
            quote(key)
  headers = {"GData-Version": '2', "X-GData-Key" : "key=%s" % YOUTUBE_KEY}
  res = urlfetch.fetch(video_url, headers = headers)
  if res.status_code == 200:
    jj = json.loads(res.content)
    if jj.has_key(u"feed") and jj[u"feed"].has_key(u"entry"):
      for item in jj["feed"]["entry"]:
        videos.append(item[u"media$group"][u"yt$videoid"][u"$t"])
  return videos

Обновление

Только сегодня обнаружил что SDK 1.1.9 - безнадёжно устарел т.к. на данный момент можно найти уже 1.5.0. Причём с главной страницы http://www.googleappengine.ru/ если пойти по направлению "скачать" попадается 1.1.9. Кстати это от того что гугл большой это не делает ему оправдания в дезорганизации документации. Реально доки по Google App Engine на двух доменах разбросаны: это который уже упомянут и http://code.google.com/intl/ru/appengine/. Причём их смело можно назвать взаимодополняющими, по крайне мере для новичка.

По поводу квот, квот для развития приложения более чем достаточно. Только мне не понятно одно: сделать 20 запросов на другой веб сайт - это в разы меньше процессорного времени чем сохранить эти же 20 элементов в хранилище. Я кончено понимаю что оно очень большое, но только что на магнитной ленте это там всё. С чтением дела обстоят лучше, но не на много. Вообщем хранилище требует основных ресурсов процессорного времени и на данный момент приложение укладывается в 2% от дневной квоты. До кэшированяи ещё не добрался, но об это пока даже не думаю.

пятница, 20 мая 2011 г.

Классы хранилища и новая архитектура приложения

Наконец-то пришло озарение и я осилил доку по хранилищу. Вообщем этот пост формально ничего нового не будет содержать а здесь будет лишь только работа над ошибками. Первое что надо учесть, что в классе Model до чёртиков зарезервированных слов.
Так же зарезервировано подчёркивание где-только можно, надо быть с ним аккуратней. Итак как как в блоге не стоит цель сделать ревью сервисам гугл, а лишь практическое использование, то описание классов будет кратким:
Model - аналог таблицы
Expando - динамический класс, в котором хранятся объекты с различными свойствами.
PolyModel - это класс Model, с полиморфизмом.
ReferenceProperty - свойство которое используется как ссылка на другой объект и хранит его ключ. Непосредственно у ключей есть названия, я так понял это и есть способ обеспечить уникальность объектам. Как вы заметили на всё это ушёл без малого один день, но за этот день поменялась архитектура приложения, и появились так же некоторые идеи. Что мы имеем сегодня.
[trendec@localhost trendstat]$ ls -al
total 284
drwxrwxr-x 4 trendec trendec   4096 May 20 21:42 .
drwxr-xr-x 9 trendec trendec   4096 May 17 23:21 ..
-rw-r----- 1 trendec trendec    380 May 20 12:41 app.yaml
-rwxr-xr-x 1 trendec trendec  98154 May 14 00:26 bottle.py
-rw-r--r-- 1 trendec trendec 114796 May 18 19:47 bottle.pyc
-rw-r--r-- 1 trendec trendec     96 May 19 15:25 cron.yaml
-rwxrwxrwx 1 trendec trendec    471 May 20 21:30 index.yaml
-rwxr-x--- 1 trendec trendec    633 May 20 21:27 main.py
-rw-r--r-- 1 trendec trendec    515 May 20 19:38 models.py
-rw-r--r-- 1 trendec trendec   1051 May 20 19:38 models.pyc
-rw-r--r-- 1 trendec trendec   1796 May 20 19:49 news.py
-rw-r--r-- 1 trendec trendec   2244 May 20 19:49 news.pyc
-rw-r--r-- 1 trendec trendec    141 May 19 23:40 settings.py
-rw-r--r-- 1 trendec trendec    247 May 20 00:36 settings.pyc
drwxrwxr-x 2 trendec trendec   4096 May 18 20:36 static
-rw-r--r-- 1 trendec trendec    759 May 20 19:44 tasks.py
-rw-r--r-- 1 trendec trendec   1297 May 20 19:48 tasks.pyc
drwxrwxr-x 2 trendec trendec   4096 May 20 17:57 views
Уже явно прослеживается разбиение задач на подзадачи по списку файлов. А так же в детелях.


Листинг models.py:

#!/usr/bin/env python
# encoding: utf-8

from google.appengine.ext import db

class News(db.Model):
  title = db.StringProperty()
  url = db.LinkProperty()
  domain = db.LinkProperty()
  publisher = db.StringProperty()
  image = db.StringProperty()
  content = db.StringProperty()
  time = db.DateTimeProperty()

class Trends(db.Model):
  trend = db.StringProperty()
  time = db.DateTimeProperty(auto_now=True)
  video = db.StringListProperty()
  news = db.ListProperty(db.Key)
  ct = db.TextProperty()
  cf = db.TextProperty()
Таким образом все модели вынесли в отдельный файл


Листинг news.py:

#!/usr/bin/env python
# encoding: utf-8

from google.appengine.api import urlfetch
from urllib import quote
from settings import GOOLE_KEY
from datetime import datetime
from datetime import timedelta
import re
try:
  import json
except ImportError:
  import simplejson as json

def get_news(key):
  news = []
  rt = re.compile("<.*?>", re.I)
  rd = re.compile("^http://[^/]+", re.I)
  rn = re.compile("(.*?)($| [-+][012]\d\d\d$)", re.I)
  news_url = 'http://ajax.googleapis.com/ajax/services/search/news?v=1.0&q=%s&key=%s' % \
            (quote(key), GOOLE_KEY)
  res = urlfetch.fetch(news_url)
  if res.status_code == 200:
    jj = json.loads(res.content)
    if jj.has_key(u"responseStatus") and jj[u"responseStatus"] == 200:
      if jj.has_key(u"responseData") and jj[u"responseData"].has_key(u"results"):
        for item in jj[u"responseData"][u"results"]:
          d = {}
          d["title"] = rt.sub("", item[u"title"])
          d["url"] = item[u"unescapedUrl"]
          d["domain"] = rd.search(item[u"unescapedUrl"]).group(0) + "/"
          if item.has_key(u"content"):
            d["content"] = rt.sub("", item[u"content"])
            d["content"] = d["content"] if len(d["content"]) < 150 else "%s..." % d["content"][:147]
          else:
            d["content"] = ""
          d["image"] = item[u"image"][u"tbUrl"] if item.has_key(u"image") and item[u"image"].has_key(u"tbUrl") else ""
          d["publisher"] = item[u"publisher"] if item.has_key("publisher") else ""
          if item.has_key("publishedDate"):
            d["time"] = item[u"publishedDate"]
            rr = rn.match(d["time"])
            d["time"] = datetime.strptime(rr.group(1), "%a, %d %b %Y %H:%M:%S")
            tz = rr.group(2)
            if tz:
              d["time"] += timedelta(seconds = (-1 if tz[0] == '-' else 1)* int(tz[1:3])*60 + int(tz[3:]))
          news.append(d)
  return news
Новости потерпели некоторое изменения, в них появилась дополнительная информация, включая дату публикации (пока даже не знаю зачем).

Листинг tasks.py:

#!/usr/bin/env python
# encoding: utf-8

from google.appengine.ext import db
from google.appengine.api import urlfetch
from models import News
from models import Trends
from news import get_news
import re

def trends_update():
  trends = []
  trend_url = "http://www.google.com/trends/hottrends/atom/hourly"
  rl = re.compile("
  • .*?
  • ", re.I|re.S) rt = re.compile("<.*?>", re.I) res = urlfetch.fetch(trend_url) if res.status_code != 200: abort(404) content = res.content trends = rl.findall(content) trends = [rt.sub("", t).strip() for t in trends] for t in trends: item = Trends() item.trend = t for n in get_news(t): item.news.append(News.get_or_insert(n["url"], **n).key()) item.video = [] item.ct = "" item.cf = "" item.put()
    Этот скрипт вызывается кроном. И основной файл совсем похудел. Кстати, вынужден вас огорчить, или может обрадовать. Целиком файлы всего приложения публикуются в последний раз, это что бы сформировалось понимания всего приложения. В дальнейшем, что б не громоздить куски кода, это будут только новые методы или классы и может быть редко полностью файлы.

    Листинг main.py:

    #!/usr/bin/env python
    # encoding: utf-8
    
    from bottle import route, run, view, request, redirect, response, post, abort, debug, error
    from google.appengine.ext import db
    import tasks
    from models import News
    from models import Trends
    
    debug(True)
    
    
    @route('/')
    @route('')
    @view('home')
    def home_view():
      trends = []
      for t in Trends.all().order("-time").fetch(20):
        t.news_direct = News.get(t.news)
        trends.append(t)
      return {"trends" : trends }
    
    @route('/tasks/trends_update')
    @route('/tasks/trends_update/')
    def trends_update():
      tasks.trends_update()
    
    def main():
      run(server='gae')
    
    if __name__ == '__main__':
      main()
    Вот, это уже что-то. Напомню, это работающее приложение которое можно увидеть по адресу http://trendstat.appspot.com/.

    Новости

    С индексами вроде разобрались. Только у меня на данный момент висит два индекса в статусе "Error", которые мне не удаётся удалить командой:
    ./appcfg.py vacuum_indexes trendstat/
    
    Неприятно, но никаких дискомфортов в работе это не создаёт. Поэтому кладём болт и двигаемся дальше. В предыдущей статье мы немного отпарсили гугл. В этот раз мы поступим иначе. Гугл сам предлагает совершенно бесплатно свои данные, правда в ограниченных количествах, около 1000 запросов в сутки через API. Для этого просто надо получить ключик для своего приложения. Я создам файл 'settings.py' куда я записал ключиг, и в будущем если на него и будут появляться ссылки в листингах то там содержится секретная информация типа ключей паролей и т.д., т.е. одни константы. Вообщем ключиг есть, вспоминаем там как и чего с этим API делать. Вообщем никаких сюрпризов, но вот проблема которая меня мучала сейчас всплыла: 'urllib' там есть вообще? Оказывается что есть, только через него нельзя к HTTP-библиотеке обращаться, а так используете в своей удовольствие. Писал за полночь, вот в итоге что получилось.
    Листинг news.py:
    #!/usr/bin/env python
    # encoding: utf-8
    
    from google.appengine.api import urlfetch
    from urllib import quote
    from settings import GOOLE_KEY
    import re
    try:
      import json
    except ImportError:
      import simplejson as json
    
    def get_news(key):
      def create_news(title, url, content):
        item = '''%s

    %s

    ''' % \ (url, title, content if len(content) < 150 else "%s..." % content[:147]) return item.replace("\n", "") #control news = [] rt = re.compile("<.*?>", re.I) news_url = 'http://ajax.googleapis.com/ajax/services/search/news?v=1.0&q=%s&key=%s' % \ (quote(key), GOOLE_KEY) res = urlfetch.fetch(news_url) if res.status_code != 200: return news jj = json.loads(res.content) if jj.has_key(u"responseStatus") and jj[u"responseStatus"] == 200: try: news = [create_news(rt.sub("", url[u"title"]), url[u"unescapedUrl"], rt.sub("", url[u"content"])) \ for url in jj[u"responseData"][u"results"]] except KeyError: pass return news

    четверг, 19 мая 2011 г.

    Индексы

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

    Как выяснилось в последствии индексы для запросов с одной только сортировкой писать не надо, хотя в доках на официальном сайте пока что сказано иначе.
    [trendec@localhost google_appengine]$ ./appcfg.py update_indexes trendstat/  
    Uploading index definitions.
    Error 400: --- begin server output ---
    Creating a composite index failed: This index:
    entity_type: "Trends"
    ancestor: false
    Property {
      name: "time"
      direction: 2
    }
    
    is not necessary, since single-property indices are built in. Please remove it from your index file and upgrade to the latest version of the SDK, if you haven't already.
    --- end server output ---

    Хранилище

    После того как крон настроен можно использовать его для пополнения БД именуемой в терминах гугл - хранилище. Немного покопавшись в доках, поймал себя на на том что крутится мысль что это жесть. На самом деле всё оказалось не так. Первое что пришлось сделать изменить структуры модели (таблицы в терминах хранилища), сделать её ещё более линейной и тупой, ниже вы можете в этом убедится сами. Это нормально, т.к. к этой модели будет прикручен кэш, отдельная модель которая будет содержать нужные выборки (но это потом). Ввиду того что уже крон настроен, осталось прописать действия в скрипте. По крону добавляется запись, а по запросу выдаётся.
    Листинг 'main.py':
    #!/usr/bin/env python
    # encoding: utf-8
    
    from bottle import route, run, view, request, redirect, response, post, abort, debug, error
    from google.appengine.api import urlfetch
    from google.appengine.ext import db
    import re
    
    debug(True)
    
    class Trends(db.Model):
      trend = db.StringProperty()
      time = db.DateTimeProperty(auto_now=True)
      video = db.StringProperty()
      news = db.TextProperty()
      ct = db.TextProperty()
      cf = db.TextProperty()
    
    
    @route('/')
    @route('')
    @view('home')
    def home_view():
      trends = db.GqlQuery("SELECT * FROM Trends ORDER BY time DESC LIMIT 20")
      return {"trends" : trends }
    
    @route('/tasks/trends_update')
    @route('/tasks/trends_update/')
    def trends_update():
      trends = []
      trend_url = "http://www.google.com/trends/hottrends/atom/hourly"
      rl = re.compile("
  • .*?
  • ", re.I|re.S) rt = re.compile("<.*?>", re.I) res = urlfetch.fetch(trend_url) if res.status_code != 200: abort(404) content = res.content trends = rl.findall(content) trends = [rt.sub("", t).strip() for t in trends] for t in trends: __t = Trends() __t.trend = t __t.video = "" __t.news = "" __t.ct = "" __t.cf = "" __t.put() def main(): run(server='gae') if __name__ == '__main__': main()

    День третий

    Тренды гугл обновляет каждый час, поэтому нечего туда каждый раз лазить и парсить, а сделать это надо через крон и сохранить в "хранилище". Для этого сначала надо разобраться как там устроен крон.
    Крон там работает по схеме HTTP запросов методом GET по указанному вами адресу. Для этого создаётся ещё один yaml-файлик:
    Листинг cron.yaml:
    cron:
    - description: hourly trends update
      url: /tasks/trends_update
      schedule: every 1 hours
    
    Теперь надо добавить обработчик этого адреса в скрипт 'main.py'. А что б его не мог запускать кто попало, перед перенаправленеим на основной скрипт в 'app.yaml' указываем что обращаться по адресу можно только с правами админа.

    Листинг 'app.yaml':
    application: trendstat
    version: 1
    runtime: python
    api_version: 1
    
    handlers:
    - url: /static
      static_dir: static
    
    - url: /(.*\.(gif|png|jpg))
      static_files: static/\1
      upload: static/(.*\.(gif|png|jpg))
    
    - url: /tasks/trends_update
      script: main.py
      login: admin
    
    - url: /robots.txt
      static_files: static/robots.txt
      upload: static/robots.txt
    
    - url: /.*
      script: main.py

    среда, 18 мая 2011 г.

    Парсим тренды

    Думаю настало время с чего-то начать. Начнём пожалуй с самого просто - тупо спарсим тренды, которые удобно располагаются прямо в атоме. Гугл по этому поводу надеюсь не растроится, что его немного отпарсили, всё таки на то он и атом. Насчёт удобно это я скорей загнул, т.к. с атомом не работал ни разу. И в данный момент пытаюсь с помощью поиска разрешить философский вопрос: как в GAE принято, писать заново, или попробовать найти библиотеку.

    Вообщем 10 минут поиска меня убедили что здесь принято делать большую часть работы самому а не ждать помощи от библиотек. Собственно как оказалось это 2 строчки кода, но об этом ниже.

    Итак, как выглядит приложение в Google App Engine? Это как минимум 3 файла: 'app.yaml', 'index.yaml' и приложение 'main.py'. Гугл на своё усмотрение предлагает использовать либо свою разработку в плане фреймворка, либо любой который работает с WSGI - что я и сделал. Сразу же взял Bottle. Таким образом приложение представляет из себя следующий список:

    [trendec@localhost trendstat]$ ls -al
    total 248
    drwxrwxr-x 4 trendec trendec   4096 May 18 20:11 .
    drwxr-xr-x 9 trendec trendec   4096 May 17 23:21 ..
    -rw-r----- 1 trendec trendec    142 May 18 19:47 app.yaml
    -rwxr-xr-x 1 trendec trendec  98154 May 14 00:26 bottle.py
    -rw-r--r-- 1 trendec trendec 114796 May 18 19:47 bottle.pyc
    -rw-r----- 1 trendec trendec    471 May 18 20:03 index.yaml
    -rwxr-x--- 1 trendec trendec    818 May 18 20:03 main.py
    drwxrwxr-x 2 trendec trendec   4096 May 18 19:16 static
    drwxrwxr-x 2 trendec trendec   4096 May 18 19:16 views
    
    В директории 'static' содержится файл стилей 'simple.css' и 'robots.txt' а в директории 'view' шаблон главной страницы.
    Ниже приведу листинг файлов.
    Файл 'main.py':
    #!/usr/bin/env python
    # encoding: utf-8
    
    from bottle import route, run, view, request, redirect, response, post, abort, debug, error
    from google.appengine.api import urlfetch
    import re
    
    debug(True)
    
    @route('/')
    @route('')
    @view('home')
    def home_view():
      trends = []
      trend_url = "http://www.google.com/trends/hottrends/atom/hourly"
      rl = re.compile("
  • .*?
  • ", re.I|re.S) rt = re.compile("<.*?>", re.I) res = urlfetch.fetch(trend_url) if res.status_code == 200: content = res.content trends = rl.findall(content) trends = [rt.sub("", t).strip() for t in trends] return {"trends" : trends } def main(): run(server='gae') if __name__ == '__main__': main()

    Файл 'app.yaml':
    application: trendstat
    version: 1
    runtime: python
    api_version: 1
    
    handlers:
    - url: /static
      static_dir: static
    
    - url: /(.*\.(gif|png|jpg))
      static_files: static/\1
      upload: static/(.*\.(gif|png|jpg))
    
    - url: /robots.txt
      static_files: static/robots.txt
      upload: static/robots.txt
    
    - url: /.*
      script: main.py

    Архитектура проекта

    Я пошутил, речь сейчас пойдёт о проекте который состоит из одной странице. Поэтому правильней будет назвать "Архитектура страницы". Проект будет разиватся по мере "развития". А сейчас речь о насущем.

    Вообщем, все страницы будут одинаковые и выглядеть:
    • Тайтл, шапка.
    • Заголовок, собственно текущий тренд.
    • Основная колонка:
      • Какая-нибудь характеристика тренда.
      • Видео
      • Новости, тупо ссылки.
      • Фичи всякие (если будут)
    • Вторая колонка:
      • Список текущих трендов
      • Ещё один список трендов по какой-нибудь характеристике.
      • И ещё один какой-нибудь маленький бесполезный список для того что б добавить нолик к количеству доступных страниц.
    Вот, теперь вопрос к этой "какой-нибудь" характеристики тренда, с учётом того что базы данных в песочнице не реляционные. Сразу вспоминается что там есть крон и поэтому один столбец в базе будет содержать исходные данные, а во втором они будут обрабатываться каждый час. Вообщем, это и будет какая нибудь характеристика - время нахождения в тренде.

    Итак раз уж коснулись про БД, то мне видится это одной таблицей, в которой находится:
    • Тренед.
    • Флаг текущих трендов.
    • Время последнего апдейта.
    • Позиция во время последнего апдейта.
    • Другие тренды во время последнего апдейта.
    • Столбец предварительных данных "некой характеристики"
    • Столбец обработанных данных "некой характеристики"
    • Видео
    • Новости
    • Фичи

    Это первой взгляд на БД, уверен что во время написания придёт другой, т.к. что-то Datastore совсем у них ни на что не похоже. Сообщу об этом позже прямо здесь.

    Сообщаю. Всё стало ещё проще и линейней.

    вторник, 17 мая 2011 г.

    Hello World!

    Что ж, путь в 10 000 ли начинается с первого шага. Дак сделаем же это! Я считаю необходимым запустить это приложение на локальной машине, коль уж есть такая возможность. Для этого качаем среду разработки (SDK). Там есть из чего выбрать! Мой выбор пал на Python под Linux. Т.е. сразу пишем в консоле:
    wget http://googleappengine.googlecode.com/files/google_appengine_1.1.9.zip
    unzip google_appengine_1.1.9.zip
    
    Собственно это и была инсталляция, как сказано в README. Затем создал папку 'trendstat' прямо в ~/google_appengine/. И только хотел написать первое приложение как с удивлением обнаружил что оно уже написано и располагаться в папке 'new_project_template'. Что ж, скопировал всё к себе и попробовал запустить как там сказано:
    ./dev_appserver.py trendstat/
    

    Вышел фэйл. Нет, запустить-то оно запустилось, и всё должно быть нормально. Но дело в том что Linux работает через CoLinux, и что бы из под виндов посмотреть приложение, как это я обычно делаю, я обращаюсь к нему по IP, а локалхост в виндах совсем другое. Вообщем это всё не важно, т.к. что бы запустить на нужном айпи-адресе, пишем:
    ./dev_appserver.py -a 192.168.1.10 trendstat/
    
    Теперь в и виндах доступно по адресу http://192.168.1.10:8080/
    Следующим шагом надо разместить "сайт" на хостинге. Идём в свой кабинет и создаём приложение с таким же именем. Сразу редактируем 'app.yml', где в первой строчке указываем настоящее имя приложения, которое указано в кабинете и которое ещё совпадает с именем директории где всё это находится на локальной машине. Например 'trendstat', т.к 'trendec' уже было занято. Таким образом приложение будет располагаться по адресу http://trendstat.appspot.com/. Перед редактированием не забываем:
    chmod u+w *
    
    для всех файлов в нужной директории. После того как указали имя приложения можно попробовать загрузить его на хостинг
    ./appcfg.py update trendstat/
    
    После того как этот скрипт спросил логин, пароль на аккаунт гугл, сразу же загрузил приложение на хостинг и теперь весь миру может увидеть послание! На самом деле, очень, очень круто. Мне всё понравилось. На сегодня всё.
    P.S. Это версия очень устарела, из админки доступна другая версия 1.5.0!

    Хостинг

    Не долго думая, зная положительные отзывы, я выбрал Google App Engine. Для тех кто вообще не знает что такое, это это сервис в основе которого лежат облачных вычисления. А если без умных слов (потому что я и сам честно не совсем в курсе что это) - это хостинг в котором ты платишь ровно за то что используешь, с квотированием. Естественно есть и бесплатные квоты, для маленьких сайтегов и стартапов. Что касается цен для тех кто вырос, жалоб что дорого - я так и не нашёл.

    Но! В чём же подвох, почему же другие хостинг-провайдеры сразу же не ретировали перед этим чудом. Ну во-первых у них собственная среда разработки в которой поддерживаются два языка: Python, Java. Те о том что бы запустить пхпэшку даже думать не надо. Но и эта не основная проблема. Сама среда накладывает существенные ограничение на используемые в этих языках стандартные библиотеки, которые в большей степени заменены на собственные. А то что не заменено - то не положено. Объясняют это тем что приложение запускается в "песочнице", где действуют повышенные степени безопасности бла-бла-бла. Но при этом везде в описании не забывается упомянуть что ресурсов у них "ебать конём", и они почти все ваши.

    Вообщем, всё меня эту устраивает и даже нравится. Но как уже можно было догадаться я понятия не имею как делать приложения в этой "песочнице".

    Идея

    Любой, даже самый маленький проект начинается с идеи. И вот он она у меня возникла! Прямо сегодня, поэтому пишу по горячим следам. Основная цель:
    • Сделать интересный сайт, в котором была бы собрана полезная информация.
    • Не потратить на его развитие ни копейки. (Я не буду здесь объяснятся почему так, предположим я очень жадный). А это значит, домен - бесплатный, хостинг - бесплатный, разработчики - энтузиасты (ваш покорный слуга).
    Задача не из лёгких. Во-первых, так как я писать ничего не умею, то надо будет эту полезную информацию для сайта где-то искать. Ну так что б она была действительно полезная её надо как уже написано выше найти, собрать, обработать, организовать и преподнести. И желательно так как этого бы хотел посетитель.





    Постоянные читатели

    Copyright © trendec. Технологии Blogger.