| @@ -0,0 +1,24 @@ | ||
| ImportNew Android App 从0到1 | ||
| ============================== | ||
| [ImportNew](http://www.importnew.com)是一个专注于 Java 相关技术分享的博客,平常自己关注ImportNew比较多,曾经有想过自己开发一个客户端出来,可以随时随地方便在移动设备上阅读,不过很多时候仅仅想想就过去了,但这次是玩真的,从零开始写ImportNew客户端,前后断断续续用业余时间花了两周多时间把Demo做了出来,实践证明实现一个想法并没有想象中的那么难,主要问题还是太懒。 | ||
|
|
||
| ####为什么要做ImportNew Android客户端 | ||
| 最早接触Android还是在10年的时候,当时Nokia还是市场主流,周围同学几乎人手一台Nokia,不过隔壁宿舍有个土豪买了个Android 系统的SumSang手机,那是第一次体验到Android,那界面在当时绝对算得上惊艳,尽管那时还没见过iPhone长啥样。于是回宿舍后手痒痒地开始按照官方文档把开发环境搭建好开启人生中第0个Android App------那个人尽皆知的Hello World,那时的开发工具和现在根本没法比,开个模拟器比蜗牛还慢,此后就再也没去折腾了。今天又重新拾起来,主要是因为我终于有了自己的第一台Android手机(这样装逼或许会被打的),其实真正的初衷源于自己想做一款专属健康管理App,因为大部分码农从来都不在乎自己的身体健康。那么问题来了,你的第1个App为什么是ImportNew客户端?其实ImportNew的前身就是前面提到的那个健康管理App。最开始画了一个ListView出来,接着了解到Fragments的用法,看到Fragments的一个例子是左侧列表Items,右侧是某个Item的详情,这让我想到可以先做个ImportNew客户端出来,于是就开干了。 | ||
|  | ||
| ####ImportNew技术细节 | ||
| 前后端分离,做各自擅长的事是一个应用程序延续生命力基本要素。客户端所展示的数据均源自ImportNew网站,后端有专门的爬虫,不定时爬取最新文章,通过提供Restful风格接口返回结构化文档给客户端展示。 | ||
| ####后端技术 | ||
| 爬虫使用`tornado.queues`模块用协程的方式实现异步的生产者/消费者模型,并发地爬取页面。在单机环境下爬完整站所有文章在1分多钟的时间,如果用普通[Requests](http://docs.python-requests.org/en/latest/)库单线程模式则会花费30多分钟。 | ||
|  | ||
|
|
||
| 后端接口使用Python/Tornado实现,数据库直接使用Redis,这种比较简单的客户端用Redis简直是绝配,文章列表用了SortedSet,文章详情用Hash结构来表示,完全没必要用MySQL。 | ||
| ####前端技术 | ||
| 最开始做Demo的时候用ListView组件展示文章列表,自定义ViewHolder来保存试图引用提高ListView的滑动流畅度,后来发现有一个更高级的控件RecycleView用来替换ListView,RecyclerView是谷歌V7包下新增的控件,该控件默认实现了ViewHold模式。此外文章的详细页面用过好几种方法,最开始直接传递一个url给Webview加载文章的详细页面,但这样的实现方式显然与直接用浏览器没什么区别。因此,后来接着把文章的详细页面数据也一同爬取到,此时Webview加载的一个文本。文章的样式依然通过css控制,只不过Css是放在本地加载,而无需再通过WebView去远程服务器加载样式文件,这样的方案可以提高页面的响应速度。所有的网络请求用异步加载库Volley。 | ||
|  | ||
|
|
||
| 所有代码已开源:[ImportNewApp](https://github.com/lzjun567/ImportNewApp),[ImportNewAPI](https://github.com/lzjun567/ImportNewAPI)。App下载地址: | ||
|  | ||
|
|
||
| 最后,感谢这个伟大的开源时代,让学习变得更简单。 | ||
|
|
||
|
|
| @@ -0,0 +1,109 @@ | ||
| ubuntu 搭建 pharicator | ||
| ====================== | ||
| #### 安装Nginx, MySQL, PHP (LEMP) | ||
|
|
||
| sudo apt-get update | ||
| sudo apt-get install mysql-server php5-mysql | ||
| sudo apt-get install nginx | ||
| sudo apt-get install php5-fpm | ||
| 配置php | ||
|
|
||
| sudo vim /etc/php5/fpm/php.ini | ||
| 找到cgi.fix_pathinfo=1所在的行,把1改为0 | ||
|
|
||
| cgi.fix_pathinfo=0 | ||
| 修改配置www.conf: | ||
|
|
||
| sudo vim /etc/php5/fpm/pool.d/www.conf | ||
| 找到listen = 127.0.0.1:9000,替换成/var/run/php5-fpm.sock | ||
|
|
||
| listen = /var/run/php5-fpm.sock | ||
| 重启php-fpm: | ||
|
|
||
| sudo service php5-fpm restart | ||
|
|
||
| 添加Phabricator 虚拟主机: | ||
|
|
||
| sudo vim /etc/nginx/sites-available/phabricator | ||
| 配置文件内容: | ||
|
|
||
| server { | ||
| listen 80 default_server; | ||
| listen [::]:80 default_server ipv6only=on; | ||
|
|
||
| root /home/ubuntu/phabricator/webroot; | ||
| index index.html index.htm; | ||
|
|
||
| server_name phabricator.example.com; # 配置你自己的域名 | ||
|
|
||
| location / { | ||
| index index.php; | ||
| rewrite ^/(.*)$ /index.php?__path__=/$1 last; | ||
| } | ||
|
|
||
| error_page 404 /404.html; | ||
|
|
||
| error_page 500 502 503 504 /50x.html; | ||
| location = /50x.html { | ||
| root /usr/share/nginx/www; | ||
| } | ||
|
|
||
| location = /favicon.ico { | ||
| try_files $uri =204; | ||
| } | ||
|
|
||
| location /index.php { | ||
| try_files $uri =404; | ||
| fastcgi_pass unix:/var/run/php5-fpm.sock; | ||
| fastcgi_index index.php; | ||
| include fastcgi_params; | ||
| } | ||
| } | ||
|
|
||
| 需要注意的点: | ||
| * 如果没有配置rewrite,则会显示错误: | ||
|
|
||
| Request parameter '__path__' is not set. Your rewrite rules are not configured correctly. | ||
|
|
||
| * | ||
| git clone git://github.com/facebook/libphutil.git | ||
| 如果没有安装 libphutil.git会报错: | ||
|
|
||
| Unable to load libphutil. Put libphutil/ next to phabricator/, or update your PHP 'include_path' to include the parent directory of libphutil/. | ||
| 如果没有安装 arcanist | ||
|
|
||
| git clone git://github.com/facebook/arcanist.git | ||
| 报错 | ||
|
|
||
| [Core Exception/PhutilBootloaderException] Include of 'arcanist/src/__phutil_library_init__.php' failed! | ||
|
|
||
| 设置数据库: | ||
|
|
||
| ./bin/config set mysql.host localhost | ||
| ./bin/config set mysql.user root | ||
| ./bin/config set mysql.pass 123456 | ||
| 最后运行: | ||
|
|
||
| ./bin/storage upgrade | ||
| 此时首页可以显示出来了。 | ||
|
|
||
| ####Mail设置 | ||
| 1. 安装sendmail | ||
|
|
||
| sudo apt-get install sendmail | ||
| 2. mailer-adpter 修改成 PhabricatorMailImplementationPHPMailerLiteAdapter | ||
|
|
||
| 3. 修改PHPMailer | ||
|
|
||
| ./bin/config set phpmailer.smtp-host smtp.qq.com | ||
| ./bin/config set phpmailer.smtp-port 465 | ||
| ./bin/config set phpmailer.smtp-protocol SSL | ||
| ./bin/config set phpmailer.smtp-user xxxx@qq.com | ||
| ./bin/config set phpmailer.smtp-password xxxx | ||
| ./bin/phd restart | ||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
| @@ -1,27 +1,67 @@ | ||
| 可能会不常用到的Git命令 | ||
| ================== | ||
| 1. 统计代码行数 | ||
|
|
||
| git ls-files | xargs wc -l | ||
| 2. 统计python代码行数 | ||
|
|
||
| git ls-files | grep .py | xargs wc -l | ||
|
|
||
| 3. 更新fork项目 | ||
| 如果你fork了别人的项目,过段时间突然发现该项目更新了很多内容,于是你想同步更新到自己的仓库中,可以按如下步骤: | ||
|
|
||
| 1. 添加一个remote指向上游仓库 | ||
|
|
||
| git remote add upstream https://github.com/keleshev/schema.git | ||
| 2. 获取上游远程分支更新内容 | ||
|
|
||
| git fetch upstream | ||
| 3. 合并到本地分支 | ||
|
|
||
| git checkout master | ||
| git merge upstream/master | ||
|
|
||
| 4. 删除远程分支 | ||
|
|
||
| git push origin --delete branch-a # 删除远程分支branch-a | ||
| * 错误处理:删除远程分支的时候报错 | ||
|
|
||
| error: unable to delete 'lzjun': remote ref does not exist | ||
| error: failed to push some refs to 'git@bitbucket.org:sponialtd/openplay_pylibs.git' | ||
| 我本地显示的分支(远程仓库中已经没有了该分支): | ||
|
|
||
| remotes/origin/lzjun | ||
| 解决办法是: | ||
|
|
||
| git fetch -p origin #http://stackoverflow.com/questions/10292480/when-deleting-remote-git-branch-error-unable-to-push-to-unqualified-destinatio | ||
|
|
||
| 5. 不再同步某些已经加入仓库的文件 | ||
|
|
||
| 有时候忘记添加.gitignore文件,而误把一些pyc文件同步到了仓库中,此时你希望不再同步这些文件,使用: | ||
|
|
||
| git update-index --assume-unchanged path/to/file | ||
| 如果现在你又想把它加入到仓库中,怎么办?使用: | ||
|
|
||
| git update-index --no-assume-unchanged path/to/file | ||
|
|
||
| 6. 删除一次commit | ||
|
|
||
| 由于某些原因导致误操作增加了一次commit,现在想删除它,怎么办? | ||
|
|
||
| git log | ||
|
|
||
| commit 663019f323084a7ebdab0aa96223272816d64322 | ||
| Author: liuzhijun <lzjun567@gmail.com> | ||
| Date: Thu Aug 27 12:25:12 2015 +0800 | ||
|
|
||
| Remove TODO | ||
|
|
||
| commit 0d6aa254961945372d3108ab053b51426194cbaf | ||
| Author: liuzhijun <lzjun567@gmail.com> | ||
| Date: Thu Aug 27 12:22:59 2015 +0800 | ||
|
|
||
| Remove TODO | ||
| 我要删除6630这个commit | ||
|
|
||
|
|
||
| @@ -0,0 +1,15 @@ | ||
| 1. ObjectId用ObjectId存储,不要用字符串来存,ObjectId只占用12个字节,而字符串是它的两倍多,其次可以从ObjectId对象中获取时间等信息。 | ||
|
|
||
| 导入/导出 | ||
|
|
||
| mongodump -d <our database name> -o <directory_backup> | ||
|
|
||
| mongorestore -d DATABASE ./dump-folder | ||
|
|
||
|
|
||
| or 查询 | ||
| 查询时,有时需要一个值在多个字段中,比如:用户输入的可能是邮箱,也可能是手机号码,那么我需要用or来操作: | ||
|
|
||
| {$or: [{username: xxxxx}, {phone: xxxxxxx}]} | ||
|
|
||
|
|
| @@ -0,0 +1,8 @@ | ||
| 守护进程方式启动mongodb进程 | ||
| mongod --fork --dbpath data/rs0-0 --logpath log/rs0-0/rs0-0.log --rest --replSet rs0 --port 37017 | ||
|
|
||
| 2015-06-18T13:57:11.637+0800 ** WARNING: --rest is specified without --httpinterface, | ||
| 2015-06-18T13:57:11.638+0800 ** enabling http interface | ||
| about to fork child process, waiting until server is ready for connections. | ||
| forked process: 14141 | ||
| child process started successfully, parent exiting |
| @@ -1,10 +1,101 @@ | ||
| Celery-----分布式任务队列 | ||
| ======================= | ||
| 高可用:任务失败或者连接断了自动重试 | ||
| 快:一个celery进程可以处理上10万的任务每分钟 | ||
| 灵活:可以自定义实现每一个模块 | ||
|
|
||
|
|
||
|
|
||
| 如果你是windows用户,首先现在安装[redis](https://github.com/MSOpenTech/redis/blob/2.6/bin/release/redisbin64.zip), | ||
|
|
||
| 安装celery | ||
|
|
||
| pip install celery | ||
| pip install redis | ||
| 默认会安装好celery最新版本 | ||
|
|
||
| 创建Celery实例 | ||
|
|
||
| app = Celery('tasks', broker='redis://localhost') | ||
|
|
||
| 创建任务(tasks.py): | ||
|
|
||
| from celery import Celery | ||
|
|
||
| app = Celery('tasks', broker='redis://localhost') | ||
|
|
||
| @app.task | ||
| def add(x, y): | ||
| return x + y | ||
|
|
||
| 启动worker进程: | ||
|
|
||
| celery -A tasks worker --loglevel=info | ||
|
|
||
|
|
||
| 在代码中调用task: | ||
|
|
||
| >>> from tasks import add | ||
| >>> add.delay(4, 4) | ||
| 执行过程:先会把任务放入队列(默认名字就叫celery)中,如果worker进程启动了,就会从队列中取出来,消费掉它。 | ||
|
|
||
| AMQP 术语 | ||
| MESSAGE | ||
|
|
||
| {'task': 'myapp.tasks.add', | ||
| 'id': '54086c5e-6193-4575-8308-dbab76798756', | ||
| 'args': [4, 4], | ||
| 'kwargs': {}} | ||
|
|
||
|
|
||
|
|
||
|
|
||
| 发送消息的客户端是生产者 | ||
| 接受消息的是消费者 | ||
| broker:消息服务器,路由消息从生产者到消费者 | ||
|
|
||
|
|
||
|
|
||
| 交换机(exchange): 接收消息,转发消息到绑定的队列,有好几种类型 | ||
|
|
||
| 正常发送消息和接受消息的步骤: | ||
| 1. 创建exchange | ||
| 2. 创建队列 | ||
| 3. 绑定队列到exchange上 | ||
|
|
||
|
|
||
| 把Celery应用到Application中去: | ||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
| Highly Available | ||
|
|
||
| Workers and clients will automatically retry in the event of connection loss or failure, and some brokers support HA in way of Master/Master or Master/Slave replication. | ||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
| @@ -0,0 +1,71 @@ | ||
| 小记:datetime 与 timestamp 相互转换遇到的坑 | ||
| ======================= | ||
| 事情是这样的,今天遇到一个业务场景:按照比赛的时间start_at作为分页查询的条件获取赛程列表,首先初始化20条数据(数据库用的是MongoDB): | ||
|
|
||
| 第1条记录: start_at: 2015-08-15 01:13:17.330299 | ||
| 第2条记录: start_at: 2015-08-15 01:13:18.330299 | ||
| 第3条记录: start_at: 2015-08-15 01:13:19.330299 | ||
| 第4条记录: start_at: 2015-08-15 01:13:20.330299 | ||
| 第5条记录: start_at: 2015-08-15 01:13:21.330299 | ||
| 第6条记录: start_at: 2015-08-15 01:13:22.330299 | ||
| 第7条记录: start_at: 2015-08-15 01:13:23.330299 | ||
| 第8条记录: start_at: 2015-08-15 01:13:24.330299 | ||
| 第9条记录: start_at: 2015-08-15 01:13:25.330299 | ||
| 第10条记录: start_at: 2015-08-15 01:13:26.330299 | ||
|
|
||
| 客户端请求的时候通过`last_id`来确定下次从什么位置获取,第一次请求的时候不需要此参数,系统默认从第1条开始查询,此处假设`page_size`为3,每次获取3条。第1次请求完,服务端会返回一个`last_id`给客户端,那么这个`last_id`是怎么生成的呢?服务端会把第3条记录的start_at从datetime转换成timestamps,返回一个int类型的时间戳给客户端,第次一的查询条件是: | ||
|
|
||
| cursor = conn.matches.find({}).sort([('start_at', 1)]).limit(3) | ||
| 返回前3条数据以及`last_id`(`last_id`是根据第三条数据数据的的`start_at`转换成时间戳之后的值) | ||
|
|
||
| last_id = time.mktime(start_at.timetuple()) | ||
| >>> 1439572399.0 # 第三条记录 2015-08-15 01:13:19.330000 转换成时间戳的值 | ||
| 客户端发起第2次请求获取第2页的时候,把该数值传递到服务端,服务端接收到`last_id=1439572399`后,做一次转换,转换成datetime类型: | ||
| start_at = datetime.datetime.fromtimestamp(last_id) | ||
| 第二次查询的条件是: | ||
|
|
||
| cursor = conn.matches.find({'start_at': {'$gt': start_at}}).sort([('start_at', 1)]).limit(3) | ||
| 于是碰到一个奇怪的问题:**第二次查询返回的第一条数据和第一次查询返回的数据是一样的**,感觉查询条件`$gt`变成了`gte`,这太不科学了,开始根本找不到原因,于是把初始化数据修改了一下: | ||
|
|
||
| 第1条记录: start_at: 2015-08-14 11:05:21 | ||
| 第2条记录: start_at: 2015-08-14 11:05:22 | ||
| 第3条记录: start_at: 2015-08-14 11:05:23 | ||
| 第4条记录: start_at: 2015-08-14 11:05:24 | ||
| 第5条记录: start_at: 2015-08-14 11:05:25 | ||
| 第6条记录: start_at: 2015-08-14 11:05:26 | ||
| 第7条记录: start_at: 2015-08-14 11:05:27 | ||
| 第8条记录: start_at: 2015-08-14 11:05:28 | ||
| 第9条记录: start_at: 2015-08-14 11:05:29 | ||
| 第10条记录: start_at: 2015-08-14 11:05:30 | ||
|
|
||
| 然后重复上面的查询,此时返回的结果是正常的。究其原因是什么呢?从两份数据的对比,唯一的区别就是前者的时间带有_毫秒数_,为啥带上毫秒就出问题了呢,于是开始一步步调试发现一处细节,timestamp转换成datetime的时候,最后的毫秒丢失了: | ||
|
|
||
| start_at = datetime.datetime.fromtimestamp(1439572399) | ||
| >>> 2015-08-15 01:13:19 | ||
| 于是问题就可以解释的通了,没有带毫秒的`2015-08-15 01:13:19`肯定是小于`2015-08-15 01:13:19.330299`的,因此第二次查询的时候把第一次的查询返回的最后一条数据也查出来了。问题定位到了,那么就好解决了,原来最终的bug就出现在datetime转成timestamp的埋下的。 | ||
| last_id = time.mktime(start_at.timetuple()) | ||
| 转换的时候,会自动忽略掉毫秒级别的值,解决的办法就是把毫秒数加上: | ||
|
|
||
| time.mktime(start_at.timetuple()) + start_at.microsecond * 0.000001 | ||
|
|
||
| 整个世界都清静了。 | ||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
| @@ -0,0 +1,67 @@ | ||
| Python 字典数据类型(dict)源码分析 | ||
| ================================ | ||
| 字典类型是Python中最常用的数据类型之一,它是一个键值对的集合,字典通过键来索引,关联到相对的值,理论上它的查询复杂度是 O(1) : | ||
|
|
||
| >>> d = {'a': 1, 'b': 2} | ||
| >>> d['c'] = 3 | ||
| >>> d | ||
| {'a': 1, 'b': 2, 'c': 3} | ||
| 通过key来访问value: | ||
|
|
||
| >>> d['a'] | ||
| 1 | ||
| >>> d['b'] | ||
| 2 | ||
| >>> d['c'] | ||
| 3 | ||
| >>> d['d'] | ||
| Traceback (most recent call last): | ||
| File "<stdin>", line 1, in <module> | ||
| KeyError: 'd' | ||
| 遇到不存在的key就会出现异常KeyError,当然也可以使用`d.get('d')`的方式默认返回`None`值。 那么字典的内部结构是怎样的呢? | ||
|
|
||
| ####哈希表 (hash tables) | ||
| 哈希表(也叫散列表),根据关键值对(Key value)而直接进行访问的数据结构。它通过把key和value映射到表中一个位置来访问记录,这种查询速度非常快,更新也快。这个映射函数叫做哈希函数,存放值的数组叫做哈希表。 | ||
|
|
||
| 1. 数据添加过程:把key通过哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。 | ||
| 2. 数据查询过程:再次使用哈希函数将key转换为对应的数组下标,并定位到数组的位置获取value。 | ||
|
|
||
| 但是,对key进行hash的时候,不同的key可能hash出来的结果是一样的,那么就使用链表的数组法来表示。 | ||
|
|
||
| Python 的字典结构就是使用哈希表结构来实现的。一个好的hash函数应最小化冲突的数量, | ||
|
|
||
| 解决哈希表发生碰撞的方法:开放寻址法,链接法。 | ||
|
|
||
| ####开放寻址法(open addressing) | ||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
| http://www.cnblogs.com/michaelyin/archive/2011/02/14/1954724.html | ||
| http://www.laurentluce.com/posts/python-dictionary-implementation/ | ||
|
|
||
| http://stackoverflow.com/questions/327311/how-are-pythons-built-in-dictionaries-implemented | ||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
| @@ -0,0 +1,66 @@ | ||
| Python中的Enum | ||
| ================ | ||
| Enum(枚举)在很多应用场景中都会出现,因此绝大部分编程语言都实现了Enum类型,Python也不列外,但列外的是Enum在Python3.4中才被正式支持,我们先来看看Python3中的Enum是怎么使用的。 | ||
| 枚举的创建方式很简单,就像创建一个类一样,只需继承Enum: | ||
|
|
||
| >>> from enum import Enum | ||
| >>> class Role(Enum): | ||
| ... admin = 1 | ||
| ... manager = 2 | ||
| ... guest = 3 | ||
| 它的语法和定义`class`完全是一样的,但它并不是一个真正的class。这里的`Role`是Enum类型,里面的成员`admin`,`manager`都是它的实例对象,它们的类型是`Role`类型的: | ||
|
|
||
| >>> type(Role) | ||
| <class 'enum.EnumMeta'> | ||
| >>> type(Role.admin) | ||
| <enum 'Role'> | ||
| >>> | ||
| 枚举的每一个实例对象都有自己的名字和值: | ||
|
|
||
| >>> Role.admin.name | ||
| 'admin' | ||
| >>> Role.admin.value | ||
| 1 | ||
| 枚举内部更像是一个OrderedDict: | ||
|
|
||
| Role.__members__ | ||
| mappingproxy(OrderedDict([('admin', <Role.admin: 1>), ('manager', <Role.manager: 2>), ('guest', <Role.guest: 3>)])) | ||
| >>> | ||
| Python2.x: | ||
|
|
||
| #!/usr/bin/env python | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| __author__ = 'liuzhijun' | ||
|
|
||
| def enum(name, *sequential, **named): | ||
| values = dict(zip(sequential, range(len(sequential))), **named) | ||
| values['values'] = values.values() | ||
| # NOTE: Yes, we *really* want to cast using str() here. | ||
| # On Python 2 type() requires a byte string (which is str() on Python 2). | ||
| # On Python 3 it does not matter, so we'll use str(), which acts as | ||
| # a no-op. | ||
| # return type(str(name), (), values) | ||
| import collections | ||
| aa = collections.namedtuple(str(name), values.keys()) | ||
| return aa(**values) | ||
|
|
||
|
|
||
| JobStatus = enum( | ||
| 'JobStatus', | ||
| QUEUED='queued', | ||
| FINISHED='finished', | ||
| FAILED='failed', | ||
| STARTED='started', | ||
| DEFERRED='deferred' | ||
| ) | ||
|
|
||
| print JobStatus.QUEUED | ||
| print JobStatus.FAILED | ||
| print JobStatus.STARTED | ||
| print JobStatus._fields | ||
| print JobStatus.values |
| @@ -0,0 +1,3 @@ | ||
| Python日志最佳实践 | ||
| ================== | ||
| 默认的日志级别是**WARNING** |
| @@ -0,0 +1,14 @@ | ||
| MagicMock | ||
|
|
||
|
|
||
| >>> from mock import MagicMock | ||
| >>> thing = ProductionClass() | ||
| # 可以指定返回值 | ||
| >>> thing.method = MagicMock(return_value=3) | ||
| >>> thing.method(3, 4, 5, key='value') | ||
| 3 | ||
| # make assertion about how they have been used | ||
| >>> thing.method.assert_called_with(3, 4, 5, key='value') | ||
| # 执行异常 | ||
| mock = Mock(side_effect=KeyError('foo')) |
| @@ -0,0 +1,49 @@ | ||
| Python Mock 学习笔记 | ||
| =================== | ||
|
|
||
| ####hello.py | ||
|
|
||
| #!/usr/bin/env python | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| __author__ = 'liuzhijun' | ||
|
|
||
| import os | ||
|
|
||
|
|
||
| def rm(filename): | ||
| if os.path.isfile(filename): | ||
| os.remove(filename) | ||
| ####test_hello.py | ||
|
|
||
| #!/usr/bin/env python | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| import unittest | ||
|
|
||
| import mock | ||
|
|
||
| from hello import rm | ||
|
|
||
|
|
||
| class RmTestCase(unittest.TestCase): | ||
| @mock.patch('hello.os.path') | ||
| @mock.patch('hello.os') | ||
| def test_rm(self, mock_os, mock_path): | ||
| mock_path.isfile.return_value = False | ||
| rm("any path") | ||
| print mock_os | ||
| self.assertFalse(mock_os.remove.called, "no call") | ||
| mock_path.isfile.return_value = True | ||
| rm("any path") | ||
| mock_os.remove.assert_called_with("any path") | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| unittest.main() | ||
|
|
||
| *test_rm*函数的第一个参数有靠近该方法最近的那个装饰器提供。 | ||
|
|
||
| 重构*hello.py*: | ||
|
|
||
|
|
| @@ -0,0 +1,101 @@ | ||
| 通知系统设计 | ||
| ============= | ||
|
|
||
| 通知就是某某东西(Object)被某个人(actor)改变(verb)后需要报告给用户(subject) | ||
|
|
||
| notification_id, user_id | ||
|
|
||
|
|
||
|
|
||
|
|
||
| actor:角色 | ||
| 球队:福山金威(把你移出了俱乐部) | ||
| 领队:福山金威领队(把你设置为球员10号) | ||
| 联赛:和特联赛(邀请你参赛) | ||
| 球员:梁月明(已经激活,正式加盟) | ||
| 系统:OpenPlay欢迎你加入 | ||
| 其他: | ||
| verb:动作 | ||
| 设置 | ||
|
|
||
|
|
||
| 图标显示的类型就是角色 | ||
|
|
||
|
|
||
| id | ||
| rceiver_id | ||
| is_read | ||
| sender: actor{id, name , type} | ||
| verb: remove/invite/join/ | ||
| object: 目标对象 球队 | ||
|
|
||
|
|
||
| 赛事组织者 :需要包含赛事信息 | ||
|
|
||
|
|
||
| 我已加入xxx俱乐部 | ||
| receiver_id:我 | ||
| verb:加入 | ||
| Object_id:xxx俱乐部 | ||
| sender:系统 | ||
|
|
||
| XXX俱乐部加入了xxx联赛(所有球员收到通知) | ||
| receiver_id :所有球员 | ||
| object_id :xxx联赛 | ||
| "verb": 加入 | ||
| sender: xxx俱乐部 | ||
|
|
||
| XX 球员激活了帐号 | ||
| receiver_id:领队 | ||
| verb: 激活 | ||
| sender: xx球员 | ||
| Object_id:账号 | ||
|
|
||
| 赛事组织者公布了赛程 | ||
| receiver_id:所有球员 | ||
| verb:公布 | ||
| sender:赛事组织者 | ||
| Object_id:赛程(可以为空) | ||
|
|
||
| 赛事组织者修改了赛程 | ||
| 同上 | ||
|
|
||
| 收到 XX 赛事组织者的消息:文字内容 | ||
| receiver_id:领队 | ||
| verb:收到 | ||
| sender:赛事组织者 | ||
| Object_id:文字内容 | ||
|
|
||
| XXX(俱乐部名称)被 XXXX(赛事名称)取消参赛资格 | ||
| receiver_id:领队 | ||
| verb:取消资格 | ||
| sender:赛事组织者 | ||
| Object_id :该赛事 | ||
|
|
||
| XXX(俱乐部名称)领队把你设置为 领队 | ||
| receiver_id:球员 | ||
| verb:设置 | ||
| sender:领队 | ||
| Object_id:俱乐部 | ||
|
|
||
| 你已被 XXX(领队)移出俱乐部 | ||
| receiver_id:你 | ||
| verb:移出 | ||
| object:俱乐部 | ||
| sender:领队 | ||
|
|
||
| 收到 XXXX(赛事名称)的参赛邀请:同意 / 拒绝 | ||
| receiver_id:领队 | ||
| verb:收到 | ||
| object:邀请(一封邀请的文案) | ||
| sender:赛事组织者 | ||
|
|
||
| 文案:XXX(俱乐部名称)邀请你加入俱乐部 | ||
| receiver_id:球员 | ||
| verb:邀请 | ||
| Object:俱乐部 | ||
| sender:俱乐部 | ||
|
|
||
|
|
||
|
|
||
|
|
| @@ -1 +1,28 @@ | ||
| a = {u'status': 1, u'passport': u'', u'weight': 95.0, | ||
| u'area': {u'province': u'\u6d69\u5e02', u'country': u'\u6fb3\u5927\u5229\u4e9a', u'district': u'\u5f6c\u5e02', | ||
| u'city': u'\u535a\u5e02'}, u'gender': 0, u'id_card': u'440105198107105116', | ||
| u'birth': u'2006-09-19 08:20:06', u'token': u'27b55fcf10c54b5cb7da90e8d543d76f', u'height': 216.0, | ||
| u'phone': u'18873390760', u'op_id': 239559, u'roles': [], u'avatar_uri': u'http://www.baidu.com', | ||
| u'team': {u'name': u'\u516c\u79c0\u6885', u'short_name': u'\u8042\u5efa\u534e', | ||
| u'area': {u'province': u'\u91d1\u51e4\u5e02', u'country': u'\u4e9a\u7f8e\u5c3c\u4e9a', | ||
| u'district': u'\u5b81\u5e02', u'city': u'\u7ea2\u5e02'}, u'op_id': u'T376542', | ||
| u'id': u'5628778414028f4c0d405f0c', u'logo_uri': u'http://www..com/'}, | ||
| u'id': u'5628778414028f4c0d405f0f', u'nationality': u'\u5189\u6d9b', u'country_code': u'+852', | ||
| u'email': u'dylanninin@gmail.com', u'account_roles': [u'player'], u'name': u'zhang si'} | ||
|
|
||
|
|
||
| b = {'status': 1, 'avatar_uri': u'http://www.baidu.com', 'weight': 95.0, 'sender_id': '5628778414028f4c0d405f15', | ||
| 'height': 216.0, 'phone': u'18873390760', 'op_id': 239559, 'birth': '2006-09-19 08:20:06', | ||
| 'nationality': u'\u5189\u6d9b', 'country_code': u'+852', 'id': '5628778414028f4c0d405f0f', | ||
| 'competition_id': 'None', 'name': u'zhang si', 'roles': [], | ||
| 'area': {u'province': u'\u6d69\u5e02', u'country': u'\u6fb3\u5927\u5229\u4e9a', u'district': u'\u5f6c\u5e02', | ||
| u'city': u'\u535a\u5e02'}, 'gender': 0, 'id_card': u'440105198107105116', | ||
| 'token': '27b55fcf10c54b5cb7da90e8d543d76f', 'passport': u'', | ||
| 'team': {'name': u'\u516c\u79c0\u6885', 'short_name': u'\u8042\u5efa\u534e', | ||
| 'area': {u'province': u'\u91d1\u51e4\u5e02', u'country': u'\u4e9a\u7f8e\u5c3c\u4e9a', | ||
| u'district': u'\u5b81\u5e02', u'city': u'\u7ea2\u5e02'}, 'op_id': u'T376542', | ||
| 'id': '5628778414028f4c0d405f0c', 'logo_uri': u'http://www..com/'}, 'email': u'dylanninin@gmail.com', | ||
| 'account_roles': [u'player']} | ||
|
|
||
| print sorted(a.keys()) | ||
| print sorted(b.keys()) |