최근 들어 Air Print 기능을 지원하는 프린터들이 많이 출시되고 있습니다. 회사에서 사용하고 있는 프린터는 지원이 되지 않아서 방법이 없나 찾아 봤는데, Mac(Lion)에 공유된 printer를 Air Print로 지원하게 하는 프로그램들이 있네요.
무료인데다가 iOS에서 메일이나 사진 메모 등의 print도 문제없이 지원되네요. 앞으로 자주 사용하게 될 듯 합니다.
최근 들어 Air Print 기능을 지원하는 프린터들이 많이 출시되고 있습니다. 회사에서 사용하고 있는 프린터는 지원이 되지 않아서 방법이 없나 찾아 봤는데, Mac(Lion)에 공유된 printer를 Air Print로 지원하게 하는 프로그램들이 있네요.
무료인데다가 iOS에서 메일이나 사진 메모 등의 print도 문제없이 지원되네요. 앞으로 자주 사용하게 될 듯 합니다.
iPhone에서 Google Calendar 사용법을 기록해 둡니다.
요약하면 Microsoft Exchange 설정으로 지정하고 email에 Google 계정 이메일 주소(Google Apps를 사용하는 경우에는 @nemustech.co.k와 같은 App주소)를 설정, 도메인 이름은 비워두고, Server 명에 m.google.com을 입력합니다.
계정에 설정된 공유 calendar는 iPhone 설정 화면에서 지정하지 않고, Safari로 http://m.google.com/sync 로 접속하여 웹에서 지정을 합니다. 언어설정을 English(US)로 변경하면 동기화할 Calendar 목록이 나오게 됩니다.
23개월간 iPhone 3GS를 사용하다가 이번에 iPhone 4S (32G white)로 변경을 했습니다.
배터리가 일찍 소진된다는 보고, 통화 중 Noise가 난다는 보고를 여러 커뮤니티 사이트에서 봤는데, 다행히도 뽑기운이 좋았는지 큰 문제 없이 사용을 하고 있습니다. 배터리는 좀 빨리 다는 듯한 느낌이 들긴 한데, 주변에 충전할 데가 많아서 그런대로 견디고 있습니다. 이미 iPhone 4에 적용이 되었던 Retina Display를 직접 써 보니 좋긴 하네요.
Dual-core A5 칩은 기대했던 것보다 빠르다는 느낌은 들지 않았습니다. iOS 5.x의 3GS가 생각보다 빠른 속도를 내 주고 있었기 때문인지도 모르겠네요. 기대했던 카메라는 만족스럽지만, 구동속도가 조금만 더 빨라졌으면 하는 바람과 카메라 셔터음 소거 기능(법적인 문제 때문에 불가능하다고 했지만)이 있었으면 하는 생각이 듭니다. 회의 중에 가끔 사진 찍을 일이 있는데 깜짝깜작 놀라고 있습니다. 4S에 들어간 Siri는 재밌는 장난감인데, 영어공부를 열심히 해야겠다는 생각을 들게 합니다. Cancel을 의도하고 발음한 것의 대부분이 Cancer로 인식을 하네요.
REST API를 지원하는 server가 필요해서 django로 구성 가능한 몇가지 framework(django-piston, django-tasypie, django-rest-framework)을 테스트 해 봤다.
세가지 framework 중에서 django-piston을 이용해서 작업을 했는데, 나중에는 django-rest-framework으로 변환할까 생각중이다.
두개의 테이블(user, rule)과 10여개의 API set을 가진 서버를 구성하는데, 반나절 남짓의 작업으로 가능했다. 2011.10월 시점으로 공식적으로 v0.2.2버전이 release 되었다. 개발은 쉽고 편했는데, 2년여간 update가 되지 않았고, 문서가 상대적으로 빈약했다. 최근에 새로운 maintainer가 코드를 인수인계 받아서 update가 기대되고 있다.
from piston.resource import Resource from latte.api.handlers import UserHandler, RuleHandler, RuleSearchOwnerHandler, RuleSearchTargetHandler class CsrfExemptResource(Resource): """A Custom Resource that is csrf exempt""" def __init__(self, handler, authentication=None): super(CsrfExemptResource, self).__init__(handler, authentication) self.csrf_exempt = getattr(self.handler, 'csrf_exempt', True) user = CsrfExemptResource(UserHandler) urlpatterns = patterns('', url(r'^user/?$', user), url(r'^user/(?P<email>[^/]+)/?$', user), )
from piston.handler import BaseHandler, AnonymousBaseHandler from piston.utils import rc, require_mime, require_extended from django.contrib.auth.models import User from latte.api.models import Rule class UserHandler(BaseHandler): """ EntryPoint for registration """ allowed_methods = ('GET', 'POST', 'DELETE') fields = ('email',) def read(self, request, email=None): """ """ if not email: return User.objects.filter(is_superuser=False) try: return User.objects.get(email=email,is_superuser=False) except (User.DoesNotExist, KeyError): return rc.NOT_FOUND def delete(self, request, email=None): """ """ if not email: return rc.BAD_REQUEST try: user = User.objects.get(email=email,is_superuser=False) except (User.DoesNotExist): return rc.NOT_FOUND user.delete() return rc.DELETED def create(self, request): """ Process connection : register user with email and password """ if not request.content_type: return rc.BAD_REQUEST data = request.data try: user = User.objects.get(username=data['email']) return rc.DUPLICATE_ENTRY except (User.DoesNotExist, KeyError): user = User(username=data['email'], email=data['email']) try: user.set_password(data['password']) user.save() except KeyError: return rc.BAD_REQUEST return rc.CREATED
v1.0.0-beta 버전이 release 되었다. dehydrate, hydrate function에서 GET과 PUT에 대한 세부적인 제어를 하게 되어 있다.
from django.conf.urls.defaults import * from tastypie.resources import ModelResource from tastypie.api import Api from api.resources import UserResource, RuleResource from django.contrib import admin admin.autodiscover() v1_api = Api(api_name='v1') v1_api.register(UserResource()) urlpatterns = v1_api.urls
from django.contrib.auth.models import User from latte.api.models import Rule from tastypie.resources import ModelResource from tastypie.authorization import Authorization from tastypie import fields from tastypie import http class UserResource(ModelResource): class Meta: queryset = User.objects.filter(is_superuser=False) list_allowed_methods = ['get', 'post'] detail_allowed_methods = ['get', 'post'] excludes = ['id'] resource_name = 'user' include_resource_uri = False authorization = Authorization() fields = ['username'] # def hydrate(self, bundle): # bundle.obj.email = bundle.data['username'] # return bundle def obj_create(self, bundle, request=None, **kwargs): try: user = User.objects.get(username=bundle.data['email']) return http.HttpConflict() except (User.DoesNotExist, KeyError): user = User(username=bundle.data['email'], email=bundle.data['email']) bundle.obj = user return bundle ...
앞의 두 framework 대비해서 자동으로 API 접근에 관한 문서를 만들어주는 장점이 있다. Django의 class based view에 기반을 해서 간단하고, 모듈화 된 장점도 있다.
from django.conf.urls.defaults import * from djangorestframework.resources import ModelResource from djangorestframework.views import ListOrCreateModelView, InstanceModelView from latte.api.resources import RuleResource from latte.api.views import TestView urlpatterns = patterns('', url(r'^test/$', TestView.as_view(), name="test"), url(r'^rule/$', ListOrCreateModelView.as_view(resource=RuleResource), name="rule-list"), url(r'^rule/(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource=RuleResource), name="rule-detail"), ... )
from django.core.urlresolvers import reverse from djangorestframework.views import View from djangorestframework.response import Response from djangorestframework import status from latte.api.models import Rule class TestView(View): """ TestView basic """ # model = Rule fields = ('title', 'when', 'where', 'message') def get(self, request): """ Handle Get requests """ # print request.data return Rule.objects.all()
doctest와 unittest를 이용해서 수작업으로 test suite를 만들던 중, 이를 자동화 할 수 있는 방법을 찾다. 필요한 package는 nose와 tdaemon이다.
sudo pip install nose django-nose tdaemon
settings.py에 다음 라인을 추가한다.
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
INSTALLED_APPS = (
...
'django_nose',
...
)
tdaemon --test-program=django --custom-args="--with-growl"
Mac에서 growl notification을 받기 위한 package.
hg clone http://bitbucket.org/osantana/nosegrowl cd nosegrowl/nose-growl sudo python setup.py install
정해진 시간에 주기적으로 수행되는 website를 구성하기 위한 package이다. Email 알림을 보낸다든지, site의 DB정리 등의 일을 처리할 수 있다.
간단한 해결책으로는 특정한 작업을 수행하는 URL을 주기적으로 호출하는 cron을 사용하는 방법도 있다. 그러나 이 방법은 권한 설정이나 configuraiton 설정등이 복잡해지는 문제가 있다. (속도문제도???)
Celery는 주로 분산 task queue에 주안을 두고 개발이 되었으나, 스케쥴러를 통한 작업도 가능하다. 보통 RabbitMQ를 back-end로 사용하나 여기서는 보다 간단한 djkombu를 사용하였다.
easy_install django-celery djkombu
settings.py에 등록 CARROT_BACKEND = "django" INSTALLED_APP = ( ... 'djcelery', 'djkombu', )
./manage.py syncdb
tasks.py 파일 생성(models.py가 위치한 directory) from celery.task.schedules import crontab from celery.decorators import periodic_task @periodic_task(run_every=crontab(hour='*', minute='*', day_of_week='*')) def test(): print "firing test task"
sudo ./manage.py celeryd -v 2 -B -s celery -E -l info
http://celeryproject.org/docs/django-celery/getting-started/first-steps-with-django.html
1분 단위로 수행하는 task를 등록해서 테스트를 해 봤는데, 정확히 1분마다 호출되지는 않는다.
doctest와 unittest 방법이 있다. 여기서는 unittest에 대해서 정리한다.
import unittest class MyFuncTestCase(unittest.TestCase): def testBasic(self): a = ['larry', 'curly', 'moe'] self.assertEquals(my_func(a, 0), 'larry') self.assertEquals(my_func(a, 1), 'curly')
test runner는 수행할 때 두 장소에서 unit tests를 찾는다.
import unittest from myapp.models import Animal class AnimalTestCase(unittest.TestCase): def setUp(self): self.lion = Animal.objects.create(name='lion', sound='roar') self.cat = Animal.objects.create(name='cat', sound='meow') def testSpeaking(self): self.assertEquals(self.lion.speak(), 'The lion says "roar"') self.assertEquals(self.cat.speak(), 'The cat says "meow"')
Test 데이타베이스늘 실제 production database가 사용되지 않고, 매번 test database가 만들어진다.
test client는 더미 웹 브라우저처럼 동작하는 Python class이다. 이를 통해 Django application을 뷰를 보거나 상호작용하는 것처럼 할 수 있다.
>>> from django.test.client import Client >>> c = Client() >>> response = c.post('/login/', {'username': 'john', 'password': 'smith'}) >>> response.status_code 200 >>> response = c.get('/customer/details/') >>> response.content '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 ...'
회사 메일을 forward해서 사용하고 있는 gmail을 7571MB중에서 7449MB를 썼다. Gmail에 특정 날짜 이전의 메일을 정리하는 기능이 없어서 만든 script이다. 프로그램을 수행하면, 2010년 1월 1일 이전의 메일을 전부 휴지통으로 이동한다.
Gmail은 imap.store('+FLAGS', '\\Deleted') 후에 imap.expunge()를 하더라도 실제 메일을 삭제하는 것이 아니라, 해당 메일에서 라벨만 제거하는 식으로 동작을 한다. 지우기 위해서는 메시지를 ‘[Gmail]/Trash’로 이동한 후, 휴지통에서 삭제하는 방식을 사용해야 한다.
휴지통 이름은 ‘언어설정’에 따라 달라지는 것 같다. 한국어로 설정했을 때는 결과값이 실패로 넘어오는데, 영문으로 바꾼 후에는 정상 동작한다.
#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright 2011 Jaemok Jeong(jmjeong@gmail.com) # # purgegmail.py # # Google mail purge utility # # [2011/04/19] import imaplib import datetime import getpass import sys # add user information gmail_user = '' # gmail account gmail_pass = '' # gmail passwd purgeDate = datetime.date(2010,01,1) # 이 날짜 앞의 메일들은 전부 삭제 # constant gmail_host = 'imap.gmail.com' def process(): imap = imaplib.IMAP4_SSL(gmail_host) try: imap.login(gmail_user, gmail_pass) except: print "Login failed" sys.exit(-1) num = imap.select() searchString = ('(before "%s")' % purgeDate.strftime("%d-%b-%Y")) typ, data = imap.search (None, searchString) y_or_n = raw_input("Delete %d messages? " % len(data[0].split(' '))) if (not (y_or_n == 'y' or y_or_n == 'Y')): return data = ','.join(data[0].split(' ')) typ, data = imap.copy(data, '[Gmail]/Trash') if typ == "OK": print "Delete sucessful!" else: print "Delete failed!" imap.close() imap.logout() if __name__ == '__main__': if gmail_user == '': gmail_user = raw_input('gmail id: ') if gmail_pass == '': gmail_pass = getpass.getpass() process() print "Finished..."
Python binding for Admob Api
#!/usr/bin/env python # -*- coding: utf-8 -*- # # Python binding for Admob API # # Copyright 2011 Jaemok Jeong(jmjeong@gmail.com) # # [2011/03/14] import urllib,urllib2 import sys import datetime try: import json except ImportError: raise ImportError, "Python 2.6+ has built-in json module" class AdmobNetworkErrorException(Exception): pass class AdmobErrorException(Exception): pass class AdmobApi(object): """ Retrieve Admob information """ def __init__(self, client_key=''): self.client_key = client_key self.opener = urllib2.build_opener() def login(self, email, pw): urlBase = 'https://api.admob.com/v2/auth/login' requestData = urllib.urlencode({ 'client_key': self.client_key, 'email': email, 'password' : pw, }) try: urlHandle = self.opener.open(urlBase, requestData) except urllib2.HTTPError, urlib2.URLError: raise AdmobNetworkErrorException result = json.loads(urlHandle.read()) if not result['errors']: return result['data']['token'] else: print result['errors'] raise AdmobErrorException def search(self, token): urlBase = 'https://api.admob.com/v2/site/search?%s' requestData = urllib.urlencode({ 'client_key': self.client_key, 'token': token, }) requestUrl = urlBase % requestData try: urlHandle = self.opener.open(requestUrl) except urllib2.HTTPError, urlib2.URLError: raise AdmobNetworkErrorException result = json.loads(urlHandle.read()) if not result['errors']: return result['data'] else: print result['errors'] raise AdmobErrorException def stats(self, token, id, start_date, end_date): urlBase = 'https://api.admob.com/v2/site/stats?%s' requestData = urllib.urlencode({ 'client_key': self.client_key, 'token': token, 'site_id': id, 'start_date': start_date, 'end_date': end_date, 'time_dimension' : 'day', }) requestUrl = urlBase % requestData try: urlHandle = self.opener.open(requestUrl) except urllib2.HTTPError, urlib2.URLError: raise AdmobNetworkErrorException result = json.loads(urlHandle.read()) if not result['errors']: return result['data'] else: print result['errors'] raise AdmobErrorException if __name__ == '__main__': client_key = '' # You can create client key at http://www.admob.com/api email = '' # ID passwd = '' # Passwd admob = AdmobApi(client_key) token = admob.login(email, passwd) today = datetime.datetime.now() # start_date = datetime.date(2009,1,1) start_date = today + datetime.timedelta(days=-7) end_date = today + datetime.timedelta(days=1) result = admob.search(token) for i in result: print "== ", i['name'], " ==" data = admob.stats(token, id=i['id'], start_date=start_date.strftime('%Y-%m-%d'), end_date=end_date.strftime('%Y-%m-%d')) for j in data: if j['revenue'] != 0: print j['date'], ":", j['revenue']
GUI prototyping을 할 필요가 있어서 wxPython을 사용해 보았다. MacOSX 버전은 Carbon API를 사용하기 때문에 32-bit 모드에서만 동작을 해서, windows 버전으로 개발을 했다.
Tutorial(http://wiki.wxpython.org/Getting%20Started, http://www.zetcode.com/wxpython)과 demo code, 문서들이 잘 되어 있어서 처음 learning curve는 낮은 편이었다.
세밀한 제어가 가능했지만, Designer없이 화면 layout을 구성하는게 번거로운 단점이 있었다. 결과물의 품질은 native win32 coding으로 했을때와 별 차이가 없어 보였다.
만든 프로그램은 py2exe를 이용하여 실행파일로 만들어 배포할 수 있다.
from distutils.core import setup
import py2exe
import sys; sys.argv.append('py2exe')
py2exe_options = dict(
ascii=False, # Exclude encodings
excludes=['_ssl', # Exclude _ssl
'pyreadline', 'difflib', 'doctest',
'optparse', 'pickle', 'calendar'], # Exclude standard library
dll_excludes=['msvcr71.dll'], # Exclude msvcr71
packages=['mako.cache'],
compressed=True, # Compress library.zip
)
setup(name='Test,
version='0.5',
description='Test,
author='jmjeong',
console=['test.py'],
options={'py2exe': py2exe_options},
)
output은 exe와 library등으로 구성되어 있는데, NSIS를 이용하여 압축하여 하나의 실행파일로 만들 수 있다. 4M 크기의 최종 결과물이 만들어졌다. 프로그램을 수행하면, 자동으로 압축을 해제하고 실행하기 때문에 처음 실행 시간이 약간 걸린다.
!define py2exeOutputDirectory 'dist\'
!define exe 'test.exe'
; Comment out the "SetCompress Off" line and uncomment
; the next line to enable compression. Startup times
; will be a little slower but the executable will be
; quite a bit smaller
;SetCompress Off
SetCompressor lzma
Name 'qb'
OutFile ${exe}
SilentInstall silent
;Icon 'icon.ico'
Section
InitPluginsDir
SetOutPath '$PLUGINSDIR'
File '${py2exeOutputDirectory}\*.*'
GetTempFileName $0
DetailPrint $0
Delete $0
StrCpy $0 '$0.bat'
FileOpen $1 $0 'w'
FileWrite $1 '@echo off$\r$\n'
StrCpy $2 $TEMP 2
FileWrite $1 '$2$\r$\n'
FileWrite $1 'cd $PLUGINSDIR$\r$\n'
FileWrite $1 '${exe}$\r$\n'
FileClose $1
nsExec::Exec $0
Delete $0
SectionEnd