ラベル Python の投稿を表示しています。 すべての投稿を表示
ラベル Python の投稿を表示しています。 すべての投稿を表示

2017年7月23日日曜日

久しぶりにGoogle Cloudを触ってはまる、の巻

プログラマの皆さん、こんにちは。

自分が趣味で開発しているスマフォアプリの宣伝ブログを立ち上げようとしているのですが、記事を書くのが面倒なのでデータ収集→HTML編集→Blogger投稿を自動化しようと考えました。

Googleのサービスを触らなくなって久しいのですが

  • ID/Passwordによる認証はもはやサポートされてない(Oauthを使う必要あり)
  • Oauth用アカウントには二種類あって
    • Webブラウザやスマフォアプリに人間がID/Passwordを入れてOauth認証
    • 鍵/秘密鍵をJWTで送り付けて認証する(サービスアカウント)
というところまでは割とすんなり理解しました。

で、今回はBlogger投稿の完全自動化を目指しているのでサービスアカウント一択となります。以下のような簡単なコードを書いてみました。

from oauth2client.service_account import ServiceAccountCredentials
from httplib2 import Http
from googleapiclient.discovery import build


def get_blogger_service():
    scopes = ['https://www.googleapis.com/auth/blogger']
    credentials = ServiceAccountCredentials.from_json_keyfile_name(
        'credentials.json', scopes=scopes
    )
    http_auth = credentials.authorize(Http())
    return build('blogger', 'v3', http=http_auth)

def create_public_post(blogger_service, blog_id, content):
    request = blogger_service.posts().insert(
        blogId=blog_id,
        body=content
    )
    response = request.execute()
    return response

if __name__ == '__main__':
    service = get_blogger_service()
    content = {
        "kind": "blogger#post",
        "id": '俺様のblog id',
        "title": "posted via python",
        "content": "
hello world test
" } print create_public_post(service, '俺様のblog id', content)


上記コードを実行すると例外が発生し



googleapiclient.errors.HttpError: httperror 403="" access="" alt="json" blogger="" blogs="" but="" don="" e="" have="" https:="" permission="" posts="" re="" requesting="" resource.="" returned="" sorry="" t="" this="" to="" v3="" when="" www.googleapis.com="" you=""

というエラーが返っていることがわかります。最初はサービスアカウントに持たせた権限が足らないのかなと思いましたが、Owner権限(ほぼなんでもできる)に設定しても症状は変わりません。

悩むこと数日。

こんなやり取りを見つけました。

  • Paulという人がBlogger API v3で(自分と同様に)自動化された投稿を試みるも、403で跳ねられる。何がおかしいのかを質問。
  • BrettというおそらくはGoogleの中の人が「Service Accountからの投稿は許可してないよ」と一行回答。
  • Peterが「なんでじゃい?」と食い下がる。
  • Brettの回答「Blogger投稿には利用規約への同意が必要。自動化されたサービスアカウントには同意ができない。だからダメ」
  • Johnという別人が議論に参戦。「おかしいだろそれ。Blogger API v3のドキュメントではService Accountについて言及してるし、Google ConsoleからBlogger APIを選ぶと、紐づけアカウントでService Account選べるようになってるぞ」と反論。
  • Brettは頑なに「利用規約に同意しないとBloggerへの投稿はできない」というだけ。

ドキュメントが誤解を招くものなのも問題だし、利用規約への同意がないと投稿ができないっていうのもYes/Noの選択だけなんだからプログラムにやらせてもいいと思うんだけど。

まあGoogleもでかい会社だし、こういうお役人みたいな対応する社員がいてもおかしくはないかな、と。

ということでブラウザ経由で認証する仕組みに実装を変えます。で、Headless Browserをスクリプトで制御すればなんとかなるでしょ...

2011年12月26日月曜日

django-coverage

Django Coverageがなかなかよろしい。

2011年12月16日金曜日

BayPIGgiesに参加してきた

BayPIGgies=Bay Area Python Interest Groupの略。このあたりのPythonプログラミング愛好家が集う会。Mountain ViewのSymantecで昨日開催された会合に参加してきた。講師はMarilyn Davisさん。参加者は20名ほど。老弱男女ごっちゃまぜ。自分より年配と思われる方々で3割くらいを占めていた。

題目は「Pythonism」。Pythonらしさ、ということ。それは何かというと結局は「読みやすさ」に尽きるのだという。

自分は新しい言語を習う場合は

  • 変数やクラス定義
  • フロー制御
程度の文法だけ見たら、あとは提供されるフレームワークに必要な記述方法やテスト方法を学んでいくやり方なので「これがPythonでの書き方だ」というのをあまり意識したことがなかった。なので、昨日のMarilyn Davisさんの講義はなかなか参考になった。以下、自分の無知さをさらけ出しておく。

無知その(1) __init__でいちいち変数代入を書いていた。
以前の俺
class MyClass(object):
    def __init__(self, arg1 = None, arg2 = None):
        self.arg1 = arg1
        self.arg2 = arg2

if __name__ == '__main__':
    mc = MyClass(arg1 = 10, arg2 = 20)
    print mc.__dict__

今日からの俺
class MyClass(object):
    def __init__(self, **kwds):
        self.__dict__.update(kwds)

if __name__ == '__main__':
    mc = MyClass(arg1 = 10, arg2 = 20)
    print mc.__dict__


無知その(2) Loopのelse文を知らなかった。

#!/usr/bin/python

for num in range(1,200):  #to iterate between 10 to 20
   for i in range(2,num): #to iterate on the factors of the number
      if num%i == 0:      #to determine the first factor
         j=num/i #to calculate the second factor
         print '%d equals %d * %d' % (num,i,j)
         break #to move to the next number, the #first FOR
   else:        # else part of the loop
      print num, 'is a prime number'


無知その(3) Decoratorの便利さを知らなかった
import signal
import time

class TimeoutError(Exception):
    def __init__(self, value = "Timed Out"):
        self.value = value
    def __str__(self):
        return repr(self.value)

def timeout(seconds_before_timeout):
    def decorate(f):
        def handler(signum, frame):
            raise TimeoutError()
        def new_f(*args, **kwargs):
            old = signal.signal(signal.SIGALRM, handler)
            signal.alarm(seconds_before_timeout)
            try:
                result = f(*args, **kwargs)
            finally:
                signal.signal(signal.SIGALRM, old)
            signal.alarm(0)
            return result
        new_f.func_name = f.func_name
        return new_f
    return decorate

@timeout(5)
def mytest():
    print "Start"
    for i in range(1,10):
        time.sleep(1)
        print "%d seconds have passed" % i


if __name__ == '__main__':
    try:
        mytest()
    except TimeoutError:
        print "Timout!"


無知その(4) context handler(with)を知らなかった
What’s New in Python 2.6より
from decimal import Decimal, Context, localcontext

# Displays with default precision of 28 digits
v = Decimal('578')
print v.sqrt()

with localcontext(Context(prec=16)):
    # All code in this block uses a precision of 16 digits.
    # The original context is restored on exiting the block.
    print v.sqrt()



これくらいかな。あと、テストをどう書くかというのは結構議論が盛り上がった。

2011年12月3日土曜日

GAE1.6+Python2.7+DjangoでSessionを使う

Googleが公開しているDjangoの使い方は随分と古いが、Sessionをsettingsから外すように書かれている。これだとrequest.sessionが使えないのでappengine_djangoなどのツールを使ってSessionMiddlewareを差し込むわけだが、ただそれだけのためにごちゃごちゃと3rd partyのライブラリを入れるのは悔しいので、最低限度の方法を試してみた。

appengine_djangoには、Session保存のバックエンドとしてmemcacheを使うモジュールが公開されているのでこれを使うことにした。このスレッドの#2からリンクされているcache_backend.pyをプロジェクトの適当な場所(自分はlib配下)に保存し、settings.pyに以下を追加。


MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
)

# http://code.google.com/p/google-app-engine-django/issues/detail?id=57                                       
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
CACHE_BACKEND = 'lib.cache_backend://'

これでめでたくDjangoのviewでrequest.sessionを使えるようになる。

2011年12月2日金曜日

GAE 1.6+Django 1.2

Google App Engine 1.6.0から公式にPython 2.7+Django 1.2がサポートされたということなんだけど、ぐぐってみても従前からのappengine-djangoを使う方法ばかりでてきてよくわからん。

ということで色々試してみた。以下、わかったこと:

1. Djangoをzipする必要なし
ただしdjango-admin.pyとかmanage.pyとかで楽チンすることはできなくなる。まあ、そんなに面倒な事もないでしょ。GAEのコンソールも使えるし。

2. ローカルにDjangoをインストールする必要もなし
google_appengineディレクトリ配下に必要なDjangoのライブラリは入っている。

3. Virtualenvの元でも動く
先日書いたとおり

で、django-admin.py使わないということはディレクトリ配置やsettings.pyとかurls.pyとかを自前で打ち込むということなわけで、大した話ではない。まずディレクトリ配置はこんな感じ。
.
├── app.yaml
├── main.py
├── project
│   ├── __init__.py
│   ├── poll
│   │   ├── __init__.py
│   │   ├── views.py
│   ├── settings.py
│   ├── templates
│   │   └── __init__.py
│   ├── urls.py

app.yaml
librariesというエントリでDjango 1.2を使うことを宣言。
application: helloworld
version: 1
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /.*
  script: main.app

libraries:
- name: django
  version: "1.2"

main.py
settings.pyの所在を定義。

import os

os.environ['DJANGO_SETTINGS_MODULE'] = 'project.settings'

import django.core.handlers.wsgi
app = django.core.handlers.wsgi.WSGIHandler()

project/settings.py
必要なライブラリとアプリケーション所在場所、urls.pyの場所を定義。テンプレートを使う場合はその所在場所を定義。

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
)

INSTALLED_APPS = (
    'project.poll'
)

ROOT_URLCONF = 'project.urls'

import os
ROOT_PATH = os.path.dirname(__file__)
TEMPLATE_DIRS = (
    ROOT_PATH + '/templates',
)


project/urls.py
URL定義は通常のDjangoアプリと同じ。

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^$', 'project.poll.views.index'),
    )

project/poll/views.py
テスト用簡単なview

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^$', 'project.poll.views.index'),
    )
(GAE)masayang@ubuntu-vm:~/Documents/GAE/dempa$ cat project/poll/views.py
from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello")


2011年9月18日日曜日

python+botoとSimpleDBで手抜き開発

コツコツとGoogle App Engine上でアプリケーションを開発していたのだが、最近の料金変更を機会に方針を変更することに決めた。要はAmazon Web Service移行である。

で、データベース。GoogleはDatastoreを提供していたが、Amazonは幾つかの選択肢がある。

  • RDS(MySQL)
  • EC2にMySQL
  • SimpleDB
RDSは個人で使うにはちょっと豪華すぎる。今さらMySQLを設置保守するのも面倒。残るはSimpleDBでAmazonの説明によれば
You can get started with Amazon SimpleDB for free. New and existing customers receive 25 SimpleDB Machine Hours and 1 GB of Storage for free each month. Many applications should be able to operate perpetually within these free tier limits.
ということなので、よほど無茶な使い方をしなければ無料で行けそうだ。となるとあとはアプリケーションからの操作だが、これはbotoを使えばなんとかなろう。ただし説明書は絶望的なくらい揃っていない。ソースを読むしかない。以下、メモ。

boto.sdb.db.Modelを継承すれば永続化は簡単。
from boto.sdb.db.model import Model
from boto.sdb.db.property import *

class QuoteModel(Model):
    ticker = StringProperty()
    dt = DateTimeProperty()
    op = FloatProperty()

q = QuoteModel()
q.ticker = "ABC"
q.dt = some_date
q.op = 1.23
q.put()

boto.sdb.db.Queryを使って検索。

from boto.sdb.db.query import Query

query = Query(QuoteModel)
query.order('dt')
q = query.filter('ticker =', ticker)
q = query.filter('dt >=', some_date1)
q = query.filter('dt < ', some_date2)

for r in q:
    #dt順にならんだQuoteModelに対する操作が可能
botoの設定は/etc/boto.cfgにいれておけばよい。あとはPylonsでも組み合わせればいいんですかね。

2011年6月24日金曜日

超手抜き・既存システムのデータからSalesForce Objectを生成する

既存システムとSalesForceを連携させる方法は色々あるけど、既存システムで発生した新データをSalesForceに定期的にバッチ転送するのがお気軽&既存システムをいじくる必要がないので安心。
必要なもの:

単一Leadの生成と保存はこんな感じになる。




上記は最低限の項目しか埋めてない。実際に利用可能な項目はSetup->App Setup->Customize->Leadsを参照。Custom Fields使えば独自項目も簡単に設置できる。

既存システムから新規データを抽出するのはそんなに難しい話じゃないでしょ。あとはcronとかで。