diff --git a/.gitignore b/.gitignore index 83bc9367c..2dfb574c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,13 @@ +# 隐藏文件 不被push + +*.pyc +*.db +*.log +*.wsgic + +mykv.db + +# _book/* node_modules/* diff --git a/0MOOC/01_01_win7.md b/0MOOC/01_01_win7.md new file mode 100644 index 000000000..99d0a840a --- /dev/null +++ b/0MOOC/01_01_win7.md @@ -0,0 +1,105 @@ +# windows setting + +win7配置 +只有win系统的电脑 嗯 要用win系统 就不能用Ubuntu了 怎么办 google search windows shell + +- [我的windows软件](https://www.zfanw.com/blog/windows-software-i-use.html) + +---------- + +## 1-iTerm2 或同级别强力 Shell 托管环境 ## + +- Babun +- ConEmu +- Tmux [官网](https://tmux.github.io/) [使用参考](http://foocoder.com/blog/zhong-duan-huan-jing-zhi-tmux.html/) + +---------- + + +刚开始想到的windows 下的Powershell + +- Youtube [Introduction to Windows PowerShell Part 1](https://www.youtube.com/watch?v=bEOq-S3veiA) +- [易學易用的 Windows PowerShell](https://www.microsoft.com/taiwan/technet/columns/profwin/28-monad.mspx) + +windows 的shell 不想用 去找替代 果然谷歌大法好 嗯 + +Get [Babun,一个开箱即用的 Windows Shell](http://blog.jamespan.me/2015/04/09/babun-the-shell/) +官网的下载速度比较慢 也不急 慢慢来 + +预测有许多坑 嗯 还需要填坑 + +- 0 [Babun官网](http://babun.github.io/) +- 1 [windows下搭建类linux环境](http://borghan.com/archives/windows-build-lunix-like-environment.html) + - 在修改中文乱码时 不知道如何进行指令操作了 unix不熟悉 返回default设置 + - 在修改的时候 一个根据提示在babun.bash中修改 + - 一个 直接 `vi .oh-my-zsh/custom/babun.zsh-theme` 进入vi 命令模式修改 +- 2 [配置](http://www.rxna.cn/post/wiki/babun-pei-zhi) +- 3 [Windows 的 SHELL 程序 Babun](http://my.oschina.net/blogshi/blog/268031) + +默认的shell 是 The zsh (with .oh-my-zsh) is the default babun’s shell. + +- bash shell /bin/zsh +- bash shell /bin/bash + + +带下载好 进行配置 + +### 总 ### + +- cd # cd /usr/spool/mail +- pwd : present working directory + +参考 [shell命令](http://www.lampweb.org/linux/2/2.html) + +- [整合ConEmu](https://www.52os.net/articles/windows-install-unix-like-command-tools.html) + + [下载ConEmu](https://github.com/Maximus5/ConEmu) + + 注意 需要将 `C:\CMD.babun\cygwin\bin\mintty.exe -t C:\CMD.babun\cygwin\etc\minttyrc` 修改 修改为自己babun所在的文件目录 + +> 在conEmu窗口右上角右键--settings--startup--tasks,点“+”号添加一个新task,task parameters留空,也按照babun官网介绍中配置图标等信息,在下面的commands中加入: +C:\CMD.babun\cygwin\bin\mintty.exe -t C:\CMD.babun\cygwin\etc\minttyrc + + + 意外 使用 babun 还是出现乱码 + +> conEmu 是window下的多标签命令行工具,可以方便的新建cmd、cmd admin、powershell、powershell admin多种命令行,设置很多,功能强大 + +---------- + +## Dash 或同类文档查询应用/服务/环境 ## + +Dash是OS X 系统下的 那么寻找替代 + +- [Velocity](http://velocity.silverlakesoftware.com/) + +---------- + +## 文本编辑器 ## + +- [Sublime Text 2](http://www.sublimetext.com/2) + +## 自动化 ## + +- [AutoHotkey](http://www.autohotkey.com/) + + +---------- + +## Babun ## + +Problem + +- 当我再次重启电脑 再次使用babun时 出现问题 zsh 2064 child_info_fork 嗯 google search got [this same problem](https://www.cygwin.com/ml/cygwin/2013-01/msg00020.html) + - 尝试reboot ok 没问题了 + + +---------- + +## VI ## + +- 指令模式 Esc +- 输入模式 a/A i/I o/O 6个键 任选一个 我选择i + + - [vi指令说明](http://www2.nsysu.edu.tw/csmlab/unix/vi_command.htm) +- :wq 保存 退出 +- :q 直接退出 +- :q 不保存退出 + +10/2/2015 diff --git a/0MOOC/01_02_ubuntu.md b/0MOOC/01_02_ubuntu.md new file mode 100644 index 000000000..31cc4092b --- /dev/null +++ b/0MOOC/01_02_ubuntu.md @@ -0,0 +1,112 @@ +# Go Ubuntu + +- Ubuntu 安装 双系统 +- Ubuntu 配置 python 开发环境 + +# Ubuntu 安装 # + +昨天你折腾了近8个小时的Ubuntu安装 终于将Ubuntu安装在你原系统为win7的笔记本上 并让笔记本成为了双系统 嗯Cool +这下 你可以折腾 学习unix + +## 触发 ## + +win上你让cli折腾的毫无体肤 你又每天买mac +太恼怒了 于是乎 你准备转向Unix系统试试 那么入门最好的就是Ubuntu + +Linus说: + +> - Microsoft isn't evil, they just make really crappy operating systems. +- Software is like sex: it's better when it's free. + +## 安装进程 ## + +- 准备阶段 + - 你确定使用USB U盘安装Ubuntu系统 所以你检测你的电脑能否是使用USB Boot[http://www.pendrivelinux.com/testing-your-system-for-usb-boot-compatibility/](http://www.pendrivelinux.com/testing-your-system-for-usb-boot-compatibility/) + - 你进行了检测 你的电脑是`esc`进入电脑的BOOT 设置 你将先前的`CD ROM` boot修改 为USB BOOT 这样你的电脑就能使用USB U盘安装Ubuntu系统 + - 你从官网下载好Ubuntu14.04.3LTS[http://releases.ubuntu.com/14.04.3/](http://releases.ubuntu.com/14.04.3/) + - **Universal-USB-Installer** 下载好之后你就参考[https://help.ubuntu.com/community/Installation/FromUSBStickQuick](https://help.ubuntu.com/community/Installation/FromUSBStickQuick) 安装USB installer [http://www.pendrivelinux.com/downloads/Universal-USB-Installer](http://www.pendrivelinux.com/downloads/Universal-USB-Installer) 这样你就可将下载好的Ubuntu ISO 镜像文件 `刻录`到U盘 并将U盘作为UBS driver 驱动安装系统了 + - **分割磁盘** 在win下 你使用计算机(右击计算机)的`管理`中`磁盘管理` 对空闲的F盘进行 `压缩卷` 分隔出40G 并`删除卷` +- 安装 + - [https://help.ubuntu.com/community/WindowsDualBoot](https://help.ubuntu.com/community/WindowsDualBoot) + - 使用USB启动安装 + - 进入 install ubuntu + - 你选择了中文安装 嗯 其实你需要英文的呀 后来你在系统里面自己改好 + - 之后 你选择`自动分区` + - 会显示你之前压缩的`40g`为空闲的状态 + - 因为win只能有3+1个主分区 所以压缩出来的那`40G`成为了`逻辑分区` 你对那`40G`分割形成Ubuntu挂载点的时候 需要选择`逻辑分区` 不要选择`主分区` 这样你进行为Ubuntu分区的时候 剩余的磁盘(40G的那)就不会显示不可用了 你在这里折腾了漫长时间的 + - 进行挂载点分配 (选择逻辑分区) + - /boot 101M ext4 + - / 20G ext4 + - / home 10G ext4 + - / usr 5G ext4 + - / tmp 3G ext4 + - swap 2G + - 一路安装就OK了 +- 可选择进入系统 + - 你安装好之后 开机直接进入的是WIN7 + - 接下来你就使用EasyBCD2.3 来修改了 你参考了 + - https://help.ubuntu.com/community/WindowsDualBoot 中启动部分Master Boot Record and Boot Manager + - http://neosmart.net/EasyBCD/ +- 修改之后 重新启动 你然可以进行选择 是进入Ubuntu还是进入Windows + +---------- + +## 总 ## + +- 安装 一定要从元知识开始 直接从Ubuntu官网进行选择 阅读 +- 还是回到 元知识 +- 还是回到 元知识 + +嗯 就这样 你可以开始Ubuntu的探索了 + +27 . 十月 2015 + +*** + +# Ubuntu Python 环境配置 + +从win转到Ubuntu 进行Pyhon学习 恩 尝试回顾走过的坑 + +## Go + +- 你首先参考 [在Ubuntu下配置舒服的Pyhon开发环境](http://xiaocong.github.io/blog/2013/06/18/customize-python-dev-environment-on-ubuntu/) + - 安装pip和virtualenv + + # 安装 pip + sudo apt-get install python-pip + # 安装 virtualenv + sudo pip install virtualenv + + - 安装 git 和 gitflow + - 安装 bash-it + - 安装 Sublime Text 2 + - 这个你之前折腾过 [ST2与Py环境配置](https://jeremiahzhang.gitbooks.io/omooc2py/content/0MOOC/SubPy.html) + +以上 你折腾了蛮长时间 因为unix系统还不熟悉 所以尝试起来 暂时挺不顺手的 尤其在安装python的时候 后来: + +- 大妈在2wd4的公开课中讲到了 pyenv 你就去尝试了 安装 + + git clone https://github.com/yyuu/pyenv.git ~/.pyenv + git clone https://github.com/yyuu/pyenv-virtualenv.git ~/.pyenv/plugins/pyenv-virtualenv # 配置插件 + - 然后你在 ~/.bash_profile 文件 添加 以下这些 干吗用的 猜测配置吧 你也不太清楚 + + # for PyEnv + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$HOME/.pyenv/bin:$PATH" + export PATH="$HOME/.pyenv/shims:$PATH" + eval "$(pyenv init -)" + + - 而后pyenv安装python就更方便了 + + pyenv install --list # 查看python所有版本 + pyenv virtualenv 2.7.10 # 安装python2.7.10 + +以上 pyenv 的确好使啊! + +环境配置好了 继续[Python Star Trek](https://jeremiahzhang.gitbooks.io/omooc2py/content/) + +## 总 + +Ubuntu 刚上手 有点不适应 慢慢适应就好了 接下来 不用win啦 开心 + +星期五, 30. 十月 2015 08:10下午 \ No newline at end of file diff --git a/0MOOC/01_system.md b/0MOOC/01_system.md new file mode 100644 index 000000000..d8d0a7308 --- /dev/null +++ b/0MOOC/01_system.md @@ -0,0 +1,3 @@ +# 系统 + +本 目录 下 是雷雨 的系统设置 diff --git a/0MOOC/02_01_learngit.md b/0MOOC/02_01_learngit.md new file mode 100644 index 000000000..a99d73e3a --- /dev/null +++ b/0MOOC/02_01_learngit.md @@ -0,0 +1,227 @@ +# Learn Git # + +- Version Control 版本控制 +- Git + - git basic 3大区 + - 配置工具 + - 本地与远程库联通 +- 常用命令 + + 版本回溯 + + Tag标签 commit + + branch 分支 +- 小技巧 +- Quick setup from Github +- SSH设置 + +## Version Control ## + +- 什么是[版本控制](https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control) + +> - Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. + +版本控制是记录文件内容变化的系统 可以帮助我在将来某一天都能查看以前某个文件某个版本的修订情况 + +- 举个例子 + - 从2015年9月1日开始 我每天写硕士论文 直到今天 嗯 我是在word中写的 + - 每一天写完一点 我都保存好了 但是还是在原来的文件在保存的 直接`ctrl+s` + - 假如我想知道我每天写的不用内容 我就得每日都另存为`ctrl+shift+s`一个版本 并命名时间2015-09-30版什么的 这样30天下来 我就有30个不同版本的硕士论文 查看我每一天所写的内容 + - 但有了版本控制系统 我只要用几个简单的命令 就可以记录那30个不同的论文版本 并且最终的文件 还是只有1个而已 我想回溯到30天内任何一天的写作内容 都可以 + +以上只是一个简单的例子 +版本控制还有很多有用之处呢 + +---------- + +## Git ## + +- 什么是[Git](https://git-scm.com/) +> Git is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency + +Git是一个免费且开源的[分布式版本控制系统](https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control#Distributed-Version-Control-Systems) + +![分布式版本控制](https://git-scm.com/book/en/v2/book/01-introduction/images/distributed.png) + +如图 分布式版本控制系统中 服务器中的文件库(远程库)如同"大树树根"一样 树枝A和B(本地电脑A-B)可以从"树根"获取远程库内容 并在本地修改之后 再次传输给“树根” 而A-B可以同时对同一个文件库中的内容进行操作 A-B也可以进行互相协作 + +### Git Basic ### + +3大区 + +- 工作区 working directory + - 你可以认为是 本地仓库 Local Repository + - 也是-本地目录-本地文件夹 + - 在其中可进行操作:如我在一个文件夹中建立一个md文档等等 +- 暂存区 staging area + - 这个区 可以看作是一个虚拟区 +- .git directory (Remote Reposiroty 远程仓库) + - 代表着 [Github](https://github.com/) 远程仓库 + +### 配置工具 ### + +- 注册github + - 用户名 + - 邮箱地址 + +首先下载Git全平台版 [http://git-scm.com](http://git-scm.com) + +- 配置所有本地仓库的用户信息 (相当于配置确认你本地计算的用户ID 可以与远程仓库进行联通) + - `$ git config --global user.name “username”` + - `$ git config --global email.address email_address` + - 说明 + + “username” 是 我在github上注册的 用户名 如 “john" + + email_address 是 我在github注册 用户名 时的 邮箱地址 +- 这样设置好 我就可以和Github 远程库 进行自由联通了 怎么联通 请继续看 + +### 本地与远程库联通 ### + +- 创建远程库 gopython + - creat a new repo 新建完库之后 注意出现的代码 尤其是ssh连接 + - copy the SSH of the new repo 也可以使用其url 后面会使用 +- 在本地文件夹中创建一个新的仓库 + + 我在D盘中创建一个文件夹名为 gopython + + 我右击gopython文件夹名 再点击`git bash here` 我就对gopython文件夹进行git bash 命令行操作了 + + `$ git init` git 默认此时branch为master + + 在【gopython文件夹】中添加 README.md和 SUMMARY.md 文件 +- 远程仓库设置 + - `git add --a` + - `git commit -m "1st commit"` + - `git remote add name [the gopyhon SSH 或url 任选一]`注意`[]`不需要的 `name` 自定义远程库的名字 + - `git pull name [the gopyhon SSH 或url 任选一]` # 现将github上远程库拉到本地 才能推送 + - `git push -u name master` 推送到.git 远程库 + +---------- + +## 常用命令 ## +1-代码库clone到本地文件目录 + + git clone {remote ssh or http link} +2-在本地目录添加远程库 + + git remote add remote_name {remote ssh or http link} +3-将远程库文件pull到本地 + + git pull remote_name {remote ssh or http link} +4-本地新增or修改的文件添加到working dir + + git add --a #添加所有 +5-添加之后 需要commit到staging area 暂存区 + + git commit -m "simple tag this commit" +6-推送到github + + git push remote_name {remote ssh or http link} +7-查看Local repo所有变化修改 + + git status +8-查看远程库 + + git remote + git remote rm remote_name # 删除远程库 + +### 版本回溯 ### +1-查看版本历史 + + git log + git log --pretty=oneline # 用一行来显示git 快照下来的commit历史或版本历史 + +2-版本回退 + + git reflog # git log + git reset --hard {使用git log or git reflog中的hash值 前7位} +3-查看所有commit历史 + + git hist master --all + +### Tag 标签 commit ### + +1-给刚commit的版本贴个标签 tag:好记 + + git commit -m "" + git tag v1 + git checkout v1 # 直接使用tag回退 v1^的话 表示上一个版本 commit +2-查看所有标签 + + git tag + +### git branch ### +1-**查看**所有本地repo中所有分支 默认为master + + git branch +2-**创建分支** + + git branch {branch_name} +创建分支后 HEAD(相当于一个指针) 就指向正在工作的本体分支了 branch_name的分支 +3-HEAD 指针回到原来的master分支 + + git checkout master +4-**新建并切换到分支** branch_test + + git checkout -b branch_test +5-**合并分支** 与master合并 + + git checkout master + git merge branch_test +6-**删除分支** + + git branch -d branch_test # 合并好之后 branch_test分支可以删除了 + +#### 分支管理 #### + + git branch # 产看当前所以分支 + git branch -v # 查看各个分支最后一次commit对象信息 + git branch --merged # 已合并分支 + git branch --no-merged # 尚未合并分支 + +---------- + +## 小技巧 ## + +文件目录中 修改.git/config中的命令**绰号** 如 + + [alias] + co = checkout + ci = commit -m + st = status + br = branch + pu = push origin master + pl = pull + ad = add --a + hist = log --pretty=format:'%h %ad | %s%d [%an]' --graph --date=short + type = cat-file -t + dump = cat-file -p + rf = reflog +设置好之后 `git st` 就代表 `git status` + +---------- + +## Quick setup from Github ## + +> create a new repository on the command line + + echo # test >> README.md + git init + git add README.md + git commit -m "first commit" + git remote add origin git@github.com:JeremiahZhang/test.git + git push -u origin master + +> push an existing repository from the command line + + git remote add origin git@github.com:JeremiahZhang/test.git + git push -u origin master + +> import code from another repository + +You can initialize this repository with code from a Subversion, Mercurial, or TFS project. + +当在Github上创建好一个Repo 就有4个提示 视而不见的后果就会造成很多弯路 花很多笨功夫 之前学习的时候 看这别人的教程 却忽略元知识 笨功夫啊 + +## SSH设置 +[SSH设置参考](https://help.github.com/articles/generating-ssh-keys/) +可以使用SSH 主要是在Ubuntu上使用git碰到了SSH链接无法使用问题 + + +10/20/2015 +10/22/2015 +星期五, 30. 十月 2015 07:20下午 + diff --git a/0MOOC/02_02_gitbook.md b/0MOOC/02_02_gitbook.md new file mode 100644 index 000000000..7c0ff3a6b --- /dev/null +++ b/0MOOC/02_02_gitbook.md @@ -0,0 +1,113 @@ +# Gitbook + +## what +是什么 + +> GitBook是一个基于 [Node.js](https://nodejs.org/en/) 的命令行工具,可使用 Github/Git 和 [Markdown](http://baike.baidu.com/view/2311114.htm) 来制作精美的电子书,GitBook 并非关于 Git 的教程。 + +为什么 + +- 制作教程 教会半年前的自己 教是最好的学 +- 获得反馈 +- 可输出多种格式文件PDF epub mobi + +## install + +- Ubuntu14.04 折腾安装 + + sudo apt-get update + sudo apt-get install nodejs-legacy # 直接装nodejs会出现问题 但使用 $gitbook 时 error “/usr/bin/env: node: No such file or directory” + sudo apt-get install npm + sudo npm install -g gitbook-cli + +参考 + +- [Gitbook-cli](https://github.com/GitbookIO/gitbook-cli#how-to-install-it) 使用可以关注这个 +- [Gitbook 安装](http://cowmanchiang.me/gitbook/gitbook/contents/install.html) + +## 现象 + +- gitbook 升级后, 关联 github 有问题 + +## 分析 + +- gitbook 服务升级, github 的 hook 没有对应升级 +- 虽然增加了 [webhook](https://help.gitbook.com/github/index.html#webhooks) 但是还是出现 github与gitbook不同步现象 即 push to github 而 gitbook 没有对应改变 或者 改变较慢(当然自己自己目前发现 我的add webhook过程见 [webhook setup](https://jeremiahzhang.gitbooks.io/gitbookguide/content/build/webhookssetup.html)) 只能用double push **增补** + + push to github + + push to gitbook + +## 问题 + +- 多个仓库的自动化同步,官方不支持了. + +## 方案 + +~ 作为不折腾会死星人,当然有姿势解决所有问题... + +### 双推 double push *增补 + +所谓双推 就是将本地working directory(or我称其为Local Repo)的内容 即push到 Github 仓库 又推送到 Gitbook + +#### 实现 #### + +一推 github + +- 我在Github Fork(相当于复制为我所用)[学员模板仓库](https://github.com/OpenMindClub/OMOOC.py) 我要基于该模板仓库建立自己学习开智编程课的教程 并改名为pybeginner 该远程库ssh为 `git@github.com:JeremiahZhang/pybeginner.git` +- 在我的电脑端本体建立文件目录(文件夹) 比如我建立的是名为pybeginner的文件夹(我称其为Local Repo 本地仓库) + + `git init` 初始化 以便与远程Repo连接 + + `git remote add hub git@github.com:JeremiahZhang/pybeginner.git` 此处我将Local Repo与Github Repo远程仓库建立连接 这里hub是我定义Github repo的名字 而ssh值我add的是代表**Github上的pybeginner仓库** + + `git branch --track hub master` + + `git pull hub master` 以上将我的 github repo pybeginner拉回(传送 如星际迷航中的瞬间传送)本地local repo + + `git merge hub` [合并分支](https://git-scm.com/book/zh/v1/Git-%E5%88%86%E6%94%AF-%E5%88%86%E6%94%AF%E7%9A%84%E6%96%B0%E5%BB%BA%E4%B8%8E%E5%90%88%E5%B9%B6) + + `git br -d hub` 删除分支 hub + + 以后我只要在local repo编辑内容 再push 到github 就OK了 + + 如 `git add README.md` 修改了README.md文件 通过 git add 添加到[staging area](https://git-scm.com/book/zh/v1/%E8%B5%B7%E6%AD%A5-Git-%E5%9F%BA%E7%A1%80#%E6%96%87%E4%BB%B6%E7%9A%84%E4%B8%89%E7%A7%8D%E7%8A%B6%E6%80%81) + + `git commit -m "modified README.md"` 提交到[git repository](https://git-scm.com/book/zh/v1/%E8%B5%B7%E6%AD%A5-Git-%E5%9F%BA%E7%A1%80#%E6%96%87%E4%BB%B6%E7%9A%84%E4%B8%89%E7%A7%8D%E7%8A%B6%E6%80%81) + + `git push -u hub master` 推到github + +> 已经推到了Github这是一推 下面需要另一推才能形成二推 + +二推 gitbook + +- Gitbook网页建立我的书籍 [Python Beginner Guide For Leiyu](https://www.gitbook.com/book/jeremiahzhang/pybeginner/details) 并找到 该书的 Git url 在书籍主页右下角 Setup 中 比如本书的git url为`https://git.gitbook.com/jeremiahzhang/pybeginner.git` +- $`git remote add book https://username:apitoken@git.gitbook.com/jeremiahzhang/pybeginner.git` 建立Local repo与gitbook repo的连接 其中username为gitbook的名称 apitoken是每个Gitbook账户特有的 可以在Account setting中找到 book为对应gitbook repo名字 可以自己命名的 +- `git push -u book master` 将Local repo 推送到 gitbook + +以上就是 双推 double push + +## Disqus 评论 *增补 + +> gitbook 还支持许多插件,用户可以从 [NPM](https://www.npmjs.com/) 上搜索 gitbook 的插件,[gitbook 文档](https://github.com/GitbookIO/plugin) 推荐插件的命名方式为: +> +- gitbook-plugin-X: 插件 +- gitbook-theme-X: 主题 +> +所以,可以通过以上两种方式来搜索 gitbook 的插件或者主题 + +作为一个非常流行的为网站集成评论系统的工具,Gitbook可以集成Disqus 便于和读者进行交流 好形成反馈 + +- 1 首先注册Disqus 参考[配置 Disqus](https://openmindclub.gitbooks.io/omooc-py/content/support/Disqus_Setup.html) +- 2 install disqus plugin + + `$ npm install gitbook-plugin-disqus -g` +- 3 增加book.json文件 代码如下 + + { + "plugins": ["disqus"], + "pluginsConfig": { + "disqus": { + "shortName": "XXXXXXX" + } + } + } +- 效果 + - 在每一个页面都出现一下Disqus评论窗口 + +![disqus01](https://raw.githubusercontent.com/JeremiahZhang/gitbookguide/master/_images/disqus-01.JPG) + +详细可参考我的 [折腾GitbookGuide](https://www.gitbook.com/book/jeremiahzhang/gitbookguide/details) + +## 进展 + +- 150317 大妈创建 +- 151007 雷雨增补 \ No newline at end of file diff --git a/0MOOC/02_03_doublepush.md b/0MOOC/02_03_doublepush.md new file mode 100644 index 000000000..93a2ebe2d --- /dev/null +++ b/0MOOC/02_03_doublepush.md @@ -0,0 +1,110 @@ +# 双推 + +## 背景 ## + +在完成第一周任务的时候 每天code之后 push到github 与 gitbook中 都要使用: + + $ git push hub master + $ git push book master + +来实现[双推](https://jeremiahzhang.gitbooks.io/pybeginner/content/toolssupport/gitbook.html) 有嫌麻烦 但是一直没去改变 程序员是需要精简的 + +触动 @小赖同学 在Issue中 发表 双推的代码 + +嗯是该动起来 不能再每日都如上面上面一样的双推了 + +## 方案 + +根据 @小赖同学 提供的[stackoverflow问答](http://stackoverflow.com/questions/849308/pull-push-from-multiple-remote-locations) 去解决 + +1.使用: + + git remote set-url origin --push --add + git remote set-url origin --push --add + +2.修改 本地目录(local repo)中的.git/config + +## 执行 + +1.从.git/config获得自己github与gitbook的远程库`ssh`或`http` url (每个github 远程库都有自己的url 每一本gitbook书籍也一样)因为之前已经设置了两个 remote : hub (代表github中的remote repo)与 book (代表gitbook书籍 remote repo) + + [remote "hub"] + url = git@github.com:JeremiahZhang/pybeginner.git + [remote "book"] + url = https://https://jeremiahzhang:apitoken@git.gitbook.com/jeremiahzhang/pybeginner.git + +**注**apitoken在gitbook[用户设置](https://www.gitbook.com/@jeremiahzhang/settings) + +2.根据`git remote set-url origin --push --add ` 添加 remote 设置 添加 gitbook remote repo `http` url to [hub]中(gitbook的`ssh`我没有尝试 因为之前实现[双推](https://jeremiahzhang.gitbooks.io/pybeginner/content/toolssupport/gitbook.html)的时候没有用) + + git remote set-url remote_name --push --add jeremiahzhang:apitoken@git.gitbook.com/jeremiahzhang/pybeginner.git + +**注** remote_name 必须是我现有的 remote (此处我选择自己的remote为 `hub` )可以在.git/config查看 也可以直接在git bash 中运行 git remote查看 + +添加之后 .git/config 中出现: + + [remote "hub"] + url = git@github.com:JeremiahZhang/pybeginner.git + fetch = +refs/heads/*:refs/remotes/hub/* + pushurl = https://jeremiahzhang:apitoken@git.gitbook.com/jeremiahzhang/pybeginner.git + +新增了 + + pushurl = https://jeremiahzhang:apitoken@git.gitbook.com/jeremiahzhang/pybeginner.git + +3.以防万一 直接在hub中添加自己原先的ssh + + git remote set-url remote_name --push --add git@github.com:JeremiahZhang/pybeginner.git + +同样.git/config的变化可查看: + + [remote "hub"] + url = git@github.com:JeremiahZhang/pybeginner.git + fetch = +refs/heads/*:refs/remotes/hub/* + pushurl = jeremiahzhang:apitoken@git.gitbook.com/jeremiahzhang/pybeginner.git + pushurl = git@github.com:JeremiahZhang/pybeginner.git + +4.修改.git/config的 alias 【alias就是git 命令的 昵称或绰号】 + + [alias] + co = checkout + ci = commit -m + st = status + br = branch + pu = push hub master + pl = pull + ad = add --a + hist = log --pretty=format:'%h %ad | %s%d [%an]' --graph --date=short + type = cat-file -t + dump = cat-file -p + rf = reflog +5.修改本地文件内容 `git ad` `git ci`后 `git pu` 就实现双推 立即化简了上面 需要两次 `git push` 了 + +## 修改 ## + +考虑到 hub 已经是一个 remote 名字 就修改 添加一个新的 remote origin + +1-直接在.git/config中添加如下代码: + + [remote "origin"] + pushurl = https://jeremiahzhang:apitoken@git.gitbook.com/jeremiahzhang/pybeginner.git + pushurl = git@github.com:JeremiahZhang/pybeginner.git + +2-[alias]中 修改push的绰号 + + pu = push hub master + +3-测试 git pu 成功 + +## 反思 ## + +- DRP记住 不要重复我自己 所以当发现重复复杂的行为的时候 立即思考 搜索没有有精简的办法 +- 其实一切以上只要修改.git/config文件中代码就行了 不过还是进行命令行的可操行和拓展性更强 就像之前群里说的160推 +- 看看接下来能不能实现160推 。。。推 拓展 big picture + +10/19/2015 9:51:59 PM 添加 + + + + + diff --git a/0MOOC/02_04_SubPy.md b/0MOOC/02_04_SubPy.md new file mode 100644 index 000000000..252cedb62 --- /dev/null +++ b/0MOOC/02_04_SubPy.md @@ -0,0 +1,169 @@ +# ST2与Py环境配置 + +- Sublime Text 2 插件使用和Python环境设置 + +> [Sublime Text](http://www.sublimetext.com/)是一套跨平台的文本编辑器,支持基于Python的插件。Sublime Text 是专有软件,可通过包(Package)扩充本身的功能。大多数的包使用自由软件授权发布,并由社区建置维护。 [详见wiki](https://zh.wikipedia.org/wiki/Sublime_Text) + +嗯 就是超级好用的文本编辑器 为什么就来看看英文版的说服力吧: + +> Sublime Text is a sophisticated text editor for code, markup and prose. +You'll love the slick user interface, extraordinary features and amazing performance. + +系统:win7 +之前就捣腾过 Py2.7.8 安装就不再进行了 + +- ST2 基础设置 +- Package Control(插件管理 下载) +- ST2 中配置Python开发环境 +- 我的常用快捷键 + + `ctrl+shift+p` 去install package + + `ctrl+alt+a` 对齐代码 + + 本来 `ctrl + b` 可以进行编译 现在设置 `f5` 进行编译 + + `ctrl+shift+c` Stop 终止程序 + +---------- + +## 基础设置 ## + +- Sublime Text 2 进入 菜单 `Preferences -> Settings - User` +- 在编辑器窗口中编辑 进行我的相关配置 + + `"draw_white_space": "all", // 显示空白字符, 比如 空格 tab` + `"font_size": 13.0, // 字体大小` + `"scroll_past_end": true, // 当文件到末尾时还能继续滚动` + `"trim_automatic_white_space": false // 关闭自动删除每行前后空格` + +---------- + +## Package Control ## + +[Package Control](https://packagecontrol.io/installation#st2)用来管理 Sublime Text 2 插件的插件. 当然需要第一个安装 + +- 利用快捷键 ctrl+` or 点击菜单栏进入 + + `View > Show Console` +- 直接去官网Copy 代码 paste the appropriate Python code for your version of Sublime Text into the console. + + import urllib2,os,hashlib; h = '2915d1851351e5ee549c20394736b442' + '8bc59f460fa1548d1514676163dafc88'; pf = 'Package Control.sublime-package'; ipp = sublime.installed_packages_path(); os.makedirs( ipp ) if not os.path.exists(ipp) else None; urllib2.install_opener( urllib2.build_opener( urllib2.ProxyHandler()) ); by = urllib2.urlopen( 'http://packagecontrol.io/' + pf.replace(' ', '%20')).read(); dh = hashlib.sha256(by).hexdigest(); open( os.path.join( ipp, pf), 'wb' ).write(by) if dh == h else None; print('Error validating download (got %s instead of %s), please try manual install' % (dh, h) if dh != h else 'Please restart Sublime Text to finish installation') +- 重启生效 菜单下有 `Preferences -> Package Settings` 选项 +- 以上亦可以手动设置 [参考官网](https://packagecontrol.io/installation#Manual) + +### 使用Package control ### + +[官网Usage](https://packagecontrol.io/docs/usage) 提供了相关描述 + +> To open the palette, press ctrl+shift+p (Win, Linux) or cmd+shift+p (OS X). All Package Control commands begin with Package Control:, so start by typing Package. +> The command palette will now show a number of commands. Most users will be interested in the following: + +- windows下`ctrl+shift+p`快捷键进入Package Control 命令行 +- 安装插件 + + 输入`install package` + + 输入我想要装的插件 如 [Alignment](http://wbond.net/sublime_packages/alignment) 选择安装 + * Alignment 这个插件用于对齐代码赋值语句 例如 + + var name = "sublime" + var version = "2.0.1" + var title = "sublime text" + * 按快捷键 可在`Preferences -> Package Settings -> Alignment -> Key Bindings defult` 中查看 + * 嗯 设置完之后 **请 restart 再使用快捷键** 这里我死后被坑了 一直找不到原因 后来 restart 就 ok 了 请注意 若设置好快捷键请 restart 嗯 Alignment 效果如下 + + var name = "sublime" + var version = "2.0.1" + var title = "sublime text" + +以上参考 [Sublime Text 常用插件和设置](https://wido.me/sunteya/sublime-text-packages-and-settings/) + +### 教训 ### + +> - 嗯 Package 之后 直接去 官网进行查询相关插件 安装使用效率会比查别人高很多 吸取了教训 +> - 以后 多用官方文档 +> - 比如 我要search markdown [PC markdown](https://packagecontrol.io/search/markdown) 就有许多插件了 安装可以直接看源文档的 更快捷高效 + +- 安装[Markdown Preview](https://packagecontrol.io/packages/Markdown%20Preview) 使用 一目了然 + +---------- + +## 配置Python开发环境 ## + +- 也就是如何在Sublime text 2中运行Python code + + `Tools -> Build System -> (choose) Python` + + 打开 `.py` Python代码文件 + + `Ctrl + B` 运行 or 编译 +- 如遇到死循环或中途需要停止程序 该如何操作 + + `Ctrl + Break` or `Tools -> Cancel Build` + + 我也可以自己设置 stop 快捷键 + + 进入菜单栏 `Preferences -> Key Bindings - User` + + 将如下代码贴入 + + {"keys": ["ctrl+shift+c"], "command": "exec", "args": {"kill": true} } + + 以上 就可以用 `ctrl+shift+c` 来代替 `Ctrl + Break` 终止程序运行 + +参考 [stack overflow](http://stackoverflow.com/questions/8551735/how-do-i-run-python-code-from-sublime-text-2) + +### 问题 ### + +- 我写了一段代码 如下 + + name = raw_input('Enter your name: ') + print 'Are you really', name, '?' + print 'Are you really' + name + '?' +- `Ctrl + B` 编译 +- 显示 error + + name = raw_input('Enter your name: ') + EOFError: EOF when reading a line + +> 代码中如果使用了input等函数进行交互的时候,直接使用Ctrl+B进行编译时,运行信息栏内无法输入交互信息,程序会提示报错。 + +- 解决 安装插件 + + [SublimeREPL](https://github.com/wuub/SublimeREPL) run an interpreter inside ST2 用来编译(运行)我的py程序 在工具栏 `Tools -> SublimeREPL -> python -> RUN current file` + + **优化之快捷键设置** SublimeREPL安装之后没有快捷键 每次运行程序必须用鼠标去点工具栏 增添效率 就给 SublimeREPL 添加快捷键 `Preferences -> Key Bindings - User` + + {"keys":["f5"], + "caption": "SublimeREPL: Python - RUN current file", + "command": "run_existing_window_command", "args": + { + "id": "repl_python_run", + "file": "config/Python/Main.sublime-menu" + } + } +> - 注意 在添加快捷键的时候 因为我之前添加过一次快捷键 所以 再次添加时 需要在上一个快捷键代码行的最后加上一个 comma 而且要是 英文的comma 否则会出错 系统也会提示 +> - 查看 SublimeREPL [所有Python方法的名称及id](https://github.com/wuub/SublimeREPL/blob/master/config/Python/Default.sublime-commands) 进行快捷键设置 我这里只设置了 `SublimeREPL: Python - RUN current file` 命令 + +- 果效 + - `f5`键 可以快速进行调试和交互的程序了 + - 例如 `f5`键 运行起初的 `raw_input` 那段代码 可以进行交互 如图所示 +![REPL](https://raw.githubusercontent.com/JeremiahZhang/pybeginner/master/_image/0-REPL.JPG) + +---------- + +### 其他插件 ### + +- [SublimeCodeIntel](https://github.com/SublimeCodeIntel/SublimeCodeIntel) +> SublimeCodeIntel 可以支持代码的自动补全以及成员/方法提示等功能,安装此插件后,Sublime Text 2就有点IDE的感觉了。 + +- [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-for-ST2) +> SublimeLinter 是用来在写代码时做代码检查的,可以检查Python代码是否符合[PEP8](https://www.python.org/dev/peps/pep-0008/)的要求。 + +这两个可能太高级 嗯 入门阶段 暂时先不用这两个 虽然也装了 + +参考 [基于Sublime Text搭建Python IDE](http://loosky.net/2967.html) + +---------- + +## 总 ## + +当完成以上配置 并记录好后 才发现 大妈的模板是 + +- 现象 +- 问题 +- 分析 +- 方案 +- 执行 + +嗯 我看看自己的过程 总是没有大的框架在那 然后 走到随性而走 不好 得补这个缺 + +---------- + +## 进展 + +- 10/8/2015 雷雨创建 \ No newline at end of file diff --git a/0MOOC/02_05_tmux.md b/0MOOC/02_05_tmux.md new file mode 100644 index 000000000..cdc923146 --- /dev/null +++ b/0MOOC/02_05_tmux.md @@ -0,0 +1,188 @@ +# Tmux Explore + +雷雨开始了 Tmux 之旅 + +- 系统 Ubuntu14.04 LTS +- 系统终端 Terminal + +什么是Tmux? 终端多用利器 + +> tmux is a terminal multiplexer! +What is a terminal multiplexer? It lets you switch easily between several programs in one terminal, detach them (they keep running in the background) and reattach them to a different terminal. And do a lot more. See the manual. + +## 背景 + +雷雨在4周学习python 完成任务时 会使用较多的终端 然切换之 常头疼烦乱 在想要的多个终端之间找来找去 + +雷雨后来尝试直接使用 Ubuntu 自带终端的多个窗口 发现 当前还是只能一个窗口 + +然 大妈 在公开课 演示时 多个窗口 直接在当前显示 只要快捷键就可以直接切换 太方便了 + +雷雨在3wd4课之时 不能再视而不见了 完成了 4w的任务之后 雷雨尝试去 探索以下 Tmux + +恩 雷雨虽然之前 探索了解了下 但是 最终还是徘徊在 他人二手资料上 好的 雷雨这回 要回到 tmux manunal 来开始此次 Tmux 之旅了 + +## 安装 + +雷雨来到了 [tmux 网站](https://tmux.github.io/) 里面有提示下载 雷雨直接去看了 Github上 [tmux](https://github.com/tmux/tmux) 的README 说明 + +### 尝试1 +雷雨尝试了 Readme中的: + + $ git clone https://github.com/tmux/tmux.git + $ cd tmux + $ sh autogen.sh # 在这里就出错了 + $ ./configure && make + +错误提示: + + autogen.sh: 15: autogen.sh: aclocal: not found + aclocal failed +没有发现aclocal 没去管 + +### 尝试 2 + +后来 雷雨直接 使用 + + sudo apt-get install tmux + +来安装的 发现安装的时 1+的版本 + +恩 想装[tmux2.0](https://gist.githubusercontent.com/P7h/91e14096374075f5316e/raw/6c7eec878900d0e4e196f556360d1b9ceaf523bb/tmux_install.sh) 使用该方法 + + # tmux v2.0 installation steps for Ubuntu 14.04 (Trusty Tahr) + tmux -V + sudo apt-get update + sudo apt-get install -y python-software-properties software-properties-common + sudo add-apt-repository -y ppa:pi-rho/dev + sudo apt-get update + sudo apt-get install -y tmux + tmux -V + + # tmux v1.9 installation steps for Ubuntu 14.04 (Trusty Tahr) + sudo apt-get update + sudo apt-get install -y python-software-properties software-properties-common + sudo add-apt-repository -y ppa:pi-rho/dev + sudo apt-get update + sudo apt-get install -y tmux=1.9a-1~ppa1~t + tmux -V + + # On Ubuntu 12.04 (Precise Pangolin), step 5 would be: sudo apt-get install -y tmux=1.9a-1~ppa1~p + # On Ubuntu 13.10 (Saucy Salamander), step 5 would be: sudo apt-get install -y tmux=1.9a-1~ppa1~s + +这下 + + tmux -V #查看版本 为 2.0 + +后来雷雨 tmux 命令 无法进入 tmux window 解决方案: + + tmux attach + pgrep tmux # 这里键入之后 会显示一串数字 雷雨的是16469 然后使用下面一行命令 数字需要使用 pgrep tmux 获得的数字 + /proc/16469/exe attach + +*** + +## 配置 + +雷雨安装好 tmux 后 直接在Terminal中 使用: + + tmux + +进入 tmux window + +但是 之前参考他人教程 可以使用 快捷键 CTRL + B 然后 “ 可以水平分割窗口 雷雨尝试了 没有反应呀 + +参考 [Ubuntu manunals](http://manpages.ubuntu.com/manpages/trusty/en/man1/tmux.1.html) : + +> Specify an alternative configuration file. By default,tmux loads the system configuration file from /etc/tmux.conf, if present, then looks for a userconfiguration file at ~/.tmux.conf. + +雷雨没有找到 该文件 OK 雷雨想 要么自己新建一个 要么参考使用他人配置好的 雷雨选择了后者 + +参考 [tony/tmux-config](https://github.com/tony/tmux-config) + +下载 + + git clone https://github.com/tony/tmux-config.git ~/.tmux + +复制文件到雷雨的 Home: + + ln -s ~/.tmux/.tmux.conf ~/.tmux.conf + +这样 雷雨tmux 有了 .tmux.conf 配置文件 + +## 使用 + +### 进入tmux + +终端中 雷雨直接键入 + + tmux + +进入 tmux window + +![tmux001](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/Tmux_001.jpg) + +### 查看 keyboard shortcuts + +因雷雨 使用了 他人的配置文件 .tmux.conf 所以需要根据该配置文件来 + +- Control + a before any command +- Ctrl + a then ? # 查看 绑定的快捷键 + +查看到了 bind key + +![tmux002](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/Tmux_002.jpg) + +*** + +## 体验 + +### 分屏 + +1 + +雷雨查看了配置的快捷键之后 尝试 分屏 + +同时 Ctrl + a 然后 “ 分屏 如下 + +![tmux003](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/Tmux_003.jpg) + +现在 雷雨的光标在 下面 分割的一个 window 恩 如何回到上面的 window 呢 + +2 + +键盘直接 Ctrl a 之后 鼠标滚轮一滚就到上面的window去了 + +雷雨 光标在 上面一个window之后 相 竖直分一个 window 键盘直接 + +Ctrl a 然后 % + +效果 + +![tmux004](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/Tmux_004.jpg) + +雷雨看出 直接 在上面一个 window 中 水平分出了一个 window(原来是在光标所在的屏分出来的) + +3 + +现在分成3个window之后 如何切换呢? 雷雨发现 这样 + +键盘 Ctrl a 之后 直接使用 键盘 上下左右 箭头就可以切换了 + +4 + +雷雨想回到最初的一个window 恩 + +Ctrl a 之后 ! (英文输入法下的哦) + + +*** + +## 总 + +以上是雷雨 tmux 的初级体验 +之后的tmux 需要更多的阅读官方doc + +星期二, 10. 十一月 2015 05:47下午 +星期二, 10. 十一月 2015 05:40下午 + diff --git a/0MOOC/02_06_qiniu.md b/0MOOC/02_06_qiniu.md new file mode 100644 index 000000000..151acbec1 --- /dev/null +++ b/0MOOC/02_06_qiniu.md @@ -0,0 +1,72 @@ +# 七牛云储同步 + +- Ubuntu 14.04 LTS + +哈利路亚 + +## 背景 + +咦 雷雨的这次教程中图片 都放到了 Github 大妈提醒 放那里合适么 + +嗯 不合适 雷雨发现了云储 先用 七牛啦 [七牛](https://portal.qiniu.com/) + +## 使用 + +- 注册 可以用 Github 登录 +- 新建空间 +- 直接上传图片 恩 然后使用外链 + - 再使用 markdown 插入图片链接 就OK了 + +## 配置同步 + +- 下载文件 [命令行配置同步](http://developer.qiniu.com/docs/v6/tools/qrsync.html) +- Ubuntu 下载 tar.gz 包 + - 直接解压即可 + - 在 /HOME 新建 qiniu 文件目录 + - 将解压文件 放入 qiniu 文件 +- 恩 这里得配置 环境 添加路径 在.bash_profile 中 为了可以使用同步qrsync 之命令 + + # for qiniu sync + export PATH="$HOME/qiniu:$PATH" + + 终端中: + + source .bash_profile # 在该文件目录下哦 + +- 建立 conf.json 放入 qiniu 文件夹 + + { + "src": "/home/jeremiahzhang/Pictures/ImgofBlog", # 这里是我的同步文件目录 + "dest": "qiniu:access_key=&secret_key=&bucket=<这放空间名 不要<>哦>", + "deletable": 0, + "debug_level": 1 + } + +< AccessKey > +< SecretKey > + +以上两项 需要去7牛 获取 + +- 继续配置 在终端中 + + chmod +x qrsync + +- 之后 你就可以 打开 qiniu文件 在该文件目录下 使用命令行 + + qrsync conf.json # 进行同步 + +## o(∩∩)o + +可以将图片直接用命令行同步到 七牛 螺 + +星期四, 12. 十一月 2015 10:13下午 + + + + + + + + + + diff --git a/0MOOC/02_07_zsh.md b/0MOOC/02_07_zsh.md new file mode 100644 index 000000000..37611a117 --- /dev/null +++ b/0MOOC/02_07_zsh.md @@ -0,0 +1,36 @@ +# zsh started + +Just started should dive in + +- install zsh +- configure zsh +- oh-my-zsh + +begin with + +- [Installing ZSH](https://github.com/robbyrussell/oh-my-zsh/wiki/Installing-ZSH) +- [Getting started with ZSH on Ubuntu (for technotards) ](Getting started with ZSH on Ubuntu (for technotards) ) This is for beginner + +KO just started + +# plugin + + vim ~/.zshrc + +modify this + + plugins=(git ubuntu) + +add ubuntu plugin because git is the default plugin in my zsh + +[ubuntu plugin readme file ](https://github.com/robbyrussell/oh-my-zsh/blob/master/plugins/ubuntu/readme.md) + + +dive in + +- change themes +- [x] plugin +- and alias +- and so on + +Saturday, 14. November 2015 05:14PM diff --git a/0MOOC/02_08_st3.md b/0MOOC/02_08_st3.md new file mode 100644 index 000000000..71f0cf654 --- /dev/null +++ b/0MOOC/02_08_st3.md @@ -0,0 +1,5 @@ +# Ubuntu ST3 中文输入 + +- http://wuhao.pw/index.php/archives/140/ +- http://mydin.net/37.html +- http://jasonye.sinaapp.com/2013/07/17/solve-sublime-can-not-input-chinese-at-ubuntu/ diff --git a/0MOOC/02_config.md b/0MOOC/02_config.md new file mode 100644 index 000000000..33494a7f7 --- /dev/null +++ b/0MOOC/02_config.md @@ -0,0 +1,3 @@ +# 开发配置 + +这里是 雷雨 Pyhon Enterpise 启航前的 一些配置 diff --git a/0MOOC/CLI.md b/0MOOC/CLI.md deleted file mode 100644 index d1eaba47d..000000000 --- a/0MOOC/CLI.md +++ /dev/null @@ -1,12 +0,0 @@ -# CLI 私人教程 - -## 背景 - -## 安装 - -## 配置 - -## 使用 - -## 体验 - diff --git a/0MOOC/DISQUS.md b/0MOOC/DISQUS.md deleted file mode 100644 index 2b1b53954..000000000 --- a/0MOOC/DISQUS.md +++ /dev/null @@ -1,12 +0,0 @@ -# DISQUS 私人教程 - -## 背景 - -## 安装 - -## 配置 - -## 使用 - -## 体验 - diff --git a/0MOOC/README.md b/0MOOC/README.md index 4c4e0274e..22112be12 100644 --- a/0MOOC/README.md +++ b/0MOOC/README.md @@ -1,6 +1,8 @@ -# 0MOOC -~ 此目录收集, 课程进入时的体验 - -## 进展 - +# 0MOOC +~ 此目录收集, 雷雨 pyhon 启航 准备 + +恩 启航还有返航 返航后还是需要 慢慢整理 配置的 + +## 进展 + - 150924 大妈创建 \ No newline at end of file diff --git a/0MOOC/py.md b/0MOOC/py.md deleted file mode 100644 index 50fb45be9..000000000 --- a/0MOOC/py.md +++ /dev/null @@ -1,12 +0,0 @@ -# Python 私人教程 - -## 背景 - -## 安装 - -## 配置 - -## 使用 - -## 体验 - diff --git a/1sTry/0wd0-task-42-code-fundamental-problem.md b/1sTry/0wd0-task-42-code-fundamental-problem.md new file mode 100644 index 000000000..4350b5d03 --- /dev/null +++ b/1sTry/0wd0-task-42-code-fundamental-problem.md @@ -0,0 +1,81 @@ +# 那42行 + +## 现象 ## + +- 根据 [极简Python上手导念](http://wiki.zoomquiet.io/pythonic/MinimalistPyStart) 中的一页解说了 Python 代码 + - ![42 coder](http://wiki.woodpecker.org.cn/moin/ZqQuickIntoPy?action=AttachFile&do=get&target=coffeeghost-q-in-py.png) +- 会解说了 但是如何去运用 将其功能化呢?[代码解说](https://github.com/JeremiahZhang/pybeginner/blob/master/_src/om2py0w/0wex0/main.py) +- 这42行代码: + + 包含80%左右的 py 代码常见情景 + + 常见情景是什么? + + main 函数 调用 循环 print 等等 + + 是不是类似 欧式几何 中 那些 定义 公设 公理 + + 用**常见情景**(定义 公设 公理) 去 实现(证明)其他功能(命题)呢? + +---------- + +## 问题 ## + +任务是 将那42行 功能化 + +- 那么如何去功能化? ( 用怎么去证明其他命题思路去想) + +## 分析 ## + +- 什么功能化? + + 总要确定命题 才能证明吧 + + 寻找 探寻 +- 如何实现? + + 参考 运用 那 42行代码 + +## 方案 ## + +- 构思功能 +- 去实现功能 +- 教程 + + 功能设计 + + 技术要点 + + 实现难点 + + 涉及知识 + + etc + +---------- + +## 执行 + +- 构思功能 + + 到底是什么功能呢? + + 视野不广 去扩宽 + + 知识不够 去学习 + + github 是个好地方 小程序功能去找 find this repo [Show me the code](https://github.com/Show-Me-the-Code/python) + + OK show me the code as Linus says + + 题目:[0000](https://github.com/JiYouMCC/python-show-me-the-code/tree/master/0000) + > 第 0000 题:将你的 QQ 头像(或者微博头像)右上角加上红色的数字,类似于微信未读信息数量那种提示效果。 类似于图中效果 +- 构思过程 + + 思维导图 +- 执行 + + code [the code](https://github.com/JiYouMCC/python-show-me-the-code/blob/e0c7c1c37ccba38671078e0b0ff6238992a11499/0000/0000.py) + + Python Image Library install + + choose and install [Pillow 3.0.0](https://pypi.python.org/pypi/Pillow/2.2.1) 由此参考回答[Installing PIL with pip](http://stackoverflow.com/questions/20060096/installing-pil-with-pip)对于小白的我 这个需要使用命令行 我用babun + + $ pip install Pillow + + 安装之后如何使用呢?因为只有`from PIL import Image`是没有用的 error 显示找不到图片 搜了好多 最后在此处 [The Image Module](http://effbot.org/imagingbook/image.htm) 发现答案 【先从help -> google key word is very important】 + + 没有在当前工作目录中 放入所用的图片【掉入的坑 好久才爬出来 适当休息】 + + 图像是在当前工作目录中的 + + save 的也是 + + 关于Image可以在[这里](http://effbot.org/imagingbook/image.htm)找到help + + run successed as below + + ![narresult](https://raw.githubusercontent.com/JeremiahZhang/pybeginner/master/_src/om2py0w/0wex1/nar_result.png) ++ 收获 + + 库的安装 使用 及其查询 + + PIL Python Image Library Pillow + + the image module + + help 文档检索为先 ++ 总 + + 在运行别人的代码 + + 发现问题 自己理解 + + 但是如何去自行编码独立完成呢? 需要一步步积累 + +代码:[https://github.com/JeremiahZhang/pybeginner/tree/master/_src/om2py0w/0wex1](https://github.com/JeremiahZhang/pybeginner/tree/master/_src/om2py0w/0wex1 "今日代码") + +10/11/2015 3h \ No newline at end of file diff --git a/1sTry/0wd1-reflection.md b/1sTry/0wd1-reflection.md new file mode 100644 index 000000000..2542f60a2 --- /dev/null +++ b/1sTry/0wd1-reflection.md @@ -0,0 +1,30 @@ +# 从源头开始 # + +- if python then coding +- 从 [官方python tutorial](https://docs.python.org/2/tutorial/) 以前总是二手的教程 blog开始 最好的教程 应该是源头的教程 官方的 + + chapter 1-3 coding and end reading + + codes is in my pybeginner + + 难度不大 就是在练习技术 + + 掌握 核心数据类型 + + numbers + + strings + + unicode strings + + list + + time 近 6个蕃茄 +- summary + + 挑战级别2颗星 和昨天的那个 show me the code 0000 题相比 + + 大多数在那42行代码中运行到了 只是在填充数 构建python结构框架 + + 接下来 + - [x] py tutorial + - [x] statements `if` `for` `break` `continue` `pass` + - [x] `else` clauses On Loops + - [x] `range()` function + - [x] `def` function + - argument + - [x] coding style + - [] 完成简单工程行为 + +代码:[https://github.com/JeremiahZhang/pybeginner/tree/master/_src/om2py0w/0wex2](https://github.com/JeremiahZhang/pybeginner/tree/master/_src/om2py0w/0wex2) + +10/12/2015 9:39:25 PM +10/13/2015 9:18:46 PM 完成 以上3个x diff --git a/1sTry/0wd2-New-things-to-learn.md b/1sTry/0wd2-New-things-to-learn.md new file mode 100644 index 000000000..89f0ed5c4 --- /dev/null +++ b/1sTry/0wd2-New-things-to-learn.md @@ -0,0 +1,76 @@ +# New Things to Learn # + +- coding through [2.7.10 tutorial](https://docs.python.org/2/tutorial/) +- Something Learned + - `if` statement + - `for` statement + - the `range()` function + - `range(5, 10, step)` 值得注意的是取数乃是这样的 范围[ ) step 默认为1 + - `range()` 与list的结合 + + 简化为:`for i, v in enumerate(['Mary', 'had', 'a', 'litter', 'lamb']):` + - `break` and `continue` statement + - break 是哪里 打破 最近的 Loops + - continue 又是继续的哪里 最近的 Loops + - `else` 不要受制于 matlab 的`else` Python的`else`可用于Loops + - `pass` statement + - Define functions 与c中的类似 + + default argument values + + Keyword arguments 这个比较鲜活 + +## 新知 ## + + def cheeseshop(kind, *arguments, **keywords): + print '-- do you have any', kind, "?" + print "-- I am sorry, we're all out of", kind + for arg in arguments: + print arg + print '-' * 40 + keys = sorted(keywords.keys()) + for kw in keys: + print kw, ":", keywords[kw] + + cheeseshop("limburger", "it's very runny, sir.", "It's really very, Very runny, sir.", shopkeeper = 'Michael Palin', client = 'John Cleese', sketch = 'cheese shop sketch') + +- `*name01` 与 `**name02` 先后顺序 +- 涉及 dictionary 还未细看 下一步着手 +- `keys = sorted(keywords.keys())` +- 看输出结果 + + -- do you have any limburger ? + -- I am sorry, we're all out of limburger + it's very runny, sir. + It's really very, Very runny, sir. + + client : John Cleese + shopkeeper : Michael Palin + sketch : cheese shop sketch + + 明白 + + 但是为什么呢? + + kind 参数 只能是第一个 "limburger" + + *arguments 参数 "it's very runny, sir.", "It's really very, Very runny, sir. + + **keywords 参数 其他剩余 + + `keys = sorted(keywords.keys())` 不明白 进一步需要了解 Dic 再回来 看这个 confused codes +- When ? +- How to use them? + +## Next## + +- [x] dictionary dive in 官方help and [问题](http://stackoverflow.com/questions/17677523/python-keyword-output-interpretation) +- [x] 4.7.3 Arbitrary Argument Lists +- [x] 4.7.4 Unpacking Argument Lists +- [x] 4.7.5. Lambda Expressions +- [x] 4.7.6. Documentation Strings +- [x] 4.8 Intermezzo: Coding Style + +---------- + +## SUM ## + +- Coding 下去 +- 7个蕃茄学习 1个蕃茄整理 +- 反思 + + 代码练了 没有去完成一个项目 自定义的项目 发现还是不怎么入脑 + + 那么的思考 完成一个 怎样有趣的项目 如同show me the code的 learn by doing + + 这么个循序渐进额可以 + +[今日代码](https://github.com/JeremiahZhang/pybeginner/tree/master/_src/om2py0w/0wex2) \ No newline at end of file diff --git a/1sTry/0wd3-step-by-step-4_7-8.md b/1sTry/0wd3-step-by-step-4_7-8.md new file mode 100644 index 000000000..e3745d6a8 --- /dev/null +++ b/1sTry/0wd3-step-by-step-4_7-8.md @@ -0,0 +1,40 @@ +# step by step # + +- [x] dictionary dive in 官方help and [问题](http://stackoverflow.com/questions/17677523/python-keyword-output-interpretation) + + `dict{}` + + 其中 key 顺序无关紧要 + + 也有长度 + + 排序好玩 + + `key` 的熟悉 + + dict.viewkeys + + dict.viewvalues + + 可删 +> 对dictionary熟悉了 只是在什么时候用 就不太清楚了 + +- [x] 4.7.3 Arbitrary Argument Lists +- [x] 4.7.4 Unpacking Argument Lists +- [x] 4.7.5. Lambda Expressions +- [x] 4.7.6. Documentation Strings +- [x] 4.8 Intermezzo: Coding Style + - 使用4-space来缩进 而非tabs 得改 + - wrap lines 代码行长度不要超过79character + + 就像matlab代码中 有一条竖线来提醒 要将代码控制在竖线左边 + + 好读代码 + - [x] 空白行分割 函数 class 在函数内部等等 + - [x] comment 注释 + - 使用docstrings 在 4.7.6中提到 + + 第一行 summary 精简 + + 第二行 空行 + + 第三行 writting + - [x] 在 `=` `+` etc operators 与 `,` 之后使用空格 + - 这个已成习惯 + - 如 `a = 1`, `b = a` + - 只是在用一个结构中 不需要就需注意了 + - `a=f(1,2)+g(3,4)`. + - [] Name your classes and functions consistently + - Always use self as the name for the first method argument + - [] Don’t use fancy encodings if your code is meant to be used in international environments. Plain ASCII works best in any case + +代码在此 [2015-10-14 codes](https://github.com/JeremiahZhang/pybeginner/tree/master/_src/om2py0w/0wex3) + +10/14/2015 10:40:03 PM 4+1蕃茄 but 3h \ No newline at end of file diff --git a/1sTry/1wd5_go_google.md b/1sTry/1wd5_go_google.md new file mode 100644 index 000000000..e0f0ec02f --- /dev/null +++ b/1sTry/1wd5_go_google.md @@ -0,0 +1,133 @@ +# Google search + +## 触发 ## + +你在折腾自动登录网页的时候 实践了 [实践-1 自动打开浏览器 进行google搜索](https://jeremiahzhang.gitbooks.io/omooc2py/content/1tDoing/01Gowechat.html) 对吧 + +那次 你的例子只能通过 shell 调用脚本 + + python go_google.py +然后搜索的关键词 只是github + +这次是你对这个的扩展 + +- 你不止搜索 github 一个关键词 需要多个 + - 中文关键字能行么? + +---------- + +## 想法 ## + +- 你可以在调用脚本时将外部数据(关键字)传递给脚本 让其使用 +- 中文关键字呢?你可以用“utf-8”编码解决吧 + +---------- + +## 尝试 ## +### 1.传递外部数据(关键字) ### +你可以这样 + + python go_google.py keywords01 keywords02 ... + +让go_google.py脚本 使用传递过去的参数 keywords01 keywords02 +这个你就要使用 `sys` module, `sys.argv[]` + + your_keywords = sys.argv[1:] +这样你的关键词 `keywords01` `keywords02` `...`就可以被脚本使用了 注意 `sys.argy[0]` 是你的脚本全名 `go_google.py` + +这样你就得把关键词自动键入到google搜索栏中 + +代码 go_google.py: + + # -*- coding: utf-8 -*- + import sys + from selenium import webdriver # 这里使用了selenium 模块 + from selenium.webdriver.common.keys import Keys + + print """ Example 1: + ========== + + * open a new Firefox browser + * load the google homepage + * search for "your_keywords" + + """ + your_keywords = sys.argv[1:] + + browser = webdriver.Firefox() # open your firefox browser + + browser.get("http://www.google.com") # go to google webpage + assert 'Google' in browser.title # confirm that title has “Google” word in it + + search_elem = browser.find_element_by_name('q') # name = "q" is in the search field of the google webpage source code 相当于点击进入了 google 搜索栏 + + for keywords in your_keywords: + search_elem.send_keys(keywords + " ") + + search_elem.send_keys(Keys.RETURN) # search keyword Keys.RETURN like keyboard enter or Go + + print "Mission Completed!" + +以上可以实现 英文关键字 google 搜索了 +shell调用: + + python go_google.py python tutorail + +效果: + +1.自动打开firefox浏览器 +2.自动进入 www.google.com +3.自动搜索 关键字 `python` `tutorial` +如图: +![](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/google.JPG) + +### 2.中文关键字搜索 ### +1.**问题:**直接调用脚本 + + python go_google.py 编程 教程 +出错 原因 编码问题 + + UnicodeDecodeError: 'utf8' codec can't decode byte 0xbd in position 0: invalid start byte +2.方案1:你可以全部使用UTF-8编码 之前在完成第一周任务-小小交互系统的时候 就已经知道了吧 [week_1_日记交互系统](https://jeremiahzhang.gitbooks.io/omooc2py/content/2nDev/week01_interact.html) 添加 + + # -*- coding: utf-8 -*- + import sys + reload(sys) # 必须 reload + sys.setdefaultencoding('utf-8') # 默认编码 +使用utf8 can't decode + + UnicodeDecodeError: 'utf8' codec can't decode byte 0xb1 in position 0: invalid start byte +3.**分析**:`for keywords in your_keywords:`中的 `keywords`的类型是 str 但是 `0bx1` 这种格式的 +search 调试 修改 + +`search_elem.send_keys(keywords + " ")` 这部分 猜测其中 `keywords` 编码出现问题 尝试 + + search_elem.send_keys(unicode(keywords) + " ") + search_elem.send_keys(keywords.decode("utf-8") + " ") + search_elem.send_keys(keywords.encode(sys.stdout.encoding) + " ") + search_elem.send_keys(keywords.decode("utf-8") + " ") + search_elem.send_keys(keywords.decode() + " ") + search_elem.send_keys(unicode(keywords, "ISO-8859-1").encode("utf-8") + " ") +各种编码试过 都出现 同上的 `UnicodeDecodeError` +你不要再花笨功夫了 好好去理解编码的问题吧... + +有点累了 你要 KEEP CALM AND GO SLEEPING byebye + +10/23/2015 9:33:41 PM + + + + + + + + + + + + + + + + + diff --git a/1sTry/InOutputStar.md b/1sTry/InOutputStar.md new file mode 100644 index 000000000..67550201c --- /dev/null +++ b/1sTry/InOutputStar.md @@ -0,0 +1,185 @@ +# Input and Output Star + +1 +雷雨 今天 乘坐 Enterprise 来到 Input and Output Star 咦 这是一个怎样的星球呢? + +输入于输出么 就是 + +- 你输入 python 接受 +- 你要求输出 python 就输出 + +包括: + +- 输出格式化 +- 读写文件 + - 文件对象方法 + - json + +## 输出格式化 之旅 +#### 1 雷雨在这里了解到 目前遇到过三种方法 来写 values + +- expression statements +- ==print== statement +- ==write()== 函数 + +#### 2 雷雨 在这个星球上 了解到 两种方式来 format 自己的 output + +- the first way is to do all the string handling yourself 雷雨不明白这个到底是什么意思 让 string 自己处理自己 原来 是这样 + - using string slicing and concatenation operations you can create any layout you can imagine 使用 string 切片 然后再 连起来 恩 就像 雷雨以前吃的葫芦串 串起来 +- use ==str.format()== 方法 两种 将 values 值 转化为 strings的方法 + - ==repr()== 函数 这个将 alues 转化为 编译器去读的 + - ==str()== 函数 这个 将 values 转化为 雷雨 容易读 和理解的 + +哦 什么是 values 阿 恩 比如 strings numbers / structures(lists dictionaries) + +雷雨看到的例子: + + >>> s = 'hello, world.' # string + >>> str(s) + 'hello, world.' # 看到两者的区别了么 雷雨能读的 + >>> repr(s) + "'hello, world.'" # 这里输出的 请注意了 编译器能读的 + + >>> str(5) # number + '5' + >>> repr(5) # 这两个是一样的 + '5' + + >>> hello = 'hello, Leiyu\n' # 雷雨在这里输入了 字符串+ \n + >>> hellos = repr(hello) # \n 本来代表换行的 但 repr() 这后 + >>> print hellos # 还是print 原样输出了 + 'hello, Leiyu\n' + >>> repr(hello) # 这个 还是让 编译器去读的 雷雨读这个 比较别扭 + "'hello, Leiyu\\n'" # 又是双引号 又是单引号的 + >>> + +#### 3 雷雨 接下来学习 制作 平方 立方 的表 or 类似矩阵形式 + + >>> for x in range(1, 11): # 方式1 + ... print repr(x).rjust(2), repr(x*x).rjust(3), + ... print repr(x*x*x).rjust(4) + ... + 1 1 1 # 这里是输出 直接在python中运行的 + 2 4 8 + 3 9 27 + 4 16 64 + 5 25 125 + 6 36 216 + 7 49 343 + 8 64 512 + 9 81 729 + 10 100 1000 + + >>> for x in range(1,10): # 方式 2 + ... print '{0:2d} {1:3d} {2:4d}'.format(x, x*x, x*x*x) # 雷雨看到 这里的 str.format() 形式 恩 原来可以这样输出 + ... + 1 1 1 + 2 4 8 + 3 9 27 + 4 16 64 + 5 25 125 + 6 36 216 + 7 49 343 + 8 64 512 + 9 81 729 + +看 这里 输出的 数字字符 自动 右对齐了 原来 + +- ==str.rjust() ==返回 str 自动右对其 +- ==str.ljust()== 自动左对齐 +- ==str.center()== 对中啦 + +然后看这里: ==str.zfill(n)== + + >>> '-3.1415'.zfill(10) + '-0003.1415' # 自动fill 10位 -和 . 也算一位 + >>> '3.1415926'.zfill(5) + '3.1415926' + +#### 4 之后是 ==str.format()== 雷雨发现 原来 是这样使用的 + + >>> print 'we are the {} who say "{}!"'.format('Knigths', 'Ni') # {} 来放 .format() 中的 一个 字符串 + we are the Knigths who say "Ni!" + + >>> print '{0} and {1}'.format('spam', 'eggs') # 注意{0} {1} 代表的 str 是社么 + spam and eggs + + >>> print '{1} and {0}'.format('spam', 'eggs') # 现在 雷雨晓得了 恩 {0} 是 spam + eggs and spam + + >>> print 'this {food} is {adjective}.'.format( + ... food='spam', adjective='absolutely horrble') # 还可以使用关键词 + this spam is absolutely horrble. + + >>> print 'the story of {0}, {1}, and {other}.'.format( + ... 'Bill', 'Leiyu', other='Jeremiah') # 还可以混合着来 + the story of Bill, Leiyu, and Jeremiah. + +奥 还可以这样 使用 {!s} for str() , {!r} for repr() 和 {0:.5f} :注意 ==:== + + >>> import math + >>> print 'the value of PI is approximately {}.'.format(math.pi) + the value of PI is approximately 3.14159265359. + + >>> print 'the value of PI is approximately {!r}.'.format(math.pi) + the value of PI is approximately 3.141592653589793. + + >>> print 'The value of PI is approximately {0:.5f}.'.format(math.pi) + The value of PI is approximately 3.14159. + +恩 还可以这样玩: + + >>> dic = {'Sjored': 137, 'Jack':9801, 'Jeremiah': 9366} # 这里是一个 dictionary + >>> for name, phone in dic.items(): + ... print '{0:10} ===> {1:10d}'.format(name, phone) + ... + Sjored ===> 137 # 看这里是对其的哦 + Jeremiah ===> 9366 + Jack ===> 9801 + + >>> print ('Jack: {0[Jack]:d}; Sjored: {0[Sjored]:d};' + ... 'Jeremiah: {0[Jeremiah]:d}'.format(dic)) + Jack: 9801; Sjored: 137;Jeremiah: 9366 # 这理还是少了一个空格 对不对 看下面 + + >>> print ('Jack: {Jack:d}; Sjored: {Sjored:d};' # 这里最后一个 分号; 后没有空格 看 输出也是没有空格的 + ... 'Jeremiah: {Jeremiah:d}'.format(**dic)) + Jack: 9801; Sjored: 137;Jeremiah: 9366 + +好了 雷雨今天的旅程就到这里 + +## 总 + +好啦 雷雨 今天学习了 + +- str() 给人看的 +- repr() 给编译器看的 +- str.format() # 主要是这个的使用 输出 + +星期三, 11. 十一月 2015 11:29下午 1.5h + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/1sTry/bottle.md b/1sTry/bottle.md deleted file mode 100644 index 5e1cb5549..000000000 --- a/1sTry/bottle.md +++ /dev/null @@ -1,12 +0,0 @@ -# bottle 私人教程 - -## 背景 - -## 安装 - -## 配置 - -## 使用 - -## 体验 - diff --git a/1sTry/kvdb.md b/1sTry/kvdb.md deleted file mode 100644 index b599b22ca..000000000 --- a/1sTry/kvdb.md +++ /dev/null @@ -1,12 +0,0 @@ -# kvdb 私人教程 - -## 背景 - -## 安装 - -## 配置 - -## 使用 - -## 体验 - diff --git a/1sTry/nose.md b/1sTry/nose.md deleted file mode 100644 index 7cf22565f..000000000 --- a/1sTry/nose.md +++ /dev/null @@ -1,12 +0,0 @@ -# nose 私人教程 - -## 背景 - -## 安装 - -## 配置 - -## 使用 - -## 体验 - diff --git a/1sTry/packagestar.md b/1sTry/packagestar.md new file mode 100644 index 000000000..9226d0024 --- /dev/null +++ b/1sTry/packagestar.md @@ -0,0 +1,198 @@ +# Packages + +雷雨 今天 在探索 Packages 星球 [packages star](https://docs.python.org/2/tutorial/modules.html#packages) + +## 背景 + +雷雨 作为 Python 初学者 需要 对 Python Tutorial 有一个探索 1遍是必须的 + +## 什么是 Packages + +雷雨对 packages 和 module 常常混淆了 不清楚两者的区别 雷雨想到底什么是 Packages呀 奥 在 packages star 提到了 + +> Packages are a way of structuring Python’s module namespace by using “dotted module names”. For example, the module name A.B designates a submodule named B in a package named A. + +恩 Package A 是 submodule B 的 上级 or Parent or 老大 哈利路亚 在雷雨看来 + +- a collection of modules is a package +- package 包含着 module +- 或者 module 是 package 子集 +- 或者 多个modules 组成一个 Package + +好了 这下 雷雨 明白了 Python 中 module 和 package 之间的关系 再也不会混淆了 + +## 怎么使用Packages + +雷雨想 那到底怎么使用 雷雨看到 以下 一个 sound package 的结构 + + sound/ Top-level package + __init__.py Initialize the sound package + formats/ Subpackage for file format conversions + __init__.py + wavread.py + wavwrite.py + aiffread.py + aiffwrite.py + auread.py + auwrite.py + ... + effects/ Subpackage for sound effects + __init__.py + echo.py + surround.py + reverse.py + ... + filters/ Subpackage for filters + __init__.py + equalizer.py + vocoder.py + karaoke.py + ... + +在 sound 这个 老大级 Top-level package下 有3个 subpackages(formats/effects/filters) +每个 package(包括子package)首先都有 __init__.py 来初始化 这个package +这些 __init__.py 文件是为了让 python 识别 这个文件目录下 是 packages + +雷雨知道了这些之后 问 那如何使用这些 packages 呢 +雷雨看到 python 提供几种使用方式: + +- 1 葫芦串儿a.b.c 引入 + + import sound.effects.echo # 1 葫芦串儿 引入 + +这样引入 使用就需要这样 + + sound.effects.echo.echofilter(input, output, delay=0.7, atten=4) # 葫芦串儿 然后再使用 echo 中的 echofilter 函数 + +- 2 吃掉一个package葫芦a.b 引入 + + from sound.effects import echo + +使用就这样 + + echo.echofilter(input, output, delay=0.7, atten=4) + +- 3 直接引入 函数 葫芦 + + from sound.effects.echo import echofilter + +这样 就需要这样使用了 + + echofilter(input, output, delay=0.7, atten=4) + +雷雨知道了以上几种方法 建议使用以下这种形式 + + from package import item + +- 4 还有这种 Importing * From a Package + + from sound.effects import * + +雷雨知道 这个 就是将sound下effects package下的所有 .py 都引入了 但是这个使用需要 effects package下的__init__ .py 要有行代码 + + effects/ Subpackage for sound effects + __init__.py # 就是这个文件 要有行代码 + +这行代码就是 + + __all__ = [ "echo", "surround", "reverse"] + +## 什么时候使用 + +恩 当雷雨需要使用 得用 package 的时候啦 就得 引入啦 + +## 总 + +好了 雷雨简单探索了 Package Star 恩 以前雷雨使用的时候 还真不知道这么多呢 + +雷雨登上 enterprise 离开了 Package Star + +准备探索另外一个 star + +星期二, 10. 十一月 2015 08:49下午 2 tomato + +*** + +# Python Package Install + +哈利路亚 这个是雷雨 之前在 win系统 上 探索 package install +那个时候 雷雨 不知道 真不不知道 package 与 module 的不同呢 +来看看雷雨的探索过程 + +## 背景 ## + +有一个微信公众号 每次写点内容 总要输入密码 浏览器记录了也没有用 +所以想写一个python脚本 直接调用 登录微信公众号网页[https://mp.weixin.qq.com/](https://mp.weixin.qq.com/) + +## 探索 + +- 经过 Google Search [http://stackoverflow.com/questions/2910221/how-can-i-login-to-a-website-with-python](http://stackoverflow.com/questions/2910221/how-can-i-login-to-a-website-with-python) 要安装 automating Web browsing Package [twill](http://twill.idyll.org/) + + 根据 [easy install](http://peak.telecommunity.com/DevCenter/EasyInstall#downloading-and-installing-a-package) 安装 twill 不行啊 + + 不知其他package行不行 还待验证 +- 居然没有直接去 the Python Package Index [PyPI](https://pypi.python.org/pypi)网页去找 **教训** +- 醒悟过来后 找到[twill-1.8.0](https://pypi.python.org/pypi/twill) +- 安装遇到问题 回归 help文档 Installing Python Modules + +## 安装 ## + +### 方案-0 Pip install ### + pip install package_name + +### 方案-1 ### + +在[PyPI](https://pypi.python.org/pypi)下载对应的Package 一般package 有说明怎么安装 安装说明来也可以 +但是也可以下载 package 使用python进行安装 + +- 从PyPI下载Packege文件 如[twill-1.8.0](https://pypi.python.org/pypi/twill) gar.gz文件到目录C:\temp +- 解压package压缩包 可以在文件目录中 `C:\temp\twill-1.8.0` 有个setup.py脚本 +- 使用Python安装 (window Powershell中) + + C:\temp\twill-1.8.0 + python setup.py build + python setup.py install +可以成功安装 + +在使用Twill过程发现 + + ImportError: No module named lxml +缺少 `lxml` module 安装 Package [lxml](https://pypi.python.org/pypi?%3Aaction=search&term=lxml&submit=search)使用babun安装 + + pip install lxml +执行 `from twill.commands import *`提示缺少requests模块 No module named requests go on + + pip install requests +执行 又提示 + + cssselect seems not to be installed. See http://packages.python.org/cssselect/ +继续 + + pip instal cssselect +总算完工 + +### 方案-2 ### + +若在PyPI中的package有安装说明 就直接用command line安装 + +## 总 ## + +- 需求外部 python package babun中 直接`pip install` +- 直接从PyPI中寻找Package + - 若直接有安装说明 就用cmd line命令安装 +- 下载压缩文件 解压 +- 安装 window shell中 安装 + +### 方案-3 在ubuntu系统上了 + +假如安装 requests package + + sudo apt-get install python-requests # 安装 + + +10/20/2015 10:06:09 PM +星期四, 12. 十一月 2015 01:45下午 mod and tranfer + + + + + + \ No newline at end of file diff --git a/1sTry/qpy.md b/1sTry/qpy.md deleted file mode 100644 index 7d6edf2ac..000000000 --- a/1sTry/qpy.md +++ /dev/null @@ -1,12 +0,0 @@ -# qpy 私人教程 - -## 背景 - -## 安装 - -## 配置 - -## 使用 - -## 体验 - diff --git a/1sTry/requests.md b/1sTry/requests.md deleted file mode 100644 index 207595df9..000000000 --- a/1sTry/requests.md +++ /dev/null @@ -1,12 +0,0 @@ -# requests 私人教程 - -## 背景 - -## 安装 - -## 配置 - -## 使用 - -## 体验 - diff --git a/1sTry/restful.md b/1sTry/restful.md deleted file mode 100644 index 27f51b02c..000000000 --- a/1sTry/restful.md +++ /dev/null @@ -1,12 +0,0 @@ -# restful 私人教程 - -## 背景 - -## 安装 - -## 配置 - -## 使用 - -## 体验 - diff --git a/1sTry/sae.md b/1sTry/sae.md deleted file mode 100644 index e7ca6ad8c..000000000 --- a/1sTry/sae.md +++ /dev/null @@ -1,12 +0,0 @@ -# sae 私人教程 - -## 背景 - -## 安装 - -## 配置 - -## 使用 - -## 体验 - diff --git a/1sTry/sqlite.md b/1sTry/sqlite.md deleted file mode 100644 index f2438c7f9..000000000 --- a/1sTry/sqlite.md +++ /dev/null @@ -1,12 +0,0 @@ -# sqlite 私人教程 - -## 背景 - -## 安装 - -## 配置 - -## 使用 - -## 体验 - diff --git a/1sTry/week_0.md b/1sTry/week_0.md new file mode 100644 index 000000000..d95108809 --- /dev/null +++ b/1sTry/week_0.md @@ -0,0 +1,22 @@ +# week_0 + +- 配置学习环环境 + - github + - gitbook + +主要任务 + +- 根据 极简 Python 上手导念 中的一页解说 Python 代码 +- 录入为个人仓库中的 _src/om2py0w/0wex0/main.py +- 并将其功能化, 即: + - 基于现有 42 行代码涉及的能力 + - 设计一个可用的小功能 + - 并完成对应的开发教程,包含: + + 功能设计 + + 技术要点 + + 实现难点 + + 涉及知识 + + ... + - `嗯哼`: 运用 RDD(小黄鸭调试法) 面向半年前的自己述说过程中各种磨难就对了! +- 确保每一个可运行版本,都及时 push 到个人仓库中 +- (**提醒:** 过程中的各种中间版本非常重要,一定要及时 push 入仓库) \ No newline at end of file diff --git a/1sTry/week_1.md b/1sTry/week_1.md new file mode 100644 index 000000000..cf42690c1 --- /dev/null +++ b/1sTry/week_1.md @@ -0,0 +1,3 @@ +# week_1 + +1w练习 diff --git a/1sTry/week_4.md b/1sTry/week_4.md new file mode 100644 index 000000000..32dcac612 --- /dev/null +++ b/1sTry/week_4.md @@ -0,0 +1,3 @@ +# week_4 + +雷雨 第4周的 python star 自个儿的探索教程 \ No newline at end of file diff --git a/2nDev/week01_interact.md b/2nDev/week01_interact.md new file mode 100644 index 000000000..4b0d2507d --- /dev/null +++ b/2nDev/week01_interact.md @@ -0,0 +1,316 @@ +# week_1 日志交互系统 + +- [Diary_1.0](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py0w/0wex0/main.py) +- [Diary_2.0](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py0w/0wex0/diary_beta_2.py) +- [使用说明](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py0w/README.md) + +## 背景 ## + +第一周的任务:交互-101 + +- 极简交互式日记系统 + - 一次接受一行日记 + - 保存为本地文件 + - 再次运行系统时 能打印过往所有日记 + +### 任务分解 ### + +在芝麻星系统中 学习卡片已经分解完 + +1. 脚本调用 +2. 调用参数 +3. 输入中文 +4. 持续交互 +5. 输出为文件 +6. 回读文本数据 + +我一上来 看完交互任务 分析的时间是比较短的 没有冷静的去分析 而是直接看一个个已经拆解好的子任务卡片了 然后等完成任务的时候 发现卡片还可以翻页 后面就是提示 然而。。。我已经完成。。。 **欲速则不达** 保持冷静 保持冷静 不要太着急 慢慢来 这是**自我探索 分析的旅程** 谨记 + +---------- + +## 1-脚本调用 ## + +- 什么是脚本 + - 脚本:就是你写代码的容器 你在哪里写代码的,那个文件就是脚本,python的脚本是**.py**格式的文件 +- 如何调用 + - win 系统 打开[powershell](https://en.wikipedia.org/wiki/Windows_PowerShell) + - python脚本所在本件目录 比如我的`mian.py`在E:\usr\pybeginner\_src\om2py0w\0wex0中 then 在 shell 中输入`cd E:\usr\pybeginner\_src\om2py0w\0wex0` + - 调用脚本: `python main.py` + +以上 可成功调用 python脚本 + +## 2-调用参数 ## + +- 什么参数 + - 就是外部数据 + - 在shell中输入的数据 + - 可以被main.py脚本调用 + +之前在看 @小赖同学 [淘宝搜索](https://wp-lai.gitbooks.io/learn-python/content/0MOOC/taobao.html)教程的时候 了解到原来在shell中输入 + + python mian.py keywords1 keywords2 ... + +就可以使 main.py 脚本调用 外部数据(参数 关键字词)keywords1 和 keywords2 + +- 调用外部数据 如 + - `python invoking_test.py i love python` +- 分析: + - 使用sys模块(System-specific parameters and functions) 之前已经了解 仍查询help文档: +> This module provides access to some variables used or maintained by the interpreter and to functions that interact strongly with the interpreter. It is always available. +> 可获得编译器中变量和与交互时所需用的函数 + +### 1.代码 ### + # -*- coding: utf-8 -*- + import sys + keywords = sys.argv[:] + print keywords + print type(keywords) +- 注意 + - 通过`sys.argv`将shell中的参数keywords**传递**到python脚本中 + - sys.argv[0]代表的是python脚本的名字 `invoking_test.py` + - sys.argv[1] 为 keywords1 + +shell 执行 `python invoking_test.py i love python` 果效: + + ['invoking_test.py', 'i', 'love', 'python'] + + +---------- + +## 3-输入中文 ## + +### 问题1 ### + +invoking_test.py 代码 + + # -*- coding: utf-8 -*- + import sys + print '我爱python' + keywords = sys.argv[:] + print keywords +shell调用 + + python invoking_test.py 我 爱 python +结果 + + 鎴戠埍python + ['invoking_test.py', '\xce\xd2', '\xb0\xae', 'ptyhon'] +可见 `我` 和 `爱`两个外部数据 使用被脚本调用之后 print 在shell中不正常显示中文 + +但是直接Sublime Text 直接运行是可以 `我爱python`是正常显示的 + +### 分析 ### + +- keywords1 和 keywords2 等是中文的怎么办? + - 这个和编码相关 折腾下来 发现中文坑 + - 最好写代码的时候 首次全部用英文完成 + - 然后测试中文 + - 要不会很混乱 + - shell与st2编码又是不同的。。。。 + - 不是有`# -*- coding: utf-8 -*-`申明了么?怎么还是没有用 + - 申明只对脚本中的有用 + - 而shell调用脚本 输出就需要解码啦 print '我爱python' 出现乱码 + - 鎴戠埍python + - 查看官方文档 unicode 然后 + - google search到 [http://www.kryptosx.info/archives/391.html](http://www.kryptosx.info/archives/391.html "python中文编码问题") + + +### 尝试 +代码 + + # -*- coding: utf-8 -*- + import sys + reload(sys) # 必须 reload + sys.setdefaultencoding('utf-8') # 默认编码 + + print ('我爱python'.encode(sys.stdout.encoding)) # 编码 输出 + keywords = sys.argv[:] + for words in keywords: + print words, +shell 果效 + + 我爱python + invoking_test.py 我 爱 ptyhon +ST2中也正常 +done + +---------- + +## 4-5 持续交互 + 输出为文件 ## + +- 运行等待输入 + - while 循环 +- 退出脚本 + - 是否退出 日志写完以 `end` 结尾代表退出 + - 退出时 是否添加时间 `def ask_date()` +- 输出为文件 + - txt + - 打开文件`open()` 写入`write()` + +代码 + + yes_list = ['yes', 'yep', 'ye', 'y', 'YES','YEP', 'YE', 'Y'] + no_list = ['no', 'n', 'NO', 'N'] + + def ask_date(prompt): # 退出时是否添加时间 + ok = raw_input(prompt) + if ok in yes_list: + ur_date = raw_input("Please add date: ") + if ok in no_list: + ur_date = "\n" + return ur_date + + done = False + textInput = "" + + writer = open(diary_name, "w") # if the textfile exist it'll be erased + + while (done == False): # 持续输入日志 + nextInput = raw_input("Please input ur words: ") + if nextInput == "end": + inputDate = ask_date("Do you want add date, yes or no?") # 询问退出的时候添加日期 + writer.write('\n' + inputDate) + break + else: + textInput += nextInput + print nextInput + writer.write(nextInput + "\n") # write into textfile.txt and start a new line + + writer.close() + print ("Here is ur diary: " + textInput) # 你的日志内容 + +### 问题 ### + +时间是手动输入的 是否可以询问之后 自动输入呢? + +search stackoverflow [http://stackoverflow.com/questions/415511/how-to-get-current-time-in-python](http://stackoverflow.com/questions/415511/how-to-get-current-time-in-python "datetime") + +使用 time 模块 + + from time import gmtime, strftime + strftime("%Y-%m-%d %H:%M:%S", gmtime()) +help文档中 `time.gmtime` 使用UTC(世界标准时间)的时间 +进而改为`localtime()` + +### 执行 ### + +添加模块 `from time import localtime, strftime` + +修改 `ask_date()` 中的代码 + + if ok in yes_list: + ur_date = strftime("%Y-%m-%d %H:%M:%S", localtime()) + +果效 + +![datetim vic](https://raw.githubusercontent.com/JeremiahZhang/pybeginner/master/_image/01_time_test.JPG) + +---------- + +## 6 回读文本数据 ## + +- 可以将过往的日志比如txt文件打印出来 +- 让脚本可以对电脑中(操作系统)的文件进行-寻找-打开 + - 使用`os`: `Miscellaneous operating system interfaces` + - 打开当前目录 `current_dir = os.getcwd()` `os.chdir(current_dir)` + - 找到txt文件:`glob`模块 `glob.glob(*.txt)` +- 7.2. Reading and Writing Files + - 打开 `open(filename,mode)` mode常用如下: + - "r" : 可读 + - "w" : 可写 + - "a" : append 附加在文件后面 + - 更多 参考[python-files-io](http://www.runoob.com/python/python-files-io.html) + - 读取 `read()` + +代码: + + import os, glob + current_dir = os.getcwd() + print type(current_dir) + os.chdir(current_dir) + + for file in glob.glob("*.txt"): + print(file) + file_content = open(file, "r") + print file_content.read() +pshell 执行,可以成功读取代码所在文件夹中所有的txt文件 并打印 果效: +![open_read_print](https://raw.githubusercontent.com/JeremiahZhang/pybeginner/master/_image/02_open_read_print.JPG) + +---------- + +## 整合 ## + +以上6个子任务 整合成一个完整的脚本 [main.py](https://github.com/JeremiahZhang/pybeginner/blob/master/_src/om2py0w/0wex0/main.py) + +win pshell 中调用 `python main.py 我 爱 Python` + +果效: +![diarylog](https://raw.githubusercontent.com/JeremiahZhang/pybeginner/master/_image/03_all_done.JPG) + +## 更新 ## + +- Diary_1.0 将以上个部分整合 并用function来调用 其中各个子功能 + - read_diary() + - write_diary() +- Diary_2.0 将脚本说明 使用 a multi-line docstring 形式 + + def my_function() + """Do nothing, but document it. + + No, really, it doesn't do anything. + """ + pass + + print my_function.__doc__ + +---------- + +## 使用 ## + +- 模块 + - sys + - os + - glob + - time +- build function + - open() + - file.read() + - 模块中的func + - sys.setdefaultencoding('utf-8') + - sys.argv[] + - string.encode(sys.stdout.encoding) + - os.getcwd() + - glob.glob() + - strftime() + +## 反思 ## + +第一周任务下来 先将任务完成了 最后在写教程的时候 又对代码进行了测试 因为都写在了一个脚本中 所以再次测试的时候 就重新来过 消耗了无谓的时间 缺少**全局观** **调正** + +完成任务时: + +- 分析 +- 然后 google site:stackoverflow 进行关键字搜索 对应模块 函数 +- help document 查询 + +PS + +- **淡定自在** 任务发布的时候 简单看了一下任务 没有进行太多的分析 总在想赶着完成任务似的 这是不可行 也不可持久的 记住是探索 +- **水平** 高手和新手的一个区别 任务分解能力 + + 学会分析问题 拆解问题 解决问题 + + Keep calm Keep Thinking Smartly +- **关于教程** 发现自己写的比较冗余?如何改进写教程呢? +- **改进** + - 遇见问题 或 项目 + - 分析是**什么** + - **怎么**做 分解问题 拆解项目 + - 循序渐进 + - 实践 模仿 创意 + - 子项 + - 一个个脚本编写 + - 测试 + - 整合 + - 不要一股脑儿就在 一个脚本中 进行 + + + \ No newline at end of file diff --git a/2nDev/week02_Gowechat.md b/2nDev/week02_Gowechat.md new file mode 100644 index 000000000..87fb11cfd --- /dev/null +++ b/2nDev/week02_Gowechat.md @@ -0,0 +1,287 @@ +# 自动登录网页 # + +## 背景 ## + +- 实际需求:微信公众号网页每次登录需要密码 没有记住密码功能 +- 想着写个脚本 调用 可自动登录 + +---------- + +## 想法 ## + +- 1.open the website through webbrowser like FireFox and Chrome +- 2.input user_name and password in python script to login the website + +---------- + +## 尝试 ## + +- google到 twill 模块 [http://twill.idyll.org/](http://twill.idyll.org/) 发现花了笨功夫 + - 没有仔细阅读twill文档 (笨功夫 需谨慎) +> twill is a simple language that allows users to browse the Web from a command-line interface. + +- then 在stackoverflow上又有说requests模块 + - 简单浏览 requests 文档 也不是我所理想的[http://docs.python-requests.org/en/latest/#testimonials](http://docs.python-requests.org/en/latest/#testimonials) +- 发现姿势不对 + - then re-search got this [https://www.google.co.jp/search?q=open+browser+auto+login+python&ie=utf-8&oe=utf-8&gws_rd=cr&ei=U-EoVuTWJ4PpmAXo3IOADg](https://www.google.co.jp/search?q=open+browser+auto+login+python&ie=utf-8&oe=utf-8&gws_rd=cr&ei=U-EoVuTWJ4PpmAXo3IOADg) + - got Selenium Client Driver [http://selenium-python.readthedocs.org/installation.html](http://selenium-python.readthedocs.org/installation.html) + - [PiPY](https://pypi.python.org/pypi/selenium) + +---------- + +## 探索-开始挖硒 ## + +- **安装** 因为在神奇的win 所以只能下载在本地目录 在本地目录使用 + + python setup.py install +安装好库之后就开始实践了 + +### **实践-1 自动打开浏览器 进行google搜索** ### + +代码: [go_google.py](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py1w/1wd5autologin/go_google.py) + + # -*- coding: utf-8 -*- + from selenium import webdriver + from selenium.webdriver.common.keys import Keys + + print """ Example 1: + ========== + + * open a new Firefox browser + * load the google homepage + * search for "github" + """ + + browser = webdriver.Firefox() # 打开火狐浏览器 + browser.get("http://www.google.com") # 打开url网页链接 + assert 'Google' in browser.title # 确认是Google + + elem = browser.find_element_by_name('q') # 找到搜素栏 name = "q" 在google网页源码的 search field + elem.send_keys('github' + Keys.RETURN) # 在搜索栏中输入输入关键字github后 执行搜索 Keys 相当于键盘keyboard + # The Keys class provide keys in the keyboard like RETURN, F1, ALT etc. +执行: 在shell中 + + python go_google.py + +效果: +1.自动打开Firefox浏览器 +2.进入google主页 +3.自动键入关键字 gihub +4.跳到搜索结果 + +OK 这个你可以拓展下 +1.你可以输入外部数据 python go_google.py keyword01 keyword02 +2.....你可以继续构思呢 + + +### 实践-2 登录facebook ### + +代码:[go_facebook.py](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py1w/1wd5autologin/go_facebook.py) + + # -*- coding: utf-8 -*- + from selenium import webdriver # 导入模块 + from selenium.webdriver.support import ui + from selenium.webdriver.common.keys import Keys + + def page_is_loaded(driver): # 测试 网页是否加载好了 + return driver.find_element_by_tag_name("body") != None + + driver = webdriver.Firefox() # 启动FireFox浏览器 + driver.get("https://facebook.com/") # 浏览器进入facebook页面 + + wait = ui.WebDriverWait(driver, 10) # 等待网页加载完毕 + wait.until(page_is_loaded) + + email_field = driver.find_element_by_id("email") # 找到facebook登录帐号栏 + email_field.send_keys("user_email@email.com") # 键入 user_email + + password_field = driver.find_element_by_id("pass") # 找到facebook密码输入栏 + password_field.send_keys("your_password") # 键入 your password + password_field.send_keys(Keys.RETURN) # enter 确认 +shell 执行 + + python go_facebook.py +效果: +1- 打开Firefox并 +2- 转到Fackbook页面 +3- 自动登录 facebook + +### 实践3-登录微信公众号 ### + +从上面facebook的例子 我可以解决我最初自动登录微信公众号的问题了 + +- 将 go_facebook.py 中的代码 改个url链接 + + driver.get("https://mp.weixin.qq.com/") + +- 改下email与password +- 但是你得注意 需要根据 [https://mp.weixin.qq.com/](https://mp.weixin.qq.com/) 网页源码 修改` email_field = driver.find_element_by_id("email")` `password_field = driver.find_element_by_id("pass")`这两处 + - 如何看网页源代码?你需要右击鼠标 点击`查看网页源代码` + - 你需要确定 网页 `账户输入栏`的 `id` 与 `密码输入栏` 的 `id` 如在微信公众号网页源码中 你可以找到登录的源码 + +
+

登录

+ +
+ + + + +
+
+其中 `账户输入栏` 部分源码: + +
+ + +
+其中 id="account" +所以 改为 `email_field = driver.find_element_by_id("account")` + +同理 `密码输入栏` 部分源码: + +
+ + +
+ +改为 `password_field = driver.find_element_by_id("pwd")` + +经过修改之后 你可以参见代码 [go_wechat.py](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py1w/1wd5autologin/go_wechat.py)(只要修改一下你的账户与密码)执行 可自动登录 + +这回你只要用shell调用 go_wechat.py脚本就可以自动微信公众号了 再也不用每次都要输入密码了 真cool + +### 实践-4 登录芝麻星 ### + +你尝试了两种登录方式 +1:注册邮箱 登录 可行 +2:连接github登录 可行 + +#### 方式 1 通过注册邮箱 密码#### +这一种方式和上面`实践-3 登录微信公众号`类似 只需要修改 邮箱 密码 + +- 查找[芝麻星网页](http://www.iomooc.com)源代码的登录email区域 id="email" 然后使用一下代码所定 + + email_field = driver.find_element_by_id("email") +- 查找密码区域 id="password" 然后使用一下代码锁定 + + password_field = driver.find_element_by_id("password") +- 你以为以上执行好之后 就可以了 孰不知 直接在密码行回车是无法登录芝麻星的 所以你有查找并锁定`登录`button的 id="login" 然后使用代码 + + login_field = driver.find_element_by_id("login") + login_field.send_keys(Keys.RETURN) # 相当于 点击 登录button 了 +- 通过以上的尝试 你使用win 的powershell 调用代码 [go_openmind.py](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py1w/1wd5autologin/go_openmind.py) 然后成功了 Cool + - 自动打开火狐浏览器 + - 打开芝麻星网页 + - 自动登录 KO + +#### 方式 2 通过Github#### +你刚开始以为只要锁定芝麻星网页上Github button 那个按钮就行了 然后你查看芝麻星网页源代码的Github button的部分 发现是一个herf 超链接 是这样的 + +
+ GitHub +
+ +嗯 你回去看selenium源文档[4.4. Locating Hyperlinks by Link Text](http://selenium-python.readthedocs.org/locating-elements.html#locating-hyperlinks-by-link-text) 部分 锁定link的教程 + + + +

Are you sure you want to do this?

+ Continue + Cancel + + + + continue_link = driver.find_element_by_link_text('Continue') + continue_link = driver.find_element_by_partial_link_text('Conti') +ok 你知道要怎么锁定了 + +- 添加锁定github button 并点击 代码: + + github_login_button = driver.find_element_by_link_text("GitHub") + github_login_button.send_keys(Keys.RETURN) +- 然后你运行发现 网页跳到了github的登录页 + + ![](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/gitbublogin.JPG) +- 然后你想 这个不就是还是重复类似登录微信公众号的部分嘛 嗯 然后你查看了github登录页源代码 进行锁定 email 与password栏 代码 + + # in github sign in + email_field = driver.find_element_by_id("login_field") + email_field.send_keys("balabalabala@gmail.com") # user_email_address + + password_field = driver.find_element_by_id("password") + password_field.send_keys("babalabala") # your password + password_field.send_keys(Keys.RETURN) +- 这样 你完成了 [iomooc.py](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py1w/1wd5autologin/iomooc.py) 脚本 并成功使用github账户登录芝麻星系统 调用脚本 你会 + - 自动打开火狐浏览器 + - 打开芝麻星网页 + - 自动连接Github 并登录github + - 成功登录芝麻星网页 So Cool + +---------- + +## 拓展 + +- 你可以改为其他浏览器 selenium.WebDirver 支持 Firefox Chrome Ie and Remote + - Currently supported WebDriver implementations are Firefox, Chrome, Ie and Remote + - IE就算了吧 你尽量使用开源的 +- 登入任何一个你想自动登入的网页 + +---------- + +## 总 + +- 你有没有发现什么是笨功夫了么? 你在最初探索的时候 在twill上 因为没有认真看文档 漏了一个关键 就直接白花了近2个小时的时间 折腾在各种英文文档中 +- 你的想法 尽量用英文 然后关键字 google search +- 你是不是还可以继续 比如登入芝麻星 + - 登录芝麻星 [http://beta.iomooc.com/](http://beta.iomooc.com/) 也是够烦的了 + + 连接github 每次都显示白板 + 一行代码 然后刷新才能登录 + - 嗯 这回可以自动登录了 + - 这个已经解决 你只要注册账户 + - 简单修改脚本并调用 + - [x] 你已经完成了芝麻星的自动登录 代码[go_openmind.py](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py1w/1wd5autologin/go_openmind.py) 脚本完成 稍作简单修改 + - 修改网址 + - 修改用户名与密码 + - 增加寻找 login 按钮 然后 `enter` 如下 + + login_field = driver.find_element_by_id("login") + login_field.send_keys(Keys.RETURN) + + - 如何不注册账户 + - 自动连接github + - 登入 + - 这个你该如何解决呢? + - [x] 这个你也进过探索之后完成了 +- 代码在此[https://github.com/JeremiahZhang/OMOOC2py/tree/master/_src/om2py1w/1wd5autologin](https://github.com/JeremiahZhang/OMOOC2py/tree/master/_src/om2py1w/1wd5autologin) + - 可以将以上代码写成函数形式or模块 + - 然后建议新脚本 进行交互 确定登录哪一个常用网页 KO + +# ( ̄▽ ̄) # + +10/23/2015 +10/25/2015 增加 go_openmind.py 自动登录芝麻星代码 与 使用Github登录芝麻星 iomooc.py diff --git a/2nDev/week02_LogGUI.md b/2nDev/week02_LogGUI.md new file mode 100644 index 000000000..a44fcc8ac --- /dev/null +++ b/2nDev/week02_LogGUI.md @@ -0,0 +1,90 @@ +# week_2 日志交互GUI # + +- 你的系统 win 7 +- 你的编码工具 st2 + +## 背景 ## + +你需要基于第一周的 日志交互作业[https://jeremiahzhang.gitbooks.io/omooc2py/content/2nDev/week01_interact.html](https://jeremiahzhang.gitbooks.io/omooc2py/content/2nDev/week01_interact.html) 改写成 极简交互式笔记的桌面版本 GUI + +## 开发 ## + +- 学习 Tkinter [http://effbot.org/tkinterbook/](http://effbot.org/tkinterbook/) + - 你现在 python Help文档摸索了一段时间 发现是不够的 +- 构思 + - 你先前没有好好构思 因为你不熟悉Tkinter 所以你在慢慢熟悉Tkinter模块的时候 才慢慢构思的 + - 你先决定用 Button widget来实现交互 + - 后来你改为使用Menu widget 来进行交互 就像 win中的`记事本`一样 可以: + - 打印过去日志 + - 你先想使用Listbox widget来打印过去日志 + - 后来你放弃了 放弃的原因是 Listbox虽然可以打印 但是你发现 可以使用 Text widget 就可以实现 + - 写日志 + - 新建 + - 保存 + - 退出 + +## Dive in 实现 ## + +- 基本框架 + - 使用 Menu 来实现交互 + - PastLog 打印过去日志 + - New 新建日志 + - Save 最后保存 + - 输入名字 + - Time 未自动加入 + - Exit 退出GUI应用 + - Help 文档帮助使用 +- widget + - menu + - scrollbar + - Text + - def function + - command调用 function + +---------- + +## 代码 ## + +[https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py2w/2wex0/main.py](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py2w/2wex0/main.py) + +### 难点 ### + +- 你在开发过程中纠结的地方 + - 就是没点击一次 Menu的时候 可以将过去的 widget 消除掉 + +### 解决 ### + +- 函数调用 标记 + + self.readme_Tag = False + self.printLogs_Tag = False + self.newLog_Tag = False +- 消除 Widget + + widget.pack_forget() + +以上像个结合使用 就可以将 widget 消除 重新载入你想要的 widget 再进行交互 + +### GUI 界面 ### + +![](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/Gui.JPG) + +- `PastLogs` 打印过去日志 +- `New` 新建日志 +- `Save` 保存日志 +- `Exit` 退出 GUI +- `Help` 帮助文档 + +---------- + +## 总 ## + +- 冷静自在 +- 怎样解题 来实现 怎样编程 + +---------- + +# ( ̄▽ ̄) # + +10/29/2015 6:08:05 PM +10/29/2015 6:18:54 PM mod \ No newline at end of file diff --git a/2nDev/week03_net_way.md b/2nDev/week03_net_way.md new file mode 100644 index 000000000..61c7286d2 --- /dev/null +++ b/2nDev/week03_net_way.md @@ -0,0 +1,503 @@ +# 网络版的 极简日志交互 + +你的环境: + +- 系统 Ubuntu 14.04 LTS + - 直接使用其终端 +- Python 2.7.10 + +要求: + + - 每次运行时合理的打印出过往的所有笔记 + - 一次接收输入一行笔记 + - 在服务端保存为文件: + - 在所有访问的客户端可以获得历史笔记 + - 支持多个客户端同时进行笔记记录 + +## 1 原型 +- 网络开发 明确如何进行 +- UDP协议 +- 简单的UDP服务器/客户端 + +以上都是些什么啊? 你完全摸不着头脑 + +### 尝试 探索 + +- 线索1 socket [python doc socket](https://docs.python.org/2/library/socket.html) + +什么是 [networking interface](https://en.wikipedia.org/wiki/Network_interface)? + +> In computing, a network interface is a system's (software and/or hardware) interface between two pieces of equipment or protocol layers in a computer network. + +> A network interface will usually have some form of network address.[1] This may consist of a node Id and a port number or may be a unique node Id in its own right. + +> Network interfaces provide standardized functions such as passing messages, connecting and disconnecting, etc. + +- 网络接口 + - 计算机网络中 设备or协议层之间的接口 + - 有网络地址:节点id or port nubmer(类似电话号码么?) + - 提供标准函数:传输信息 连接 断连 + +- [network socket](https://en.wikipedia.org/wiki/Network_socket) + - 通讯endpoint + - 通讯 based on IP internet protocol + +> A network socket is an endpoint of an inter-process communication across a computer network. Today, most communication between computers is based on the Internet Protocol; therefore most network sockets are Internet sockets. + +那到底是什么是 [socket](http://baike.baidu.com/subview/13870/15994413.htm) ? + +> Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。 +在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。 +Socket正如其英文原意那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务 + +![socket 工作原理](http://c.hiphotos.baidu.com/baike/c0%3Dbaike80%2C5%2C5%2C80%2C26/sign=17baf4c7d739b60059c307e588395e4f/d000baa1cd11728b45647b06cafcc3cec3fd2c4c.jpg) + +- 常用函数 + - 创建 + - 绑定 + - 接收 + - 发送 + - 接收连接请求 + +就是用来 计算机之间 **通信** 的 然后你将本科的信息技术教程拿出来查看了 恩 你居然没有卖掉。。。 + +- TCP/IP协议标准 + - 计算机网络中通信问题分为4层:[Internet protocol suite](https://en.wikipedia.org/wiki/Internet_protocol_suite) + - 应用层 Application layer + - 传输层 Transport layer (包含UDP协议)规定怎样进行端-端的数据传输 + - 网络互联层 Internet layer + - 网络接口 和 硬件层 Link layer +- UDP协议 [User_Datagram_Protocol](https://en.wikipedia.org/wiki/User_Datagram_Protocol) + - 属于传输层 +> 使用UDP协议时 网络只是尽力而为地进行快速数据传输 不保证传输的可靠性 + +###- Python 实践 1 + +例子1:[Python doc Example](https://docs.python.org/2/library/socket.html?highlight=socket#example) +ex1_server.py + + # coding=utf-8 + # refet to https://docs.python.org/2/library/socket.html?highlight=socket#example + import socket + + HOST = '' + PORT = 50007 + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind((HOST, PORT)) + s.listen(1) + conn, addr = s.accept() + + print 'connected by', addr + while 1: + data = conn.recv(1024) + if not data: break + conn.sendall(data) + conn.close() + +ex1_client.py + + # coding=utf-8 + import socket + + HOST = 'daring.cwi.nl'# the remote host + PORT = 50007 + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((HOST, PORT)) + s.sendall('hello, world') + data = s.recv(1024) + s.close() + print "received", repr(data) +1个 Terminal 中执行 python ex1_server.py +另一个 Terminal中执行 python ex1_client.py + +按照教程中的来 居然出错了 error + + Traceback (most recent call last): + File "ex1_client.py", line 8, in + s.connect((HOST, PORT)) + File "/usr/lib/python2.7/socket.py", line 228, in meth + return getattr(self._sock,name)(*args) + socket.error: [Errno 110] Connection timed out +链接超时 host的问题么? 尝试修改host 与port + + HOST = 'localhost'# the remote host + PORT = 8001 +OK 成功 +执行 ex1_server.py的终端 打印 + + connected by (addr地址) +执行 ex1_client.py的终端 打印 + + received 'hello, world' + +恩 server 端 要: + +- 1 创建socket对象 调用socket函数 + - socket.socket(socket.AF_INET, socket.SOCK_STREAM) +- 2 bind 绑定主机 s.bind((HOST, PORT)) Host主机 于端口PORT +- 3 listen 监听 s.listen(backlog) # baclog 至少为1 多个 就是可以监听多个客户端 +- 4 服务器通过socket的accept method等待客户请求链接 + - connection, address = socket.accept() + - accept() 返回tuple (connection, address) + - connection 表示socket 对象 服务器必须通过它与client通信 + - address 表示客户端的Internet address +- 5 处理: + - 服务器 和 客户端 通过send 和 recv 通信 +- 6 通信结束 使用 close方法 关闭 sock.close() or connection.close() + +恩 client 端编写要: + +- 1 创建socket对象 链接 +- 2 知道主机 地址 和 主机建立链接 + - sock.connect(host_address) # host_address = (HOST, PORT) +- 3 处理: + - 通信 send 和 recv +- 4 通信结束 sock.close() 关闭 + +### 实践 客户端请求打印过去日志 + +服务端:diary_serve.py: + + # coding:utf-8 + import socket + import jeremiah_diary + + pastlog_keyword = "p" + + # main + def main(): + # creat 创建 + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + host_address = ('localhost', 8001) + # bind + sock.bind(host_address) + # listen + sock.listen(3) + + # interact 交互 + + while True: + print "\n Now Please input" + + connection, address = sock.accept() + + data = connection.recv(1024) # reveive message from client + print "You have received message from {0}".format(data) + + if data == "p": + past_logs = jeremiah_diary.read_diary() + connection.sendto(past_logs, address) # print past logs + # write new logs + + connection.close() + + if __name__ == '__main__': + main() +客户端:diary_client.py: + + import socket + + def HELP(): + """ # Dear , Here is the Help Doc: + + 1 Input: p/past , print past logs + """ + + print HELP.__doc__ + + # Creat socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + host_address = ('localhost', 8001) + sock.connect(host_address) # 和server 建立联系 + # 交互 + pastlog_keyword = raw_input("Wanna read past logs? Input p --->") + sock.sendto(pastlog_keyword, host_address) + + back_message = sock.recv(1024) + print "Here is the past logs:---> \n" , back_message + sock.close() +jeremiah_diary.read_diary() 为你编写的jeremiah_diary.py脚本 内置函数 read_diary() + + def read_diary(): + + current_dir = os.getcwd() # 打印之前日志 + os.chdir(current_dir) + + filename_plus_content ="" + + for file in glob.glob("*.log"): + # print(file) # this is the file name + file_content = open(file, "r") + diary = file_content.read() + "\n" + filename_plus_content = filename_plus_content + file + "--->:" +diary + print filename_plus_content + return filename_plus_content +效果: +![打印过去日志](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/try_pastlog.png "pastlog_try") + +*** + +## 消息接收 +### 问题 +- 客户端 要 + - 连续发送中文 raw_input + 循环 +- 服务器 要 + - 接受 客户端 信息 中文 + - 立即保存为文件 + - 保存到服务端 +###尝试 1 持续输入 +diary_server.py 部分修改 + + while True: + print "Please Wait:---> " + + connection, address = sock.accept() + + data = connection.recv(1024) # reveive message from client + print "You have received message from {0}".format(address) + + if data == "p": + past_logs = jeremiah_diary.read_diary() + connection.sendto(past_logs, address) # print past logs + if data == "h": + help_doc = "balabala" + connection.sendto(help_doc, address) + else: + diary_name = "jeremiah_diary.log" + diary_writer = open(diary_name, "a+") + diary_writer.write(data) + back_message = "Continue to Write:--->" + connection.sendto(back_message, address) + # connection.close() + + if __name__ == '__main__': + main() + +diary_client.py 部分修改: + + # Creat socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + host_address = ('localhost', 8001) + sock.connect(host_address) + # continue interact + done = False + while done==False: + pastlog_keyword = raw_input("Please write here Dear! --->") + sock.sendto(pastlog_keyword, host_address) + back_message = sock.recv(1024) + print back_message + + sock.close() +出现问题: + +> Please write here Dear! --->this is right +Continue to Write:---> +Please write here Dear! --->ok +Traceback (most recent call last): + File "diary_client.py", line 26, in + sock.sendto(pastlog_keyword, host_address) +socket.error: [Errno 32] Broken pipe + +在客户端第二次输入后 终端 出现 Broken pipe 的错误 + +- 发现 建立socket中要使用 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)[通信SOCK_DGRAM](http://baike.baidu.com/view/4785427.htm) 原来这里建立socket的时候 就设定 通信协议 以及是IPv4还是v6的, 还有是TCP/IP 和UDP + +发现要使用UDP的 重新来过[参考tutorial](http://www.tutorialspoint.com/python/python_networking.htm) 你发现自己把TCP的和UDP的methond 混在一起了 !!! +原来以上都是在使用TCP协议的!!! + +参考学习 [programming-udp-sockets-in-python](http://www.binarytides.com/programming-udp-sockets-in-python/) + +需要安转 [netcat](https://nmap.org/ncat/) ubuntu install +> Install on Ubuntu +$ sudo apt-get install netcat-traditional netcat-openbsd nmap +To use netcat-openbsd implementation use "nc" command. +To use netcat-traditional implementation use "nc.traditional" command +To use nmap ncat use the "ncat" command. + +learn [ncat - Concatenate and redirect sockets +](http://manpages.ubuntu.com/manpages/trusty/man1/ncat.1.html) + +参考学习 [programming-udp-sockets-in-python](http://www.binarytides.com/programming-udp-sockets-in-python/) 之后 + +server.py: + + # -*- coding: utf-8 -*- + import socket + import sys + import jeremiah_diary + + def help(): + + """ # this is the help doc: + + 1- read past logs? enter:---> p + 2- want leave ?enter:---> e + 3- help doc? enter:---> h + + """ + + HOST = '' # Symbolic name meaning all available interfaces + PORT = 8888 # Arbitrary non-privileged port + + # UDP SOCKET + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + print "socket created! " + except socket.error, msg : + print "failed to created socket. ERROR code : " + str(msg[0]) + " message " + msg + sys.exit() + + # bind socket to localhost and port + try: + s.bind((HOST, PORT)) + except socket.error, msg: + print "bind failed. error code: " + str(msg[0]) + " message " + msg[1] + sys.exit() + + print "socket bind complete" + + def responses(): + if data =="e": + sys.exit() + elif data =="p": + reply = jeremiah_diary.read_diary() + elif data == "h": + reply = help.__doc__ + else: + diary_name = "jeremiah_diary.log" # 写日志 + diary_writer = open(diary_name, "a+") + diary_writer.write(data + "\n") + diary_writer.close() + reply = "Continue to Write:--->" + return reply + + + # communicate with the client + while 1: + # receive from client + d = s.recvfrom(1024) + data = d[0] # client message + addr = d[1] # client address addr = (host, port) + + reply=responses() + + s.sendto(reply, addr) + + print "message from [ " + addr[0] + ":" + str(addr[1]) + "] is --->" + data.strip() + + s.close() + +client.py: + + # -*- coding: utf-8 -*- + import socket + import sys + + # create dgram udp socket + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + except socket.error: + print "Failed to create socket" + sys.exit() + + host = "localhost" + port = 8888 + + print "please enter h to see the help.-->" + + while 1: + msg = raw_input("Enter message to send: ---> ") + + try : + # send the whole string + s.sendto(msg, (host, port)) # send to serve + + # receive data from server + d = s.recvfrom(1024) + reply = d[0] + addr = d[1] + + print "server reply: ---> " + reply + + except socket.error, msg: + print "error code: " + str(msg[0]) + " message " + msg[1] + sys.exit() + +这下 可以直接 + +- 打印日志 +- 写日志了 + +执行: +![ ](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/netbeta1.png "beta1.0") + +## 3 多个客户端 +- 什么是客户端呀? + - 客户可以在客户端 向主机服务器发送消息 +- 和服务器有什么关系? + - 客户端发送 服务器确认 接受信息 反馈等 +- 多个客户端发送消息 给服务器 对服务器会有影响么?什么影响? + - 要排队么? + - 不知晓了 +- 多个客户端可以反复获得历史消息么? + - 历史消息 已经发送给服务器了 + - 如何获得历史消息? + - 客户端要向服务器请求 + - 服务器再发过来 +尝试 复制client.py的代码 修改 host参与 port 是不行的 + +学习 [Tutorial on Network Programming with Python](http://www.mws.cz/files/PyNet.pdf) + +- 尝试 在 服务器端 d = s.recvfrom(1024) 接受信息时候 启用 s.setblocking(0) 无效果 + +如何 多个客户端发送消息给服务器呢? 想不到办法解决呀! (上面最 近的代码只能一次处理一个客户端的请求呀) + +- [ptyhon 网络编程](http://blog.csdn.net/dbanote/article/details/8856531) 提到类似问题 需要分叉 与 线程 需要使用 SocketServer [socketserve doc](https://docs.python.org/2.7/library/socketserver.html?highlight=socketserve) 相关模块 +好像又不用socket模块 如何解决呢? 之后再尝试吧 先解决 历史消息获取问题去 +- 支持多个客户端同时进行笔记记录 还未实现 的继续探索 + - 咦 你发现将client.py脚本 移动到另一个文件夹中 然后使用python调用 可以与服务器通信 这样 + - 恩 同一个文件中的两个cilent是无法同时和server通信的 它们使用同一个port 所以无法同时通信 + +## 4 历史消息获取 +- 客户端一启动 如何获得服务端的历史消息? + - 一启动 发送指令 请求服务器发送过来 + - 这个好解决 只要在 client.py中添加一个 打印过去日志的指令 while 前添加 + + # print past logs when start + s.sendto("p", (host, port)) + d = s.recvfrom(1024) + reply = d[0] + print "Past logs: ---> \n" + reply +- 运行过程中 又反复获得 历史消息 可以吗? + - 你该如何实现呢? + - 运行过程中 反复获得历史消息 + - 上面 程序 可以直接 按 p 打印过去日志了 + +## 总 +- 在探索的过程中 解决问题的也就那几步 + - 找到 socket UDP 基础模板 学习理解 + - 类比 来解决自己问题 +- 在自个儿解决中 没有从解决问题中来学习 + - 先去查了资料(比较杂)居然从 socket TCP开始了 无脑思考 + - 然后开始编程 +- 其实应该 + - 找到关键资料:如这里是 socket UDP编程 就锁定关键词 + - 立即开始编程 + - 在编程中 再去学习 相关内容 +- 恩 整理时候 直接用github commit的link 这样 就不用在这里一次次copy自己的代码了 + +记住: + +- 最小代价解决问题呀 +- 冷静 冷静 冷静 自个儿要有自个儿的节奏 + +星期一, 02. 十一月 2015 09:42下午 +星期三, 04. 十一月 2015 08:12下午 修改 + + + diff --git a/2nDev/week04_web_way.md b/2nDev/week04_web_way.md new file mode 100644 index 000000000..85219cce8 --- /dev/null +++ b/2nDev/week04_web_way.md @@ -0,0 +1,555 @@ +# 日志交互 web版 + +## 开发环境 + +- - Ubuntu14.04 LTS + 其自带终端 +- - SublimeText2 (ST2) +- - Python 2.7.10 +- - Firefox 41.0.2 for Ubuntu + +## Goal + +- 完成极简交互式笔记的WEB版 + - 通过网页访问系统 + - 每次运行时合理的打印出过往的所有笔记 + - 一次接收输入一行笔记 + - 在服务端保存为文件 + - 兼容Net版的,CLI可以进行交互 + +## 准备 + +- networks 和 internet 和 port + - **net** 本地(共用一个cable)多台计算机 可以组成一个局域网络(打CS就可以使用局域网)(Local Area Network) 这些计算机之间的访问 需要通过MAC地址了 + - **Internet** web 就是多个局域网 Network 组成的大的互联网 (打CS时 你需要连上互联网 才能与其他 network 中的 计算机用户 对战) 互联网中不同network中计算机的通信 恩 需要 Internet Protocol (IP) address 再通过 router 和 MAC 地址 进行通讯了 + - **port** 端口 物理端口的话如插usb的那个口 这里的port是 两个机子之间通信过程 通过port来定义 + - [具体参考](http://www.mws.cz/files/PyNet.pdf) +- 使用web frame work [Bottle 0.12](http://bottlepy.org/docs/dev/index.html) 来看看 Bottle的介绍 bottle是小而全的一个框架 精简而实用 恩 Pythonic +> Bottle is a fast, simple and lightweight WSGI micro web-framework for Python. It is distributed as a single file module and has no dependencies other than the Python Standard Library +- 安装Bottle + - 你可以直接下载[bottle.py](http://bottlepy.org/docs/dev/index.html) 到自己的项目文件夹中 + - 或者 你也可以 安装 + - sudo pip install bottle +- 为什么要用框架 + - web的开发 像建造楼房 打好外部的框架结构 再在此基础上 建造 就容易多了 + - 有 web framework 恩 web的建造 也是容易的 + +## 熟悉 + +- 你可以走一遍 [Tutorial: Todo-List Application](http://bottlepy.org/docs/dev/tutorial_app.html#complete-example-listing) + - 恩 建议先将代码自己码好 然后 运行理解 + - 恩** 以亲自码码为容 以复制粘帖为耻 ** +- 建立一个简单的网页访问 **webserver.py** 代码如下 + + from bottle import * + + @route('/') # here you can type http://localhost:8010 to see hello welcome + @route('/hello') + def hello(): + return """ # 返回的是html语法 恩 这样网页前端访问 有个基本的样子么 + +

Dear Friend!

+

This is Your Diary Web! have fun!

+ + + + """ + run(host='localhost', port=8010, debug=True, reloader=1) +- ST2中运行 恩 怎么运行 参考这篇[SubPy](https://jeremiahzhang.gitbooks.io/omooc2py/content/0MOOC/SubPy.html) +- 打开Firefox(你也可以用其他浏览器)键入 http://localhost:8010/ 就可以直接访你所建立的网页了 +- 效果: +![01webhello](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/01webhello.jpg) + +### 理解 + +- from bottle import * 导入模块 这样你才可以使用 bottle 的命令嘛 +- route() 恩 这个 decorator 是为你的网页开路的 上网总得有个路径吧 恩 route() 来为你开路 +> The route() decorator binds a piece of code to an URL path. + - 你可以直接使用 @route('/hello') 这样的话你就需要键入 http://localhost:8010/hello 来访问网页 + - 在前面 添加 @route('/') 那么直接键入 http://localhost:8010/ 就可以访问上面网页了 + - 可以这么理解 恩 @route('/') 为 @route('/hello') 开好路了 直接 http://localhost:8010/ 就可以到达 http://localhost:8010/hello 内容 +- **函数** 恩 你有没有看到 @route() 后面都紧跟(或绑定bind)着一个定义的函数? 这里定义了 def hello() 这样 @route() 开好路 hello() 函数调用了 + - *hello()* 函数直接返回的是 html 的内容 恩 这样 网页 + - 什么是 *html*?恩 看这里 [html](https://en.wikipedia.org/wiki/HTML) 简单来说 就是建立网页的 + - 恩 因为 *hello()* 函数返回的是 html 代码内容 所以打开 http://localhost:8010/ 链接 就直接建立了一个web网页 +- **return** 要返回 html代码 这样就可以建立网页嘛 +- run() 跑起来 才能 进行网页访问呢 + - host 与 port 主机信息 port 端口信息 + - **debug = True** 在开发的时候 你可以使用 如果运行出错 会显示错误内容 方便你debug 也可以直接写一行 debug(True) 开发完之后 记得取消哦 + - **reloader = 1** 确认重新加载 恩 这样 你只要修改代码内容保存后 会直接自动加载 + +*** + +## Dive In + +在 **webserver.py** 中添加代码 + + import sqlite3 + ... + @route('/new', method='GET') + def new_item(): + + if request.GET.get('save','').strip(): + + new = request.GET.get('task', '').strip() + conn = sqlite3.connect('todo.db') + c = conn.cursor() + + c.execute("INSERT INTO todo (task,status) VALUES (?,?)", (new,1)) + new_id = c.lastrowid + + conn.commit() + c.close() + + return '

The new task was inserted into the database, the ID is %s

' % new_id + else: + return template('new_task.tpl') + ... + +例外新建 template 模块 new_task.tpl 代码如下:(需保存在与webserver.py同一个项目目录中) + + %#template for the form for a new task +

Add a new task to the ToDo list:

+
+ + +
+ +### 理解 + +- sqlite3 [DB-API 2.0 interface for SQLite databases](https://docs.python.org/2/library/sqlite3.html) DB-API 接口 +- new_item() 函数 + - 数据库中的交互 + - return template('new_task.tpl') + - 返回 template 使用的是 new_task.tpl 模块 + - new_task.tpl 模块 代码是 html 格式的 说明 返回的是html网页 + - 在执行过程 中 你会发现 首先执行的是 else 中的 return template +![02webnewitemjpg](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/02webnewitemjpg.jpg) + - 然后网页刷新 会执行 if 返回 return '

The new task was inserted into the database, the ID is %s

' % new_id 这个网页 +![03webnewitemback2](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/03webnewitemback2.jpg) +- 这里用到了 method=‘GET’ + - if request.GET.get('save','') 如果 retrun template网页中 点击了 save 恩 这个if条件就成立 然后就执行 写入数据 并执行 if条件下的 那个return 返回网页 + - 这说明 request.GET.get() 可以获取网页中的内容 主要是
下的 + - 这里 request.GET.get('save','').strip() 获取 的 value 值 + - new = request.GET.get('task', '').strip() 得到的就是 输入框text中的内容 +- 其他关于数据库的内容 暂时不管 + +这样 你就可以使用上面的代码 来进行 **极简交互式笔记的WEB版** 开发了 + +*** + +## 极简交互式笔记的WEB版 + +- 代码 webserver.py + + # coding=utf-8 + from bottle import * + import sys + @route('/write', method="GET") + def input_diary(): + + if request.GET.get('save','').strip(): # 之后执行这里 + diary_words = request.GET.get('words','').strip() + diary_name = 'Diary.log' # if not exist then creat + diary_file = open(diary_name, 'a+') + diary_file.write(diary_words + '\n') + diary_file.close() + return template('write_words.tpl', content=diary_words) # 再次返回网页 + + else: # 刚开始先执行这里的 + diary_name = 'Diary.log' + diary_file = open(diary_name, 'r') + diary_content = diary_file.read() + diary_file.close() + return template('write_words.tpl', content=diary_content) + + if __name__ == '__main__': + run(host='localhost', port=8010, debug=True, reloader=1) # switched debug off for publich applocations + +- write_words.tpl 代码 :这个 + +

Write new words into your diary.log:

+ + + +
+ +

Here is your diary content! Darling!

+ + +- firefox中键入:http://localhost:8010/write 就可以访问 web 了 + +效果: +![04webserverresult](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/04webserverresult.jpg) + +- 刚开始打印历史笔记 +- 然后输入笔记内容 打印上一次输入笔记内容 非全部历史内容 + + +### 走的弯路 + +刚开始的时候 想着 在write中 input_diary() 来一个while循环 使得可以持续输入恩 如 + +def input_diary(): + + done =True + while done == True: + + if request.GET.get('save','').strip(): + diary_words = request.GET.get('words','').strip() + + if diary_words == 'q': + sys.exit() + else: + diary_name = 'Diary.log' # if not exist then creat + diary_file = open(diary_name, 'a+') + diary_file.write(diary_words + '\n') # write words your input + diary_file.close() + + else: + return template('write_words.tpl') + +结果就是 悲惨地发现 在text输入框中 输入一次 Diary.log 日志文件 一直在写入 立马关闭停止 然后Diary.log 文件很大 CLI 命令行 rm 都无法消除 然后 直接把整个文件夹删了 才没有该文件了 + +你会发现 自然而然地想到 循环来输入 然后 在这里 行不通 走不通之后 你一直刷行 write 网页 然后发现 只要在 if 条件下 再添加 return template不就行了 么 + +恩 只是上面的 只能在textarea中显示 你上一次的输入 没法显示 所有的历史记录 + +尝试解决: + +- 在if下添加 diary_content 内容 然后 使用template输出 + + diary_content="" + + if request.GET.get('save','').strip(): + diary_words = request.GET.get('words','').strip() + diary_name = 'Diary.log' # if not exist then creat + diary_file = open(diary_name, 'a+') + diary_file.write(diary_words + '\n') + diary_content += diary_words + diary_file.close() + return template('write_words.tpl', content=diary_content) # 再次返回网页 + - 结果是 还是只能打印上一次的输入笔记 + - 分析 恩 看来 diary_content 还只是局部变量 重新刷了网页之后 还是空了 + +再次解决: + +- 重新打开文件读取 再输入好了 +- 恩 修改代码: + + if request.GET.get('save','').strip(): + diary_words = request.GET.get('words','').strip() + diary_name = 'Diary.log' # if not exist then creat + diary_file = open(diary_name, 'a+') + diary_file.write(diary_words + '\n') # write words your + diary_file.close() + diary_file = open(diary_name, 'r') + diary_content = diary_file.read() + diary_file.close() + return template('write_words.tpl', content=diary_content) + +这样就解决了历史输出 虽然看起来 不精简 但也算是解决问题了 GO 哈利路亚 + +*** + +## 总 + +- web版笔记交互 开发只需要那么几步 + - route 开路 + - Request data + - 模板 template 使用 html 来建立网页 +- 不要纠结小细节 恩 Go 哈利路亚 + +*** + +## CLI 交互 + +- 理解 + - 网页已经打开 大妈演示的时候是 lynx localhost:port 的 相当于已经打开了浏览器访问 + - 然后用命令行CLI进行 交互 python CLI.py +- 恩 所以 这里 你只要再建立一个 CLI.py 就可以 + - 恩 公开课的时候 大妈演示了 curl 基础使用 + - 这里想必用curl + - go check [curl doc](http://curl.haxx.se/docs/manual.html) 锁定 post(http) 功能 和 curl -G + +> POST (HTTP) + It's easy to post data using curl. This is done using the -d + option. The post data must be urlencoded. + +curl -G localhost:8010 获取了网页的内容 +在 终端上可以直接使用 但是用python 脚本 如何使用 尝试了 直接 +content = curl -G localhost:8010 是不行的 语法错误 恩 说明 我还没有导入相关模块 + +那么 使用什么 模块呢? search [Python code like curl](http://stackoverflow.com/questions/3973223/python-code-like-curl) go to [pycurl](http://pycurl.sourceforge.net/) 替代使用 [pycurl](http://pycurl.sourceforge.net/doc/index.html) + +linux下 install pycurl 终于 + + sudo apt-get install python-pycurl + +按照网站: +pip install pycurl 粗错了 +sudo apt-get install pycurl 也出错的 +下载压缩吧下来 python setup.py 也同样出错。。。 +晕呀晕呀 + +简单执行 CLI.py + + #! /usr/bin/env python + # -*- coding: utf-8 -*- + import pycurl + from StringIO import StringIO + import re + + buffer = StringIO() + c = pycurl.Curl() + c.setopt(c.URL, 'http://localhost:8010/') + c.setopt(c.WRITEDATA, buffer) + c.perform() + c.close() + + html_doc = buffer.getvalue() + print (html_doc) + +得到:html +![CLI01html](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/CLI01html.jpg) 。。。01 + +### 再次理解 + +- pycurl可以将url中html中内容抓取下来 +- 再要从抓取的html内容中获取 日志内容 (解析HTML) +- 写的话 就是将 input post 到 html 的textarea 中 + +### 解析HLML + +- 使用 [BeautifulSoup/bs4](http://www.crummy.com/software/BeautifulSoup/#Download) +- 走 bs4文档 http://www.crummy.com/software/BeautifulSoup/bs4/doc/ + +添加代码: + + from bs4 import BeautifulSoup + ... + soup = BeautifulSoup(html_doc, 'html.parser') + print (soup.get_text()) + +恩这样得到了所有text内容 + +![CLI02htmlcontent.jpg](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/CLI02htmlcontent.jpg) + +但是 你只想要 textarea中的内容 怎么办 +修改为: + + soup = BeautifulSoup(html_doc, 'html.parser') + soup_textarea = soup.find_all("textarea") # 这是list 类型的 + print (soup_textarea) + +恩 可以得到 textarea 中的所有内容 如下: + +![CLI03htmltextarea.jpg](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/CLI03htmltextarea.jpg) + +在此抓取 text 就ok 了吧 get_text 但是 soup_textarea 是list 类型的 你先要提取str 恩 哈利路亚 尝试提取str之后 就不知道该怎么进行了 + + soup_textarea = soup.find_all("textarea") + soup_textarea_str = str(soup_textarea).strip('[]') # convert list to string + +再次尝试: + + soup = BeautifulSoup(html_doc, 'html.parser') + soup_textarea = soup.textarea # TYPE is list + textarea_contents_str = str(soup_textarea.contents).strip('[]') + print textarea_contents_str + +这次遇到了 编码问题 print的结果是这样的: + +![CLI04htmlencode.jpg](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/CLI04htmlencode.jpg) + +结果输出的 unicode 对象了 +查看 textarea_contents_str 的type 是 str 呀 编码头疼 先放一边 soup_textarea.content 有问题 + +尝试解决: + + soup = BeautifulSoup(html_doc, 'html.parser') # bs4的格式 + soup_textarea = soup.textarea # TYPE is list + textarea_contents_str = soup_textarea.contents[0] # 这样的 类型 + print textarea_contents_str + +这下正常输出了 +![CLI05htmlprint.jpg](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/CLI05htmlprint.jpg) + +### 持续CLI写入 + +- raw_input 输入 +- 将输入的内容写入到网站 并返回CLI 这个是关键 如何进行呢? + +#### 尝试 1 继续使用pycurl + +还是使用pycurl:继续添加代码: + + c = pycurl.Curl() + c.setopt(c.URL, 'http://localhost:8010/') + + post_data = {'field': 'message'} + postfields = urlencode(post_data) + c.setopt(c.POSTFIELDS, postfields) + + c.perform() + c.close() + +结果失败: + +![CLI06sentdata.jpg](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/CLI06sentdata.jpg) + +检索 不到 然后 转 Requests 库了 [Requests](http://docs.python-requests.org/en/latest/index.html) + +#### 尝试2 使用Requests + +install Requests + + sudo apt-get install python-requests + +没事查看模块版本 python下 + + >>> import requests + >>> print (requests.__version__) + 2.2.1 + +修改CLI.py 为: + + import requests + from bs4 import BeautifulSoup + import sys + + html_doc = requests.get('http://localhost:8010/') # html_doc.text is the content of html + soup = BeautifulSoup(html_doc.text, 'html.parser') + soup_textarea = soup.textarea # TYPE is list + textarea_contents_str = soup_textarea.contents[0] + print textarea_contents_str + +效果同 +[CLI05htmlprint.jpg](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/CLI05htmlprint.jpg) + +恩 简洁了些 + +post data: http://docs.python-requests.org/en/latest/user/quickstart/#more-complicated-post-requests + +如何将 input post 到 text中内? + + postdata = {'words': 'write?'} # words 为html 中的 text name + r = requests.post('http://localhost:8010/write', data = postdata) + print r.url + print r.text + +出现与使用pycurl一样的 问题 + +![CLI06sentdata.jpg](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/CLI06sentdata.jpg) + +恩 是 html 格式的问题 OK 修改 write_words.tpl 正规一点: + + + + +

Write new words into your diary.log:

+
+ + +
+

Darling!Here is your diary content!

+ + + + +恩 好看一点是没有用的 在服务端 响应 哈利路亚 + + "GET /write?words=get+and+post&save=save HTTP/1.1" + +修改: + + postdata = {'words': 'write?', ‘save’: 'save'} + +还是不行 method 行不通 + + Method not allowed. # 去查看 method + +那倒是 + +
+ +中 method是 get 我使用我使用 request的post 行么?恩 换回 get尝试 [passing-parameters-in-urls](http://docs.python-requests.org/en/latest/user/quickstart/#passing-parameters-in-urls) + + postdata = {'words': 'write', 'save': 'save'} + r = requests.get('http://localhost:8010/write', params = postdata) + print (r.url) + +恩 这回可以了 打印的 url 为 http://localhost:8010/write?save=save&words=write 和网页端输入后一样 + +整合一下 持续交互 看结果 CLI.py + + #! /usr/bin/env python + # -*- coding: utf-8 -*- + import requests + from bs4 import BeautifulSoup + import sys + + def help(): + """ # This is help doc: + + 1. Quit Please Type : exit /q/quit/Q + 2. See Help Document Type: help/H/h/? + 3. See Diary Histroy Type: hist + + Let's Start. GO""" + + def hist_logs(): + html_doc = requests.get('http://localhost:8010/') # html_doc.text is the content of html + soup = BeautifulSoup(html_doc.text, 'html.parser') # html + soup_textarea = soup.textarea # TYPE is list + textarea_contents_str = soup_textarea.contents[0] + print textarea_contents_str + + def input_logs(yourwords): + data = {'words': yourwords, 'save': 'save'} # two input in write_words.tpl so you must add name='save' item + requests.get('http://localhost:8010/write', params = data) + + def main(): + + while True: + yourwords = raw_input('Type your words--->$') + + if yourwords in {'exit', 'q', 'quit','Q'}: + sys.exit() + elif yourwords in {'help', 'H','h','?'}: + print help.__doc__ + elif yourwords == 'hist': + hist_logs() + else: + input_logs(yourwords) # write new diarys or logs + + if __name__ == '__main__': + main() + +效果:(首先请运行 python webserver.py) + +![CLI07result.jpg](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/CLI07result.jpg) + +requests 可以解决了 那么 pycurl 类似 不过得用其GET 部分 恩 哈利路亚 + +## 继续迭代 + +- [x] CLI可以交互 +- [] 美化网页 使用 [bootstrap](http://getbootstrap.com/) +- [] 数据库使用 + +## 总 + +- 坑终于填好了 +- 没有最小代价解决问题 总是自己在琢磨 少了搜索 搜索也搜索不到 姿势不对么 不是 + +## 代码: + +- [webserver.py](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py4w/4wex0/webserver.py) +- [CLI.py](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py4w/4wex0/CLI.py) +- [write_words.tpl](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py4w/4wex0/write_words.tpl) + +星期一, 09. 十一月 2015 01:07 AM +星期一, 09. 十一月 2015 10:24下午 \ No newline at end of file diff --git a/2nDev/week05_idea.md b/2nDev/week05_idea.md new file mode 100644 index 000000000..a1c794100 --- /dev/null +++ b/2nDev/week05_idea.md @@ -0,0 +1,22 @@ +# Trek06 搭建自己设想的应用 + +这周完成了简单的公网访问 虽然迭代还没有完全 也算是将一个网站给搭建了起来 + +受到这周任务的影响 可以在此基础上 开发自己设想的应用 + +idea: + +- 搭建 表白 网站 +- 写下自己的表白内容 + - 备份 + - 加密 +- 分享给表白对象 + - 只能其拥有密钥才能阅读 + +好了: + +说是简单 就差行动 先记着为上 恩 + +释放 + +Tuesday, 17. November 2015 08:11PM \ No newline at end of file diff --git a/2nDev/week05_jinja.md b/2nDev/week05_jinja.md new file mode 100644 index 000000000..ad645570c --- /dev/null +++ b/2nDev/week05_jinja.md @@ -0,0 +1,27 @@ +# Jinja2 初体验 + +https://realpython.com/blog/python/primer-on-jinja-templating/ + +github commit [ci](https://github.com/JeremiahZhang/OMOOC2py/commit/d986fee3da52553fbe8df1452d21fd7d738385d5) + +最后 一次提交的错误提示 + + Traceback (most recent call last): + File "/usr/local/lib/python2.7/dist-packages/bottle.py", line 862, in _handle + return route.call(**args) + File "/usr/local/lib/python2.7/dist-packages/bottle.py", line 1732, in wrapper + rv = callback(*a, **ka) + File "run.py", line 10, in template_test + return jinja2_template('template.html', my_string='Wheeee!', my_list=[0, 1, 2, 3, 4, 5], title="Home") + File "/usr/local/lib/python2.7/dist-packages/bottle.py", line 3609, in template + return TEMPLATES[tplid].render(kwargs) + File "/usr/local/lib/python2.7/dist-packages/bottle.py", line 3322, in render + return self.tpl.render(**_defaults) + File "/usr/local/lib/python2.7/dist-packages/jinja2/environment.py", line 989, in render + return self.environment.handle_exception(exc_info, True) + File "/usr/local/lib/python2.7/dist-packages/jinja2/environment.py", line 754, in handle_exception + reraise(exc_type, exc_value, tb) + File "", line 1, in template + TemplateSyntaxError: expected token 'as', got 'end of statement block' + +不知道原因 待查明 \ No newline at end of file diff --git a/2nDev/week05_paas.md b/2nDev/week05_paas.md new file mode 100644 index 000000000..e826d36d3 --- /dev/null +++ b/2nDev/week05_paas.md @@ -0,0 +1,213 @@ +# 极简日志 公网版 + +开发环境: + +- 系统: Ubuntu14.04 LTS +- shell: zsh +- SublimeText3 (ST3) +- Python 2.7.10 +- Firefox 41.0.2 for Ubuntu +- Sina SAE + +分解任务: + +- 极简日志实现公网访问(直接输入网址访问) + - python 应用在SAE上创建和部署 + - bottle 如何和SAE结合 实现4w功能 +- KVDB 来收集 笔记 分类 管理 备份 +- CLI 统计 看看 + +## 0 安装SAE + +- install sae (Ubuntu环境) + + sudo pip install sae-python-dev + +其他 可参考 [SAE 安装](http://www.sinacloud.com/doc/sae/python/tools.html#id3) + +## 1 创建应用 + +登录SAE,进入 [控制台-云应用SAE](http://sae.sina.com.cn/), 点击 创建新应用 ,创建一个新的应用jeremiahzhang, 开发语言选择Python + +## 2 编辑应用代码 + +- 在本地 建立新文件目录 比如 sae 在sae 文件下 2个文件 +- 创建 应用配置文件 config.yaml + + name: jeremiahzhang + version: 2 # 因为 雷雨之前 创建过 一个版本 version 1 所以这里为 version 2 + +- 创建 index.wsgi 这里可以使用web 框架 [bottle 框架](http://www.sinacloud.com/doc/sae/python/tutorial.html#bottle) + + # coding:utf-8 + from bottle import * + import sae + import sys + + app = Bottle() + + @app.route('/') + @app.route('/write', method='GET') + def hello(): + return 'hello jeremiah-bottle demo' + + application = sae.create_wsgi_app(app) + +## 3 实现公网访问 + +[SAE git 代码部署手册](http://www.sinacloud.com/doc/sae/tutorial/code-deploy.html#git) + +在 SAE 云端 代码管理 部分 可以查看 应用仓库地址 这里使用Git版本控制 + +![py5w02saegit](http://7xo9hk.com1.z0.glb.clouddn.com/py5w02saegit.jpg) + +在 config.yaml 和 index.wsgi 所在文件目录(之前建立的sae文件目录)下 使用如下命令 + + git init + git remote add sae https://git.sinacloud.com/jeremiahzhang # 这里 jeremiahzhang 是 appname 应用名 + git add . + git commit -m 'beta 1.0 push' + git push sae master:2 # 部署到sae版本2 + +[git 设置 避免每次都要输入密码](https://help.github.com/articles/caching-your-github-password-in-git/) + +另外请注意 账户和密码为 安全邮箱 和安全密码 雷雨直接用sina 微博登录的 但不是微博的帐号和密码 请注意 + +然后 再 git add git commit 再 + + git push sae master:2 + +建立第二个版本 如图 + +![版本master:2](http://dn-jeremiahzhang.qbox.me/py5w03saegit02.jpg) + +然后打开 http://jeremiahzhang.sinaapp.com/ 是可行的 之前的不可行 + +![error](http://dn-jeremiahzhang.qbox.me/py5w04sae.jpg) + +### 3.1 出错情况 + +雷雨之前 将 index.wsgi 直接使用4w的代码 如 + + ... + app = Bottle() + + @app.route('/') + @app.route('/write', method="GET") + def input_diary(): + + if request.GET.get('save','').strip(): + diary_words = request.GET.get('words','').strip() + diary_name = 'Diary.log' # if not exist then creat + diary_file = open(diary_name, 'a+') + diary_file.write(diary_words + '\n') # write words your + diary_file.close() + diary_file = open(diary_name, 'r') + diary_content = diary_file.read() + diary_file.close() + return template('write_words.tpl', content=diary_content) + + else: + diary_name = 'Diary.log' + diary_file = open(diary_name, 'r') + diary_content = diary_file.read() + diary_file.close() + return template('write_words.tpl', content=diary_content) # 将write_words.tpl文件也放在了 sae文件下 + + application = sae.create_wsgi_app(app) + +在这里 雷雨出错了 恩 Internet server error 然后没有更详细的信息了 + +好吧 是 index.wsgi 中自己代码的问题 所以雷雨重试了上面的[bottle 框架](http://www.sinacloud.com/doc/sae/python/tutorial.html#bottle) + +KO 云端不能读取 那个日志文件呀 同本地不同哒 + +### 3.2 本地运行 + +参考 http://www.sinacloud.com/doc/sae/python/tools.html#id2 + +本地进行调试 + +在 配置文件目录下(sae 文件目录) 终端中输入 + + dev_server.py + +浏览器键入 http://localhost:8080 + +同 3.1 出错情况 中的 index.wsgi代码 在本地环境开发中 使用 是可以的[commit](https://github.com/JeremiahZhang/OMOOC2py/commit/a476ca008ddc01bd38f2982ea4f48f40f2f6b438) + +恩 删除 .tpl 这个后缀 就可以公网访问了 +不过又出现 直接输入文字 enter 就出错了 (本地测试是没有错的)表示伤心 + +看文档去 猜测是 核心代码 问题 恩 去修改 多坑 水 + +### 3.3 极简日志的公网访问 使用 KVDB + +想想 还是 SAE 云端读不了 log 文件的 恩这时就要使用 [kvdb](http://www.sinacloud.com/doc/sae/python/kvdb.html?ticket=4669740fb7c760d6d569da04fac5cb56947f8010#kvdb) + +恩 早该知道 kvdb 使用呀 怎么现在才想起来呢 + +然而 在kvdb 也是 来了个坑 + +走过大妈的 [大妈7-42](http://chaos2sae.readthedocs.org/en/latest/ch01/try.html#id10) + +没看懂 哎呀 花了笨功夫 恩 花了时间 为什么没有解决 + +后来看了 Alan Lai 同学 的 kvdb 部分 恩 才明朗起来 [公网版日记系统](https://wp-lai.gitbooks.io/learn-python/content/1sTry/sae.html) + + kv = sae.kvdb.Client() # 这个设置好后 然后 kv.set add 什么的 之后 push 到云端 就直接是个数据库呀 + +参考 Alan Lai 同学的代码 仿造 [修改代码](https://github.com/JeremiahZhang/OMOOC2py/commit/056718567b581607b20bc99305ebbf786e54b5c4?diff=split) + +然后push到sae 就ko 可以访问了 + +### 3.4 改换使用Jinja2模板 + +恩 对刚开始的 页面layout 感到不爽 想起上一周 Dama推荐 [jinja2](http://jinja.pocoo.org/docs/dev/) 模板 去学习了 + +尝试一个案例 https://realpython.com/blog/python/primer-on-jinja-templating/ 虽然是和 flask 使用的 但是 也换用 bottle嘛 + +[代码迭代1](https://github.com/JeremiahZhang/OMOOC2py/commit/aaa13e25c5943586a3655eadf53d95f9f9151d53) +[代码迭代7](https://github.com/JeremiahZhang/OMOOC2py/commit/4fee26bcd8ffeff6e0a2860c84920d68b45f16b8) + +好像 没有抓住主要矛盾 去搞这个去了 哭 + +学习 之后 使用模板 并进行开发 + +- 刚开始 添加一个 html 模块 https://github.com/JeremiahZhang/OMOOC2py/commit/d8657cb0f20ee6c4342fad5406944344444cf215 +- 使用 tag write 子模块 + - [tag and write 子模块 commit](https://github.com/JeremiahZhang/OMOOC2py/commit/5a4703c0f30c398d056d37d1202700d23ef8667e) + +最后 push 到 sae 的 git 库 + +公网显示结果(地址: http://jeremiahzhang.sinaapp.com/) + +![公网版效果](http://7xo9hk.com1.z0.glb.clouddn.com/5w06taskfinish.jpg) + +本地运行显示结果(localhost:8080) + +![本地运行结果](http://7xo9hk.com1.z0.glb.clouddn.com/5w07taskfinish.jpg) + +## 后续迭代 + +- [x] 网页 layout (使用jinja2) +- [x] 历史日志 倒序 + - 根据time 排序 还有点bug 得进行修改 (不同日期的 前面的日期会在前刚开始) +- [] 对笔记进行分类 tag标签 管理 + - [x] 一开始就输入一次标签 然后 所有日志输入都是在标签下 + - [] 但是如何将 上面的标签 和 日志进行关联 + - 尝试了 tag global + - 关联 tag 出现了问题 主要想不出该如何匹配在一起 +- [] 问题 + - []http://jeremiahzhang.sinaapp.com/ 历史日志 不能全部显示 但是 本地测试 是可以的诺 +- [] kvdb 日志 数据备份处理 +- [] CLI 交互 + +Friday, 13. November 2015 09:42PM 1st +Saturday, 14. November 2015 10:37PM 2try kvdb 公网访问 +Monday, 16. November 2015 10:06PM 网页layout +Tuesday, 17. November 2015 05:18PM 3.4-迭代 + + + + diff --git a/2nDev/week05_task.md b/2nDev/week05_task.md new file mode 100644 index 000000000..6103f46e9 --- /dev/null +++ b/2nDev/week05_task.md @@ -0,0 +1,33 @@ +# 日志交互公网版 + +week 5 + +雷雨 再次接到 ‘航行任务’ + +## 任务需求 + +- 将 上周web版 日志交互 发布为公网版 稳定 服务 +- 恩 需要通过 固定 域名访问系统 KO + - 每次运行时合理的打印出过往的所有笔记 + - 一次接收输入一行笔记 + - 在服务端保存为文件 + - 同时兼容 3w 的 Net 版本的命令行界面进行交互 +- CLI 监察/管理网站 呢: + - 获得当前笔记数量/访问数量 等等基础数据 + - 可获得所有笔记备份的归档下载 KO + +## Dive in + +试想并实践: + +- 如何分笔记类别 +- 如何建立认证功能, 防止有人误入 + - 私人的 不接受其他人使用 + - 可以和同学 朋友 一起使用 +- 如何建立数据加密?防止有人通过分析网络协议伪造数据提交? + +Friday, 13. November 2015 01:00PM 10 + + + + diff --git a/2nDev/week05_thought.md b/2nDev/week05_thought.md new file mode 100644 index 000000000..ce6aac107 --- /dev/null +++ b/2nDev/week05_thought.md @@ -0,0 +1,24 @@ +# Chaos + +本周比较 chaos + +在完成本周任务时 + +- 效率低下 +- 头脑混乱 +- 处理问题 没有抓住主要矛盾 + +感觉 chaos and tired +需要 refresh and reboot +不过 依然 得 Star Trek +好了 休息 休息 休息 + +找出自己的弱点 +也得 +发现自己的优点 + +Tuesday, 17. November 2015 08:18PM + + + + \ No newline at end of file diff --git a/2nDev/week06_thought.md b/2nDev/week06_thought.md new file mode 100644 index 000000000..3b5c27636 --- /dev/null +++ b/2nDev/week06_thought.md @@ -0,0 +1,18 @@ +# 慢下来了 + +本周开发 暂且结束 恩 总共的时间12.5个小时 恩期间还多次中断 + +发现 慢下来 之后 恩 开发没有以前用劲了 但是 效果还是令自己满意的 不过教程变成了开发笔记 这一点没有做好 + +慢下来 自己条理也清楚了 +慢下来 在每一天开发探索前 现将自己需要探索的 写在小纸条上 查资料过程中 不断提醒自己 不要偏离主线 +慢下来 最小原则 找到大妈的Chaos Wechat教程 恩 慢慢理解 分解 开发自己的 基本上大妈那个教程相当于帮自个儿完成了任务 只是里面自己填填坑罢了 + +慢下来 心情舒爽了很多 +慢下来 去看见成长的自己 恩 0.1 比 0 要好 +慢下来 体会学习的乐趣 +慢下来 继续去探索未知的领域 + +感恩节 感谢開智编程课 大妈 助教团队 学习小伙伴 感谢社群 感谢... + +Thursday, 26. November 2015 01:57PM \ No newline at end of file diff --git a/2nDev/week06_wechat.md b/2nDev/week06_wechat.md new file mode 100644 index 000000000..2e89cae60 --- /dev/null +++ b/2nDev/week06_wechat.md @@ -0,0 +1,283 @@ +# 日志交互 wechat 版本 + +开发环境 + +- 系统: Ubuntu14.04 LTS +- shell: zsh +- SublimeText3 (ST3) +- Python 2.7.10 +- Sina SAE +- 一个微信公众号 + +总纲 + +- [X]SAE应用 和 微信对接 +- [X]后台消息处理 +- []CLI + +## 0 初探 微信接入 + +- 创建微信公众号 + - 嗯 自个儿又申请了一个 [微信订阅号](http://kf.qq.com/faq/120911VrYVrA130805byM32u.html) 玩儿 (名为贝雅书屋) +- [微信接入指南](http://mp.weixin.qq.com/wiki/16/1e87586a83e0e121cc3e808014375b74.html) + - [x] 填写服务器配置(此处url 遇到一个梗 刚开始以为随便填写一个http://love.com 这样行之失效) + - 搜索 [问题](https://www.google.co.jp/search?client=ubuntu&channel=fs&q=%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%85%8D%E7%BD%AE+%E5%BE%AE%E4%BF%A1&ie=utf-8&oe=utf-8&gfe_rd=cr&ei=VsdNVvizJs_D8AfV6qvgBQ#safe=off&channel=fs&q=%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%85%8D%E7%BD%AE+%E5%BE%AE%E4%BF%A1+url+gitbook) + - 恩 看来 wechat要可以和 SAE云应用 对接那 上周SAE是为这周准备的 ok + - 大妈的[chaos2wechat 1.1.131010 documentation](https://chaos2wechat.readthedocs.org/en/latest/ch00/try.html#id15) 需要简单参考 + - 目前需要理清SAE和wechat对接 URL问题 恩 然后 验证服务器地址有效性 到最后实现开发 + - [x] 验证服务器地址有效性 + - [x] 依据接口文档是实现业务逻辑 + - 配置好以上 2个 才能成为 开发者嘛 + +### 0.1 服务器配置 + +#### 0.1.1 创建SAE应用 + +同5w [日志交互公网版](https://jeremiahzhang.gitbooks.io/omooc2py/content/2nDev/week05_paas.html) 类似 建立SAE应用 自个儿建立的应用名为: beiyastudy 恩所以网域为: http://beiyastudy.sinaapp.com + +SAE 应用文件目录 + + /path/2/your/sae/ + + config.yaml 应用配置 + + index.wsgi 应用根代码 + +code here [代码](https://github.com/JeremiahZhang/OMOOC2py/commit/60b21f27c09f2302d06474ac1c833ddd87c6c9f1) + +使用 git push 到 sae git 库 + +部署效果 访问 http://beiyastudy.sinaapp.com/ +可以 日志中心 查看访问日志 + +![访问日志](http://dn-jeremiahzhang.qbox.me/6w01sae.jpg) + +#### 0.1.2 建立与微信对接 + +进入微信公众号的开发者中心 进行配置: + +![服务器配置](http://dn-jeremiahzhang.qbox.me/6w02wechat.jpg) + +- 出现 token 验证错误 + - 搜索 到 [需要认证](http://www.cnblogs.com/txw1958/p/wechat-tutorial.html) + - 好吧 果然 等待认证中 + + +### 0.2 验证服务器有效性 SAE和微信对接 + +上面 等待 sina认证 恩 速度还蛮快的 一天之内 好的 + +认证完之后 还是出现 token问题 sina 实时查看问题 + +![3error](http://dn-jeremiahzhang.qbox.me/6w03online.jpg) + +想应该没问题 那么就是 第一反应 index.wgsi 文件问题 + +继续 dama的chaos + +更改并新增文件: 结构目录 + + /path/2/my/saewechat/ + +- config.yaml 应用配置 + +- config.py 全局配置 + +- index.wsgi 应用根 + +- module/ 模块 + | +- utility.py + +- static/ 静态文件 + | +- js/ # 暂时未加 以下 + | +- css/ + | +- img/ + +- templates/ 模板文件 + | +- base.html # 暂时未加 以下 + | +- 404.html + | +- ... + +- web/ 应用代码 + +- __init__.py + +- mana4cli.py # 未加 恩 + `- mana4api.py + +[代码这里](https://github.com/JeremiahZhang/OMOOC2py/commit/f4cfdab72e5cc9fcd09f5a2b773a1d264f1add0e) + +注意 在这里 遇到了一个坑 + +![4error](http://dn-jeremiahzhang.qbox.me/6w04error.jpg) + +恩 原来search 到大妈也问了 [dama wen](https://groups.google.com/forum/#!msg/sae-python/7jwgsVcGBxA/XYDSLyLGcNkJ) + +恩 手动将 [bottle] push到sae git库 然后 成功了 + +![5对接成功](http://dn-jeremiahzhang.qbox.me/6w05finished.jpg) + +*** + +## 1 用户消息-公众号自动应答 + +### 1.1 [x] 公众号自动应答 + +#### xml 数据包 + +SAE 和微信公众号 对接完成后 恩 用户发送信息 公众号没有结果的 就需要设置了 + +恩 [接受普通文本信息](http://mp.weixin.qq.com/wiki/17/fc9a27730e07b9126144d9c96eaf51f9.html#.E6.96.87.E6.9C.AC.E6.B6.88.E6.81.AF) +数据包 是 XML 形式的 恩 修改代码 [在此查看1](https://github.com/JeremiahZhang/OMOOC2py/commit/62982159d9114353860014444ee01832155ca2d6) + +代码改好之后 push到 sae git 库 在微信中发送 a + +在日志中心查看 访问日志是这样的 + +![01访问日志](http://dn-jeremiahzhang.qbox.me/6w01test.jpg) + +{刚开始没有查看错误日志 怎么也看不到 xml 数据包}只感到如下 然后 查看文档 了解 [验证消息真实性](https://mp.weixin.qq.com/wiki/4/2ccadaef44fe1e4b0322355c2312bfa8.html) + +> 在开发者首次提交验证申请时,微信服务器将发送GET请求到填写的URL上,并且带上四个参数(signature、timestamp、nonce、echostr),开发者通过对签名(即signature)的效验,来判断此条消息的真实性。 + +原来 这还是验证 需要查看错误日志才有 其实是debug了 + +错误日志是这样 + +![02debug日志](http://dn-jeremiahzhang.qbox.me/6w02test.jpg) + +#### 解析 xml + +更改代码 获取 xml 中的 content内容 [commit 代码在此](https://github.com/JeremiahZhang/OMOOC2py/commit/e2c03aa0051b6124c414bcc910d222bba2a5ede2) + +恩 在 微信发送消息 hi + +就可以在SAE中 得到发送消息 **hi** + +![hiXML获取](http://dn-jeremiahzhang.qbox.me/6w03test.jpg) + +所以进一步 可以使用 [xml.etree.elementtree 模块](https://docs.python.org/2.7/library/xml.etree.elementtree.html) 来 进行解析 + +#### 回复消息 + +解析微信发送过来的XML数据包后 就可以得到发送消息内容 之后根据信息 进行反馈 恩 + +主要使用 xml.etree.elementtree.findtext() 函数 再根据抓取内容进行 反馈 比如 微信端输入 h 我公众号自动反馈信息 为 **Haliluya! Welcome!是也乎!** + +[修改的代码](https://github.com/JeremiahZhang/OMOOC2py/commit/6d776a3e75c7b28e660b528ef22205f8171b3687) + +效果: + +![自动回复消息](http://dn-jeremiahzhang.qbox.me/6w04wechat.jpg) + +至此 公众号 可以自动应答 不过后续还待丰富 + +### 1.2 [x] 尝试本地测试 + +首先在sae 文件目录下 使用 + + dev_server.py # 本地运行 就是服务端 + + curl -d '[XML请求字串 http://localhost:8080/api/echo/ # 恩 这相当与微信端 XML请求字串 参考 日志中心 debug可以取得 + +![本地测试](http://dn-jeremiahzhang.qbox.me/6wd000test.jpg) + +这样就不用每次都 push到 sae了 本地操作 可以节省时间那 恩 但要注意在一个目录下 进行本地测试那 恩 看截图文件目录 + +*** + +## 2 Dive in 自动应答 + KVDB + +### KVDB + +官方的文档[kvdb](http://www.sinacloud.com/doc/sae/python/kvdb.html) 介绍比较简单 如下实例 + + import sae.kvdb + kv = sae.kvdb.KVClient() + k = 'foo' + kv.set(k, 2) # k 是key 2 是value 恩 dict字典也可以做value + kv.delete(k) + + kv.add(k, 3) + kv.get(k) + + kv.replace(k, 4) + kv.get(k) + + print kv.get_info() + + +上面 自动应答完成 那么就进一步探索 继续参考 开头dama的教程 [设计](https://chaos2wechat.readthedocs.org/en/latest/ch02/try.html#id14) + +> - 关注者OpenID对一个公众号是唯一固定 + +[修改代码1](https://github.com/JeremiahZhang/OMOOC2py/commit/77236587e1b33b841d893298bd105a78efa06856) + +直接本地测试(当前文件所在目录) + + $ dev_server.py –kvdb-file=mykv.db + +【注意】 首先在当前文件目录下建立mykv.db 恩 可以 touch mykv.db 如果不建立 会报错 提示没有该文件 + +恩 不懂 可以 如下 获取帮助 + + $ dev_server.py --help + +恩 可行 push 一次 但是dama的代码 要让人自己填补的 +自己在本地测试的时候 出现过 address 被占用 就是参考 --help文档来解决的 重新启用新的 host 和 port 就搞定了 恩 + + $ dev_server.py --port=8000 --host=localhost --kvdb-file=mykv.db + +### 修补代码 理解 + +这里主要是 + + if None == usr: + # 首次应答,没有建立档案 + KV.set(uid, {'em':'address'}) + content = "建立档案\n输入你的邮箱如\nem:foo@bar.com" + +在调试的时候 突然发现 恩 value 可以是字典的 然后才有 usr['em'] 修改和取值用呢 + +[修改的代码](https://github.com/JeremiahZhang/OMOOC2py/commit/bc9d381262ff15f0c16a6928c8f131f36b6f2922) + +恩 后来 就再添加几行 恩 向一个姑娘表白了 恩恩 要勇敢 勇敢 勇敢 + +[修改之表白代码](https://github.com/JeremiahZhang/OMOOC2py/commit/4dbcdca5e7103c5c30cadfe1d10fc58321f49139) + +好了 有了以上的基础 就可以实现 笔记的开发 了 只要再修改修改就可以了 恩 + +### 3 微信公众版 日志交互 + +- 0 写入帮助文档 h 可以返回帮助文档 +- 1 一行一行输入 然后 在微信端可以使用 **n:这是日志** 输入日志并保存 **hist** 参看日志 但是只是最近一次输入的 [代码迭代](https://github.com/JeremiahZhang/OMOOC2py/commit/8dfc42f983cb8ff8c0e86437b523d9b1d8a3662b) +- 2 需要将历次的笔记都返回给微信用户 恩 尝试理解 + - 官方文档 查看字典的使用 + - 使用 "".join() 函数 将 list 转化为 str + - [迭代](https://github.com/JeremiahZhang/OMOOC2py/commit/bc8830e77a1ae3e8d3f46cf847d2703c9543b71f) + - 输入的历史记录是无序的 就需要排序了 +- 3 解决 usr 字典 排序问题 + - 恩 直接sort 根据 自己定义的 key 来排序 + - 这里是代码[迭代](https://github.com/JeremiahZhang/OMOOC2py/commit/8f3790cc05486c7054b3edf581585c472797e4e4) +- 4 简单修补一些 help + - [代码](https://github.com/JeremiahZhang/OMOOC2py/commit/81d2b2b185acdf22191ba8e98a220d783741a136) + +本地测试好 就push了 后再微信测试发现 + +### 日志+时间+排序 + +- [x] 历史日志 还是无时间排序 恩 准备添加时间排序 + - 恩 查看 微信发送过来的XML数据包 本来就有时间戳 那么 直接将这个时间戳转化为localtime 好了 迭代添加时间戳 但历史日志没有加入该时间 [迭代代码](https://github.com/JeremiahZhang/OMOOC2py/commit/a0512a84f70e33b0ced117308a16dafba9442880) + - 在历史日志中加入输入日志的时间 [迭代代码](https://github.com/JeremiahZhang/OMOOC2py/commit/986b0816646f236816f459262bd07413be7cefc8) +- [x] 添加删除用户日志数据 功能 [迭代代码](https://github.com/JeremiahZhang/OMOOC2py/commit/fde49ec4353a93f70226dd0fa54f187704126130) +- [x] 还有就是 不会打印所有的日志 这个猜测是KVDB的问题 在前面的学习中也存在 没有解决 会一直留存到现在恩 的解决 + - 后来删除日志数据 好像正常了 后续留待观察 恩 + +## 4 微信版日志 CLI + +程序员 都要用 CLI 来调控 这才符合黑客条件嘛 dive in 得参考下 这个[CLI执行](https://chaonet.gitbooks.io/pythoncamp0/content/Chapter-2/%E6%9C%AC%E5%9C%B0%E7%8E%AF%E5%A2%83%E6%B5%8B%E8%AF%95%E5%91%BD%E4%BB%A4%E8%87%AA%E5%8A%A8%E7%BC%96%E8%BE%91&%E6%89%A7%E8%A1%8C.html) + +## time log + +Thursday, 19. November 2015 10:04PM 大概理解任务内容 和简单分解 形成框架 0 初探微信接入 1.5h [github ci](https://github.com/JeremiahZhang/OMOOC2py/commit/65ab07f9eb8e77615c16ebcdcf113f45fe1131f3) +Friday, 20. November 2015 09:48PM 到验证错误 等待实名审核中 1h +Saturday, 21. November 2015 10:47PM 1.5h 0.2 SAE与微信对接 +Sunday, 22. November 2015 11:13PM 2h 输入 h 自动回复消息 后修改 本地测试部分 +Tuesday, 24. November 2015 12:54AM 2h 第二部分 做事要专心 专心 +Wednesday, 25. November 2015 12:14AM 总2.5h 第3部分 +Wednesday, 25. November 2015 10:29PM 2h 第3部分 恩 + +总 12.5h 恩 时间整理以下 发现 一次只做一件事情 恩 只做一件 + +## 贝雅书屋 + +![二维码](http://dn-jeremiahzhang.qbox.me/wechatbeiya.jpg) diff --git a/2nDev/week07_qpy.md b/2nDev/week07_qpy.md new file mode 100644 index 000000000..80556930e --- /dev/null +++ b/2nDev/week07_qpy.md @@ -0,0 +1,337 @@ +# 日记交互 移动版 + +开发环境 + +- Ubuntu 14.04LTS +- Qpython CN 1.2.2 +- ST3 +- Android手机:KONKA V976 Android版本4.2.1 + +## 0 初探 + +主要是看资料 了解相关资讯 来分解任务 一步步完成 + +- [Installable Web Apps with the WebApp Manifest in Chrome for Android](https://developers.google.com/web/updates/2014/11/Support-for-installable-web-apps-with-webapp-manifest-in-chrome-38-for-Android) +- 大妈ZQ的[如何自在的折腾QPy ](http://codelab.qpython.org/pythonic/init-my-qpy-env.html#_1) +- [qpython wiki](http://wiki.qpython.org/) + - [start qpython](http://wiki.qpython.org/doc/how-to-start/#a-dashboard) + - [hello world qpython](http://wiki.qpython.org/doc/hello-world/) + - [Qpython 开发web app](http://wiki.qpython.org/zh/webapp/sample/) + - [QPy 编程向导](http://wiki.qpython.org/zh/doc/program_guide/#_2) + - python 核心 2.7.2 可自己更新 + - 三种运行模式 + - console 控制台 + - kivy 图形模式 + - webapp模式 + +*** + +## 1 浅尝 Qpyhon + +- 得下载 Qpython + - Google play 是么办法了 可以 腾讯应用宝 我用的刷机精灵 + - 恩 直接进入页面 中文版的 一看就能理解 + - 终端 python shell + - 编辑器 + - 程序 + - 库 + - 社区 恩讨论 +- console 模式 + - 使用 **编辑器** 编写了一个 简单的 hello world 恩 + - myhello.py 然后运行 恩 体验是 就是手机版的 python 恩 但是呢 手机端 输入比较恼火 +- web app 模式还没有尝试 + +以上 探查下来 手机上直接 webwpp开发是不可能了 为了符合最小时间反馈 恩 得考虑使用 本地测试 看了一遍大妈的折腾 下面准备探索 + +- 本地进行 webapp 如何进展 + - 本地PC端编写代码 + - push Qpython + - but 最小反馈 得使用 + - 本地调试 + - 得探索[fabric](http://www.fabfile.org/) + +*** + +## 2 分解任务 + +- [x] 探索 本地 push 到 Qpython 移动端 +- [x] 如何进行本地调试 最小反馈时间 minimize Check out time +- [] 极简日志交互系统 + - [x] 探索 [QPython 开发 WEB APP](http://wiki.qpython.org/zh/webapp/sample/) 嗯 这是在手机上的 现在是要在 Local 编程开发 + - [x] 移动端 + - [] CLI 命令行 + - [] 和微信接通 + +*** + +## 3 探索 + +目标 + +- [x] 如何进行 本地web app 开发 + - 已经明白 将本地代码push到Qpython + - 然后直接在本地 使用 python 命令 + - ssh与收集联通 + - 参见 **运行** 部分 如 + + # cd /storage/sdcard0/com.hipipal.qpyplus/scripts + # /data/data/com.hipipal.qpyplus/files/bin/python transhello.py + +- [x]如何 push 到 qpython 移动端 + - 发现可以将代码直接转化为[QRcode](http://qpython.com/#qrcode) 二维码 用Qpython 扫描即可 恩 这个方法也不错 但现在这个方法还是没有 本地直接推送简单 恩 转回 Push +- 主要参考 + - [如何自在的折腾Qpython](http://codelab.qpython.org/pythonic/init-my-qpy-env.html) + +### push or 上传 + +- [x]使用什么? SSH +- [x] 上传到 哪儿 +- [x] 这里得使用 BusyBox 手机也可以像shell一样了 + +#### SSH + +- 参考 [SSHDroid](http://www.upubuntu.com/2012/05/access-your-android-files-and-folders.html) +去应用市场下载SSHDroid [google play](https://play.google.com/store/apps/details?id=berserker.android.apps.sshdroid&hl=en) 并简单了解 + +那么怎么和PC链接 恩 上面 Google play 中有这样的介绍 + +> Linux users: + - File Transfer: natively supported by most file managers (like Nautilus or Dolphin), just enter the sftp address displayed as "Location". + - Remote Shell: run 'ssh' from the terminal. + +因为我使用Ubuntu 所以关注的是这个 还有 win 和 mac 平台的 + +恩 所以我要使用 ssh 怎么用呢? + +- [1 直接使用 ubuntu 的 file](http://www.upubuntu.com/2012/05/access-your-android-files-and-folders.html) 不行 不适合我的14.04 + +既然都提示要使用终端使用 ssh 了 那么 就 ssh 参考 + +- [sshdroid via terminal ](https://www.youtube.com/watch?v=uA9GCw7Nw5w) + +原来这么就这么简单 刚开始看 ssh 提示 没懂 + +![ssh使用](http://dn-jeremiahzhang.qbox.me/qpy01.jpg) + +恩 密码什么的有提示 默认为 admin +恩 被打断了 明天继续 + +#### 到底怎么使用SSH + +既然知道要用SSH 进入home之后 到底如何操作呢?明白路径 上传到哪儿 + +- SSHDroid 其实根目录为 + + /data/data/berserker.android.apps.sshdroid/home/ + + 类似Ubuntu中的终端中使用 ~ 回到的目录 + +- 要放到Qpython可读的文件目录中吧 + +> QPython 环境分: + +> - 只读执行文件起点 /data/data/com.hipipal.qpyplus/files/bin/ + +> - 可写资源起点以及目录意义: + + /storage/sdcard0/com.hipipal.qpyplus/ + +- cache + +- lib 各Python版本的库安装入口 + +- projects 俺的QPy 项目入口 + +- scripts 俺的QPy 脚本入口 + +- snippets + +- .run 恩 运行 相关 + +那么知道文件目录 那么就使用 [using SCP](http://support.suso.com/supki/SSH_Tutorial_for_Linux) SCP吧 + +> SCP is basically a program that uses the SSH protocol to send files between hosts over and encrypted connection. You can transfer files from your local computer to a remote host or vice versa or even from a remote host to another remote host. + +尝试 可以使用 + + scp ~/OMOOC2py/_src/om2py7w/7wex0/hello0.py root@192.168.2.100:/storage/sdcard0/com.hipipal.qpyplus/transhello.py + +将本地文件 ~/OMOOC2py/_src/om2py7w/7wex0/hello0.py 传输到手机Qpython的可写资源起点 并且更名为transhello.py 不过每次都要输入密码 admin 有点麻烦 效果如图 + +![scp ssh传输文件](http://dn-jeremiahzhang.qbox.me/qpy02.jpg) + +现在我将其推送到 /storage/sdcard0/com.hipipal.qpyplus/scripts 目录中 使用以下代码就可以了 + + scp ~/OMOOC2py/_src/om2py7w/7wex0/hello0.py root@192.168.2.100:/storage/sdcard0/com.hipipal.qpyplus/scripts/transhello.py + +本地hello0.py的代码如下 + + # -*- coding: utf-8 -*- + import androidhelper + droid = androidhelper.Android() + droid.makeToast('Hello, Jeremiah :-) !') + +传递过去之后 可以直接在Qpython运行脚本了 并且成功 + +#### busybox + +dama 吼道: + +> 等等! SSH 上来后,各种不适应,这什么 shell 环境哪,连 less, tail, vi 都没有 好意思说自个儿是 Linux 嘛?! +搜索才知道,这货叫 ash + +> 躲在 /system/bin/sh 基本上什么也不会作 + 所以,程序猿有 [BusyBox](http://www.busybox.net/FAQ.html#getting_started) + 一安装,批量将大堆习惯的工具,灌到 system/xbin/ + +OK 手机下载 BusyBox 恩安装好之后 就可以使用 less tail vi 等命令了 可以直接对手机里面文件如同在PC本地机一样操作了 恩 + +#### 运行 + +dama吼: + +> 好了,进入下一个阶段: 怎么让 QPyhon 如系统命令一样在 ash 环境中调用我们的脚本? + +- 使用 [gen_qpy_env.py](https://gist.github.com/ZoomQuiet/8642464) + - 推送到Qpython scripts 目录中 并使用Qpython 用脚本形式跑起来 + +> 肿么,让 shell 环境每次都能自动加载上这堆配置? + +按照dama而言 直接使用其代码:(恩 是进入ssh 与手机连接后 的 shell 下) + + # mount -o remount,rw /dev/block/mtdblock3 /system + # ln -s /storage/sdcard0/com.hipipal.qpyplus/projects/qpy_profile /etc/profile + # mount -o remount,ro /dev/block/mtdblock3 /system + +手机重启 SSHDroid 远程ssh登入 测试 + +![shell自动加载](http://dn-jeremiahzhang.qbox.me/qpy03.jpg) + +然后 远程人工使用 shell 调用 我scripts 文件下刚刚从本地传输过去的 transhello.py 文件 + + # cd /storage/sdcard0/com.hipipal.qpyplus/scripts + # /data/data/com.hipipal.qpyplus/files/bin/python transhello.py + +不论手机在什么界面下 会跳出 **transhello.py** 运行结果 + +#### 自动 + +> 好了,进入最后一个阶段: 怎么让所有的上传->配置->运行->日志收集 全部自动化一键完成? + + + 自动上传?!(怎么通过公匙的部署,来达到无口令远程登录手机?) + 自动运行手机端的脚本?! + 自动收集日志? + +- 探索 使用 [pexpect](http://www.noah.org/python/pexpect/) 查看doc 文档 另 [Pexpect V4](https://pexpect.readthedocs.org/en/stable/) + +恩 DAMA 那部分没读懂 + +> 先将本地公匙用口令登录的方式推上手机 + +发现好像有点远 暂时回来 直接fabirc先 + +#### Fabric + +- 本地安装Fabric +- 直接使用Dama的 fabfile.py文件 放到本周开发目录中 + - 简单修改参数 就可以 + - 本地进行代码编写 + - **fab qpy:script=当前调试脚本.py** 一键上传到手机 并自动运行 + - 在手机上真实操作 好自动化 + +此处 我将自己的文件 放入 qpython projects中的chaos项目中 并命名为 **hello.py** + +然后直接 在本周任务目录下 使用 + + fab qpy:script=hello.py + +OK 直接推送并run hello.py 代码 因为没有设置上一步的自动 所以还要输入几次密码 + +- admin +- 本机管理员密钥 +- damin + +感觉每一次这样 还是有点重复 恩 没办法了 先这样 + +日志的运行 暂且不查看了 + +[ci 代码 copy dama](https://github.com/JeremiahZhang/OMOOC2py/commit/a61bd4ecb1dfd8b7418f85f1388d32306ea8ecb8) + +*** + +## 4 Qpython Web 本周日志开发 + +以上只是为了checktime少 经历了这么多时间 + +### 4.1 初探 Qpython Web APP + +[Qpython 开发 WEB APP](http://wiki.qpython.org/zh/webapp/sample/#_1) + +- WEB APP 框架 +- 使用SL4A 接口获取地理位置信息 +- 调用安卓体系内其他 APP 的公开接口的方法 + +完成代码 main.py 在此目录下使用 + + fab qpy:script=main.py + +发现 warning + + Warning: Permanently added '192.168.2.103' (RSA) to the list of known hosts + +恩 想到 dama 那会也遇到类似 + + sed -i -e ’36d’ ~/.ssh/known_hosts + +修改还是不对原来是 变成了这个 **192.168.2.103** 修改后 成功 绕了一下 :-) + +提交Qpython的代码 直接运行后 可行 + +### 4.2 极简日志交互 + +目测 需要和 公网版orWeb版 类似 需要 + +简单 search 找到 [Python Programming with QPython](http://www.pinoyden.com.ph/index.php?topic=289814.0) 里面有较多教程可以学习 恩 + +#### home 页面 + +简单一个home页面 出错。。。 + +[ci 代码](https://github.com/JeremiahZhang/OMOOC2py/commit/8c7e5b750fe91c2a8f95078f1ced6c79c90cb04a) + + web app launch timeout(10) + +恩 再议 + +测试发现 原来主要是 + +- 路径目录没有设置 ROOT +- app.route('/', method='GET')(home) method = POST +的 改为 GET 就OK了 +- 注意 这里我直接将 数据文件mydiary.db放入了Qpython的项目目录中 + - 尝试 直接使用在 home 中添加自动新建db文件 发现 会造成不必要的麻烦 暂时不考虑 直接将db文件放入项目目录中 + - 可以尝试 自己建一个脚本 如果要新建数据db文件 就执行 恩 + +[ci 代码](ht) + +#### write 文字保存 打印日志 + +思路是这样 + +- 将html中的内容抓取过来(第4w已经过了) +- 保存入sql数据文件中([sqlite3 文档](https://docs.python.org/2.7/library/sqlite3.html) ) + +恩 中文有输入影响么 目测有 实际输入真的有 因为在html 加入了utf-8 出现错误提示 恩 直接将中文tag和 content内容加 decode('utf-8') 解码 + +[ci 代码 GET 和POST 导致耽误时间了](https://github.com/JeremiahZhang/OMOOC2py/commit/2e45a7731397b092337eeed55413dd639e7fb845) +[ci 代码 打印日志](https://github.com/JeremiahZhang/OMOOC2py/commit/d1d92774ba99dbb5e4d604b48b399d5a0b2ec660) + +效果: +![交互日志 效果](http://dn-jeremiahzhang.qbox.me/qpy04.png) + + +Friday, 27. November 2015 10:14PM 2h 初探+笔记 未达到心流状态 恩 反省过程中被打断 得手机断开 ok +Saturday, 28. November 2015 11:22PM 1.5h 分解任务于 ssh部分浅尝 +Tuesday, 01. December 2015 12:02AM 3h ssh登录 并传输 自动busybox + 自动 + fabric +Tuesday, 01. December 2015 10:46PM 2h 4.1 部分 +Wednesday, 02. December 2015 11:37PM 2.5h home 页面部分 but error +Friday, 04. December 2015 04:58PM 4h 4.2 home 和 write debug 这一段陷入一个醉点 以前做过的 现在还出现问题 说明以前的还不够明白呢 + + + + diff --git a/4Learn/0wd4-OpenCourse-Note.md b/4Learn/0wd4-OpenCourse-Note.md new file mode 100644 index 000000000..eaa99f0f8 --- /dev/null +++ b/4Learn/0wd4-OpenCourse-Note.md @@ -0,0 +1,125 @@ +# Rewminding in Coding and Everything + +10/15/2015 11:53:50 PM 听完大妈的公开课 总括是 + +- **编程思维**的培养必须要**编程** +- **靠谱行为**的基础只能是**靠谱** +- **提问**是获得技能的唯一起点 +- 教练价值源自你**真诚**的行动 +- **合意阻滞**是学习的最佳动力 + +> 开智五品 + +> - 勤奋 +> - 专注 +> - 持久 +> - 创意 +> - 良善 + +任务如下 + +- 前提: 参加 0wd4 公开课,或是看过回放 +- 时限: 1wd0 20:20 前 +- 行动: 回复此 Issue 给出对应笔记的 URL +- 内容: + + 公开课对自己触动最大的 top 3 内容 + + 对比自己过往行动, 应该有哪些可以应用以上三点建议进行改进的? + + 给出具体改进的方案来 + + 方案描述尽可能包含所有 5W1H 要素 + +10/16/2015 12:06:37 AM + +![mindmap手绘思维导图](https://raw.githubusercontent.com/JeremiahZhang/gopython/master/_image/0wd4_C2T2_novie_advice.jpg) + +---------- + +## 触动点 ## + +- 断言 +- 慎独 +- 视而不见 + +## 1-那个断言 ## + +在大妈昨天(10月15日)晚上的公开课中 大妈断言 “不会提问 就学不会编程” 被这个断言深深击中 怪不得大妈在课程刚开始的时候 贴了多次的 [提问的智慧](wiki.woodpecker.org.cn/moin/AskForHelp) 且还说道 当我们自己懂得提问的时候 大部分问题 我们就可以自己解决了 + +学习编程 其实是发现问题 解决问题的过程 而程序员就是以最小代价解决问题 但我该如何升级 在编程学习中 如专业or专家级程序员一样 去以最小代价解决问题呢? So in the beginning I should ask for help smartly. + +学会提问 编程思维习得的门槛 + +不会提问 其实是思维懒惰的表现 是要不得的 思维的懒惰 会造成行动的懒惰 所以学会不编程 + +我在最近的编程学习中 跟着官方Tutorial走(并[记录所学](https://github.com/JeremiahZhang/gopython/tree/master/PY-StarTrek-Enterprise/0w))总有一个想法就是习得的是python的术 而不是编程的道 从大妈的断言看来 编程的道起步就是**学会提问** 之前也学过VB 学过C 但是总体而言 没有习得编程思维 就因为没有真正学会提问 当看到技术问题提问时 一大堆的内容 就有点害怕 问个问题 还要那么多自己尝试那么多 还问问题干嘛 **原来学会问问题就是在自己解决问题** 现在才慢慢get到 被学校教育所奴役的! 后怕啊! + +- 行动 + - 不懂就问 但是在提问之前 请学会提问 参考 提问的智慧 + - 提问之前做好准备 叙述好自己的问题 + - 知道自己问的什么 描述清晰 + - 自己的分析如何 内容过程 + - 做了多少尝试 尝试的内容 尝试的结果如何 + - 为什么最终没有成功 + - 最后再去提问 + - 问题解决 简要说明 + - 要真诚(自己真的经过一番分析 尝试 而不是一上来 就把问题扔给前辈们) + - 要谦卑 要谦卑 要谦卑 + +---------- + +### 2-如孩子般慎独 ### + +大妈提到 小孩子的慎独 似乎是 + +- 独自一人时 要小心谨慎 +- 比喻 无监督学习 +- 例子 小孩的慎独 自玩泥巴 自得其乐 + +> 在无人监督的时候 自己跟自己的问题 自己跟自己的问题搏斗 + +当大妈讲到学校的教育的时候 想到 +其实学校成为了一个**屠宰场** 抹杀了孩子的兴趣 天性 +学校想培养的是顺从的羔羊 + +回忆自己小学时候 放学回家 用硬纸板去制作各种类似抽屉的事情 然后去放文具什么的 还乐意给同学做 完全是在无监督下学习 玩耍的 哈哈 + +上了快20年的学 发现自己的这种无监督学习的天性 消失了 在学校为了成绩争竞 为了奖学金而学习 即使有自己想学的 比如本科时候的日语 也因为没有坚持 在没有监督的情况下 放弃了 + +**慎独** 在学习的时候回归小孩子般的慎独 同时拥成人的成熟 去体会其中的乐趣 而不是那做给别人看 输出令自己满意的作品 能打动自己的作品 然后再去打动别人 + +> 去与自己的问题 与 思想 搏斗吧 +> 在独处时能谨慎不苟 + +---------- + +### 3-那么多视而不见 ### + +大妈在本次公开课中 提到好多次的视而不见 其中我一次在issue中的回答就被拿出来和大妈的回复举例 当初也想到 + +- 咦 为什么大妈的回复下面总有那么多摘引 然后就没有然后了 这是我思维的一个漏洞 只有为什么 没有解决的方案 似乎问题就解决自己了 其实是自己思维的懒惰 感谢大妈的多次提醒 + +在讲座后发现 原来 大妈是用email来回复的 自己也将Github的primary邮件改为Gmail,发现原来Github配合email就和邮件列表类似了 这就是Github生态中的一环 + +开课到如今 有好多视而不见了 反省下来 + +- 想想为什么 + - 思维上 认为不怎么重要 但大妈多次提到视而不见 想想 + - 大妈每一次邮件or任务内容中给的link连接 都是有他之用意的 要不他也不会浪费时间将无用的link去发了 这个需要get到 +- How + - 注意大妈每一次提问 回答的方式 + - 注意大妈email中的link + - 先简单浏览 + - 对自己发现问题 解决问题有用之处 特别审视之 + +---------- + +## 总 ## + +- 学会提问 是思维的变更 解决编程问题 也可解决生活中的问题 +- 慎独 不断与自己搏斗 +- 不再视而不见 乃是信任导师 + +谢谢ZQ大妈的公开课[0wd4 C2T2](http://openmindclub.qiniudn.com/res/tapes/omooc/omooc2py/151015_0wd4/index.html) +修补自己思维的漏洞 还得细听 继续修补 不断更新自己 + +> Rewminding in Coding and Everything in My Life + +10/16/2015 11:37:30 AM 3个蕃茄 \ No newline at end of file diff --git a/4Learn/2015-9-26-HiddenMission.md b/4Learn/2015-9-26-HiddenMission.md new file mode 100644 index 000000000..96e817a8f --- /dev/null +++ b/4Learn/2015-9-26-HiddenMission.md @@ -0,0 +1,104 @@ +# 开智 Python 入门体验 # + +---------- + +## 行动 ## + +- 从任何渠道挖掘 开智学院 编程课程 **首届** 旗舰班 的 所有信息 +- 记录你的挖掘路径以及成果链接 +- 从中选择出最打动你的三点,整理陈述理由 +- 回复在此 Issue 之后 +- 也可以用其它形式回复,然后回复 链接 + +嗯 我的一个误区: +> 注意是首届啊 我刚开始时候 潜意识中以为大妈写错了 应该是PY二期的吧 后来仔细看了几遍 嗯 猜测大妈在这里可能设置了一个trap +> 因为最近 开智 都在宣传 二期 我脑子的意识中 会被这种 无形 的二期的宣传信息而占据 而忘记是首届的旗舰版 +> 好 看看 有多少同学会不会也会有这样的想法 待验证 哈哈 + + +---------- + +## 挖掘 ## + +- 要首届的话 这期开智是在github上搞issues的 那么 首届的应该也在github上 + - 嗯 在github上挖掘到开智微课首届编程课程的repo [OMOOC.py](https://github.com/OpenMindClub/OMOOC.py) + - 哈哈 to be honest 我在一期的时候就已经关注了 嗯 接近源头的好处 +- 追随repo中的[学院仓库及作品追踪](https://github.com/OpenMindClub/OMOOC.py/wiki)找到[一期背景资源](https://github.com/OpenMindClub/OMOOC.py/wiki/Omooc1stIndx) +- 课程导师训练 重要的是看学院的作品 作品展示 那么当然是要看最佳作品啦 人总是要往高处走嘛 + - 嗯 因为在开智青年群 已经知道了最佳展示作品 [小小书童 demo](https://vimeo.com/129516137) 酷炫实用 嗯 开智首届 dama法力无边 + - 之后呢 嗯 在一次PY预备微信群中 大妈的whatifcoding中 有群友(好像是闪闪)提到大妈的每一次录音都会有备份 那么就去博客中找一期是否有录音 还有大妈也提到**只要点击4次就能找到一期的信息** ok 找来[Dama一期omooc记录](http://wiki.zoomquiet.io/omooc/) + - 其实 在还没开二期的时候 我就已经保存了这个链接 因为我是铁定要报二期的 一期的当然留用(那么上面的推理逻辑在我不知道的情况下也应该是有用的) + - 然后呢???一休推荐了优秀学员FrankHu的学习记录gitbook 嗯 在群里阳老也推荐了 [Frank_Hu的gitbook](https://frank-the-obscure.gitbooks.io/pythoncamp0/content/index.html) + +以上一些资源 参考学习 可以了解开智编程课程首届旗舰版的信息了 + +咦 又挖掘到Dama的Youtube[Omooc.py](https://www.youtube.com/playlist?list=PLToFpvpg6EgR1V0wt2F8zmJWnPWQXAEpc) +继续挖 [简书Omooc.py](http://www.jianshu.com/collection/3ef9b576fac8) + +---------- + +## 最动人的三点 ## + +嗯 三点 +> 大妈你是不是在珠海一直去沙滩 去看比基尼美铝啊 OK just kidding + +嗯 这个三点需要陈述出来 需要好好想想 先休息 为了迎接开智编程 养成早睡早起习惯 暂不熬夜 在需要熬夜的时候熬夜 ok 洗洗睡 晚安 Python GO to sleep time:9/26/2015 11:03:16 PM + +嗯 将挖掘到的内容简单浏览了一遍 尤其是大妈的[Dama一期omooc记录](http://wiki.zoomquiet.io/omooc/) 浏览下来(录音听完了week2 后面的以后慢慢听 ) 最打动我的那“比基尼”三点: + +- **不止编程 乃在思维** + - 一切技艺的关键 是**人** 一切方法的关键 也是人 所以人是主因 然人受大脑控制 **思维**又决定了我的学习 + - Py一期 注重的是**思维训练** 但改变思维不是那么容易 所以在学习编程中改变思维 是一种有效方法 + - 只有在**实践 练习** 中改变思维 而不是阅读那些称之为 改变XX思维 的书来改变思维 + - 那么 开智的编程课程 就是**改变思维**的过程 是在实操中锻炼纠正思维 + - 注意 在这门课中 大妈是辅助 大妈会在每一次QA中点醒人 最关键的还是我自己 具体的说 是我的思维 希望更多地明白大妈的“梗” +- **探索会学习的自己** + - 人 生来会学习 看婴儿学走路 看小孩对这个对那个好奇 都是在这个未知的世界中 + - 只是在国内的教育体制中 我的许多同学渐渐失去了那个会学习的自己 变得懒惰 + - 这是一场旅程 是探索会学习的自己的旅程 我将它成为 Python Star Trek +- **分享 交流 友爱** + - 互联网 联系的网络 分享的网络 主动联系 主动分享 希望自己能做到 不要怕自己出错 说错 犯错 越怕 越是要克服 越是要说出来 我的脸皮还得修修厚 。。。 + - 看到了 快学慢帮 很赞 有同侪压力 但**两个人总比一个人好 因为二人劳碌同得美好的果效** 需要互帮互助 需要 主动 分享交流 + +### 总 ### + +真的不只是学习编程这么技艺 而是在修炼自己思维 +不只是在上课 而是在认识更多的朋友 + +最后还是用那句在 程序员修炼之道 中看到的那句话 来提醒自己在接下来的学习中 要大胆 主动 脸皮厚 担当责任 + +> 在所有弱点中 最大的弱点就是害怕暴露弱点 + +希望在12周的 Py二期中 改变思维 学会探索会学习的自己 认识更多的朋友 + +9/27/2015 3:19:31 PM + + +---------- + +## 学员团队 ## + +那个被触发的隐藏任务 在看到大妈在这次隐藏任务中的回复 + +> great! 任务埋下 24 小时以内被 digg 出来了, mark! 学点+1 @OpenMindClub/py2staff logging it! + +在对比大妈在welcome邮件中 + +> 学员列表: omooc2py@googlegroups.com +学员团队: @OpenMindClub/py2student +模板仓库: https://github.com/OpenMindClub/OMOOC.py/tree/primer2 + + +其中只有学员团队 @OpenMindClub/py2student 这个是没有链接的 + +联系大妈的回复 看来 学员团队如同老师上课学员名册一样 + +- 帮助大妈及助教们跟踪观察学员的学习情况 +- 进行反馈 +- 分组 + +ok 不知道对不对 但为什么大妈回复中的那个 [link](https://github.com/orgs/OpenMindClub/teams/py2staff)会出现404呢??? + +咦 我发现自己越来越喜欢这种去探索 去猜测的游戏了 娃哈哈 + +9/27/2015 4:07:13 PM \ No newline at end of file diff --git a/4Learn/2wd0_stupidtime.md b/4Learn/2wd0_stupidtime.md new file mode 100644 index 000000000..304f40ed9 --- /dev/null +++ b/4Learn/2wd0_stupidtime.md @@ -0,0 +1,55 @@ +# 笨功夫 # + +## 什么是笨功夫 ## + +- [笨](www.zdic.net/z/20/js/7B28.htm) 查询到有3层意思 + - 1 不聪明 + - 2 不灵巧 + - 3 粗重 费力气的 +- 功夫 + - 乡下一直说 `这个要下功夫` 就是花时间与精力的意思 + +那么结合在一起 `笨功夫` 看来是 没有经过头脑**认真思考**的时间 + +这么说 没有花没有经过大脑认真思考的时间 就是下笨功夫 在过去的经历中那些算是笨功夫呢? + +- 上学时候的抄作业 +- 大学做物理实验后 抄的实验报告 +- 近期学习python时 没有认真思考后的搜索 + +**栗子1** + +1 最近解决`自动登录网页`问题 刚开始搜索的时候 将自己的想法(自动打开浏览器 打开指定网页 自动登录帐号密码登录)简单罗列 并进行搜索 在stackoverflow上搜索一个提问 关于登录的问题 初步了解到要用twil却没有思想这个对我解决问题是否真的有用 twill要达到的效果真的是什么 尤其在阅读其官方文档的时候 忽视了其重要的介绍 + +> twill is a simple language that allows users to browse the Web from a command-line interface. + +在CLI中浏览网页 嗯 我没有认真分析上面的话 以为可以从CLI中输入命令 然后调用浏览器去浏览网页 + +之后进过一系列进行学习 发现没有可以实现我的想法的命令呀 后来 回读Twill介绍的时候 我发现原来是自己英文分析不到位 没有认真解读理解 +总的来说 这花的时间足有2个小时 从阅读Twill文档 下载Twill使用 搜索各方面的资料尝试解决自己的问题 + +这就是因为自己没有认真分析Twiil开头介绍的那句话 而导致自己花了`笨功夫` 这里我居然“坚持”探索了2个多小时啊 + +如何避免下这样的`笨功夫` + +- 认真阅读介绍(认真思考) +- 理解Lib的真正用途后 +- 再进行下一步探索 + +**栗子2: 中文编码问题** + +在编写一个[Google搜索脚本](https://jeremiahzhang.gitbooks.io/omooc2py/content/1sTry/1wd5_go_google.html)的时候 出现的中文编码问题 一直没有解决 当时只想着快点解决问题 就只管搜索尝试如何解决问题(1个多小时未果) + +后来累了 发现没有冷静分析与思考 该了解的编码背后基本原理 之后再尝试去解决问题 + +起码基本原理知道 以后遇到编码问题 处理起来更迅速 或者不会出现编码问题了呢 + +## 如何避免下类似的`笨功夫` ## + +解决问题或做一件事之前 准备一张纸 + +- 简单罗列自己的问题 解决思路 (或事情) +- 一定时间进展记录 (一个番茄钟吧25mins) +- 不断review自己的所得 并进行改进等等 + +10/25/2015 \ No newline at end of file diff --git a/4Learn/3wd3_monthreview.md b/4Learn/3wd3_monthreview.md new file mode 100644 index 000000000..275849f44 --- /dev/null +++ b/4Learn/3wd3_monthreview.md @@ -0,0 +1,24 @@ +# 破壁与坚持 + +開智python的课程 快一个月 3次公开课 任务下来 然后你自己去探索发掘 不懂的再问 一个月之内 我问过的技术问题 个数为0 为什么 + +- 不好意思问 自己花点时间能解决的 恩 还是没有破壁呀 +- 其实发现还是自己懒得问 不懂提问的智慧 得学 立马学5W1H +- 提问嫌麻烦 这是最大的弊病了 + - 自己不好好梳理一下问题 随别问个问题 别人凭什么给你解决 + - 潜意识中 自己梳理了问题 干嘛还要问问题了 你难道还么有开窍么 问问题 就是在解决问题 解决问题 从问一个好问题开始 + +破壁 一定要破壁 是克服自己的惰性 破掉自己思维的那堵墙壁 + +回顾过去一个月 学习编程 恩 开始的时候兴奋满满 恩 现在激情貌似减了 因为自己论文方面存在着压力 头还疼着 学习的时间比之前少了 不过你可以继续坚持下去 C.S.Lewis 说过: + +> 生活中刚开始对一种事情好奇 充满激情 随后就会消失 +只要坚持下去 逝去的那份最初的激动都会通过一种更内在 更持久的兴趣得到补偿 +最重要的是 正是那些乐意接受逝去的激动 安于这种冷静兴趣的人 才最有可能在一个全新的领域 发现新的令人激动的事物 + +我愿意接受那份逝去的激动 我愿意继续去坚持和探索 自己未知的领域 就这么坚持下去 去发现新的令人激动的事物 + +Go next generation on enterprise + +星期三, 04. 十一月 2015 08:40下午 + diff --git a/4Learn/4wd4_Rbrain.md b/4Learn/4wd4_Rbrain.md new file mode 100644 index 000000000..242ee3022 --- /dev/null +++ b/4Learn/4wd4_Rbrain.md @@ -0,0 +1,59 @@ +# Star 深呼吸-换换脑子 + +![雷雨的自画像](http://dn-jeremiahzhang.qbox.me/20151111自画像.jpg "可以这样") + +以上 雷雨的自画像 + +之前看完了 Pragmatic Thinking and Learning 这本书 里面提到 + +> 很多时候你难以看清摆在面前的事情 因为你已经习惯于通过某种特定的方式来看待模式 + +雷雨 想 这就是思维定势了 如何摆脱呢 雷雨的方法 + +- 进入R模式:散步 听音乐 绘画 +- 睡觉吧 + +#### what + +雷雨在1个多月的pyhon star trek 中 发现思维方面的漏洞 比如: + +- 陷入无限倒退(搜索方面-互联网数据 链接太多) +- 最小代价解决问题 意识还不清晰 +- 总是急着想解决问题 却忘起初目标 + +#### when + +雷雨想 什么时候会出现这种情况 + +- 去作任务 编程 搜索的时候 +- 其他方面学习的时候 + +#### how + +那么 雷雨还是想提醒自己这样解决 + +- 目标清晰 +- 慢慢深入 +- 累了就进入R模式 +- 不断反馈 是否于目标一致 + +雷雨 深呼吸 换换脑子 + +## 推荐书籍 + +1. [Pragmatic Thinking and Learning - Refactor Your Wetware 程序员的思维修炼](https://book.douban.com/subject/5372651/) +2. [A_Whack_on_the_Side_of_the_Head](http://www.goodreads.com/book/show/145947.A_Whack_on_the_Side_of_the_Head) +3. [像艺术家一样思考IV](https://book.douban.com/subject/3070038/) 上面的自画像就是雷雨看了这本书 第一课后 自画的 +作者说 + +> 绘画的技术是可以通过别人的传授和自身的学习掌握的 它不是什么神秘的魔法 也不取决于什么遗传天赋 + +是也乎 编程也是一样 每一样技术 都需要日积月累的刻意练习 + +*** + +## ( ̄▽ ̄) + +Go Enterprise + +星期四, 12. 十一月 2015 diff --git a/4Learn/4wd5_renewminding.md b/4Learn/4wd5_renewminding.md new file mode 100644 index 000000000..0974e7170 --- /dev/null +++ b/4Learn/4wd5_renewminding.md @@ -0,0 +1,91 @@ +# 从行动开始 +雷雨昨天(星期四, 13. 十一月 2015) 观看了大妈的公开课 +恩 大妈讲的很精彩 + +恩 雷雨就在昨天 又失眠了 脑子里面 思考着大妈的公开课 + +## 入门 + + 市面上很多 XX入门到精通 见的好多 雷雨现在觉得 好反雷雨的人性 逆心智啊 + +触发是 有同学问大妈 编程该怎么入门 + +大妈说: 入门 从来就没有那个门啊 + +雷雨想: + +嗯 本就无门 何来入门 那为什么会有 这么多 XX入门到精通 呢 为什么呢 为什么呢 为什么呢 雷雨写下了自己找的一些原因: + +- 思维习惯 + - 因为在中国的学校 题目一直是有标准答案的 + - 雷雨想 除了 算术有正确的答案之外 这个世界只存在唯一一个标准答案:人故有一死 + - 在学校里 早期形成的思维习惯 恩 有那么个标准答案现在成为 一种枷锁了 +- 教育 + - 都是被教育出来的 都是被中国的教育教育出来的 + - 雷雨还记得自己刚上初一那会 数学老师让雷雨上去做一道数学题目 恩 雷雨用了负数表示 老师看到 就批评了我一顿(如果没有记错 老师后来教训我 还打了我呀...) 我还没有教 你就写出来了 唉 现在想来 what a joke! + +恩 雷雨在吐槽教育了 o(∩∩)o... + +中国教育 不是在教雷雨 成为一个人 而是成为一个奴隶呀 不过想想 不在北韩 雷雨也算是 比较幸福了 + +约400年前 柯美紐斯大神 在《大教学论》中说的可好了: +> 并且教导青年的方法通常都是残酷的 以致学校变成了儿童恐怖的场所 变成了他们的才智的屠宰场 + +恩 还是相当恰当的呀 教育得师法自然 + +好了 编程没有 那个入门的 门 只要去入就好了 这个世界也没有唯一正确的答案 也没有标准答案 除了死亡 雷雨在这个世界上 不过是客旅 当下 就享受编程吧 不要再去管 那个门了 + +当下 换换脑子 恩 任何技能 只要去做 去用了 就已经入了 就看自己想不想深入了 + +## 课程核心 自省 + +恩 大妈再一次表扬了 准妈妈 Zoe get 到了 这次课程的核心目标 + +雷雨参加这门開智课程 之前 就知道这课程 注重的是编程思维 但是雷雨在编程的时候 没有意识会去遵照了原来的思维习惯 去编程 没有 + +- 学会提问 +- 以最小代价解决问题 + +不过现在 想来 认识到这些 并慢慢去纠正 就是在习得编程思维 在做中学 然后习得永远是一个过程 没有 end 因为思维可以想 Galaxy 一样无限 + +嗯 雷雨想 只要在编程中 习得编程思维 没有二法 就像 台大史记公开课那位老师说的 从读历史中 去习得思辩 + +恩 那些 类似 获得xx思维的xx种法则 从来都是骗人的嘛 + +哈哈 雷雨在双十一的时候 买了史记 准备开始读呢 + +从实践中学习 行动比嘴里说的可靠 + +## 自信 + +得更加自信 + +雷雨 之前再面对问题 也是想着难处 能不能解决呀 得自己 时刻脑子里翻滚: + +- 我能 +- 我能 +- 我能 + +KO just do it + +## PS + +- 经验 +- 自省 +- 积累 +- 账单 +- 以最小代价解决问题 + +从行动中开始 + +- Google Python Style + - 英文版 看了一半 + - 中文版 过了一遍 + - 雷雨得在自己的代码中 实践 +- 恩 附上雷雨学习 绘画的草图 雷雨的手 + +![雷雨的手](http://dn-jeremiahzhang.qbox.me/20151112hand.jpg) + +星期五, 13. 十一月 2015 09:57上午 2 tomatos +星期五, 13. 十一月 2015 10:22上午 补图 + diff --git a/4Learn/5wd4_mindtrek.md b/4Learn/5wd4_mindtrek.md new file mode 100644 index 000000000..ad7176e01 --- /dev/null +++ b/4Learn/5wd4_mindtrek.md @@ -0,0 +1,127 @@ +# Mind Trek + +開智编程 是一场思维的旅行课程 +只有在行动的过程中 逐渐改变自己的思维 + +*** + +## 起初 + +開智编程一期的时候 没有来得及参加 +二期開班了 正好自己休学在家 正好有时间和精力 +主要目的 习得编程思维 改变自己思维习惯 去除自己那些多余无益的想法(那些无益想法 退却的想法 折磨得我头痛阿) + +## 过程 + +- 0w 开课 兴奋 终于开课了 大妈的风格 太棒了 + - 梗多 有趣 + - 每周BGM 愉悦 + - 带有魔力的笑声 + - 可执行的42行代码 +> 同时包含了几乎 80% 的 Python 代码常见情景. +- 1w 日志交互 CLI + - 进入 终端 进入程序员的日常 + - 体会到实际自己可以编写出一个属于自己的程序 + - 以前 那些vb+C教程 简直就 不是带领进入编程世界的 +- 2w 日志交互 GUI + - 想着可以及时完成任务 没有好好写笔记 + - 并没有思考最小代价解决问题 +- 3w 日志交互 NET 版 + - 从window 切换到 ubuntu 感到新鲜 舒服 + - 还是只想着及时完成任务 任务算是完成 笔记不是很理想 + - 激情好像有点消散了 [破壁与坚持](https://jeremiahzhang.gitbooks.io/omooc2py/content/4Learn/3wd3_monthreview.html) +- 4w 日志交互 WEB 版 + - 还是想着可以及时完成任务 + - 这次完成了任务 走了较多坑 + - 只有此次 教程 是自己满意的 以前的只是自己的开发笔记而已 不过太过于纠结细节了 + - 头脑得时刻翻滚 I can + - 将学习过程 隐喻化 会带给自己更多的乐趣(将自己的目录 和探索内容 改为自己认为有趣哒 给学习加个甜点) +- 5w 日志交互公网版 + - 【触发】 + - [Alan Lai](https://wp-lai.gitbooks.io/learn-python/content/1sTry/sae.html) 同学的秒交 使我==急==着想完成任务 + - 【想法】这周做的比较累 [chaos](https://jeremiahzhang.gitbooks.io/omooc2py/content/2nDev/week05_thought.html) + - 感到同侪压力大 虽然在issue中留言 要保持自己的节奏 但是发现 自己的节奏被打乱 + - 恩 没有耐心去看源文档 + - 恩 流串于非源文档中 缺乏了自己的思考和耐心验证 + - 争竞的心 使自己着急 在做本周任务的时候 头脑是真的混乱 + - 【拐点】 + - 本周任务简单完成后 对结果我自己是不满意的 不过算是松了口气 但反省自己这周的任务过程 意识到自己的缺陷 *性急 太关注结果 忽视过程* + - 是长年累月在教育下形成的思维牢狱 + - 思考 如何改变? 需要自己实际修炼 并且是在实际中 而不是*载之空言* + - 在此次编程中引起自己的重视了 + +*** + +## 获取 + +课程中强调 + +- 提问 +- 源文档 +- 以最小代价解决问题 +- coding 得循 [MVP](https://en.wikipedia.org/wiki/Minimum_viable_product) + +经历6周的编程学习 不知不觉 发现 + +- 慢下来 慢下来 慢下来 (5w任务后) + - 冷静分析 自在思考 + - 6w 任务 下来了 再也没有 着急的心思了 各任务是相通而又独立 耐心去阅读 源文档去解决问题 + - 多次尝试失败后 请真诚地去提问 +- 接近源头 重要性 + - 阅读源文档 起初看上去 进程会比较慢 但是经过一段时间的磨练之后 转化为自己的内隐经验(就像骑自行车经验一样) + - 源文档 刚开始全英文 会比较难 没有头绪? 理清头绪 (序) + - 并转化为自己的经验之后 效果是慢慢体现的 不止在编程中 任何学习中也是如此 +- 经验 + - 自己走过 虽然有点累 但收获真的是不一样的 + - 好记性 不如烂笔头 每日都得有备份 + - 获得自己最小代价解决问题的办法 包括 coding 中的 MVP + - 教程 or 笔记是重要的 每一次 都是自我反省 并 理清思绪 +- 学习需要些隐喻 来使得自己保持兴趣 + - 比如 将自己目录改变为自己喜欢的电影系列 Star Trek 的 metaphor 自己感觉比较有趣 也愿意去 trek 再辛苦也不怕 + - 恩 大脑就是这么容易受*欺骗* + +其他变化: + +- 昨天朋友和我聊其在工作的不满时 我不知不觉去使用 5W1H 并帮助她该如何解决这个不满了 +- 嗯 发现我之前 没有信心 进行自己的论文 现在居然来了兴趣 去 继续TREK了 + +*** + +## 总 + +- 思维的变化 是缓慢的 需要努力的 但绝对可能 给自己点耐心 多一点韧性和坚持 +- 空言不如实践 在实践中改变思维 成为行动派 + - 方法论只能作为参考 适合他的 不一定适合我 +- 界限从来就是自己给自己设定的 [Powers of Ten](https://www.youtube.com/watch?v=0fKBhvDjuy0) + - 宇宙可以那么大 + - 微观的可以那么小 却可如宇宙一样那么"大" + +看这个Ubuntu背景 不要给自己设定任何界限 +![Ubuntu 背景](http://dn-jeremiahzhang.qbox.me/universinubuntu.jpg) + +Thursday, 19. November 2015 02:55PM + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/4Learn/5wd4_upgrade.md b/4Learn/5wd4_upgrade.md new file mode 100644 index 000000000..e063cb027 --- /dev/null +++ b/4Learn/5wd4_upgrade.md @@ -0,0 +1,52 @@ +# Upgrade + +昨日大妈 提前了一日的公开课 近日失眠 恩 现在起来 回想昨天晚上的内容 反省 + +1 + +课程已经过半 在反思自己学到了什么 + +- 提问? + - 好像没有提过一次问题 + - 不过 在生活中 和朋友聊天 不自然的就会去使用 5W1H了 以前知道 但没有怎么去用 因为课程起初就提醒 到现在 慢慢改变了 +- 最小代价解决问题? + - 这个最小代价的最小边界在哪里? 因人而定 + - 如何 能以最小代价解决问题 + - 首先就是 源文档 源文档 源文档 + - 在自己完成任务 探索的过程中 发现属于自己的那个解决问题的 秘密武器 + - 恩 任何别人的方法论只能作为参考 只有在实践中 去挖掘 寻得自己的方法 +- 止损? + - 之前 没怎么深刻的关注 在广阔的data中 在解决问题的时候 得自己有一个止损 提醒 + - 准备尝试: + - 纸笔 将任务或目标 写下来(只在脑子里想是不可靠的) + - 设置 一个番茄(25mins) 的时间 来review 是否 在过去的时间中 偏离的既定目标or任务 + - 广度和深度 是在完成基本任务的基础上 恩 + +2 + +再回顾上周完成任务 效果不理想 做的过程头脑混乱 效率低下 为什么会这样 一个字 + +> ==急== + +看来我还是太注重结果 而忽视了 过程 这也是我学习 一直以来的陋病了 想着竞争 孰不知 这是一场 自己改变思维的旅程 生活 也是成长的过程 结果并没有过程那么重要 + +怎么办: + +- 要知道 每周的任务是相互独立 但是彼此也是有联系的 恩 本周的没有完成好 下周在完成任务的过程中 相当于 将最基础的部分 重新来一遍 所以 不要急 慢慢来 +- 重要的是 源文档 学会使用源文档 在完成任务的过程中 获得自己的秘密武器 才是关键 所以期间的过程 是比结果中重要的 思维改变 带来的果效 是受益无穷的 但也只有在做的过程中 带来思维的改变 空想 空言 没多大益处 +- 恩 还是耐心去读源文档吧 耐心去读 耐心 耐心 (不知从何时起 自己变得那么着急了) + +3 + +界限永远是自己设定 当你在空中飞翔 俯瞰大地 并没有界限 + +升级 +升级 +再升级 + +> 我欲载之空言 不如见之于行事之深切著明也 + +在编程中改变思维并且Update +每周的笔记和教程 正是这一改变的外在体现 + +Thursday, 19. November 2015 11:47AM \ No newline at end of file diff --git a/4Learn/6wd4_brokenthewall.md b/4Learn/6wd4_brokenthewall.md new file mode 100644 index 000000000..5cbfe2d2e --- /dev/null +++ b/4Learn/6wd4_brokenthewall.md @@ -0,0 +1,90 @@ +# 撞破思维中的那堵墙 + +- 能技 +- PDCA +- 牛人和挫人 + +## 能技-如何科学引发关注? + +大妈再一次强调这一能技 + +> 如何科学引发关注? + +首先得引发关注 重要的是得科学 嗯 +自学校受教育以来 已经养成了一种 让老师来灌输的思维 而不是主动去提问 引发关注的思维 心理似乎有一个壁垒在那里 自己不勇敢那把锤子去锤破 永远在那里 +其实最后发现 那个壁垒 可以用一根针就能戳破 那根针 就是 “厚着脸皮” 嘛 +恩 不过得科学地厚着脸皮 得真诚地引发关注 而不是自己都没经过认真尝试 就: 这个我不会 跪求 跪求 跪求 等等 + +比如: + +恩 我只在Github上只提过一个问题(虽然与实际python编程不相关) 真的是脸皮薄 不敢问 一问 咦多丢人那 哭 自个儿再 search 吧 +然而自己还是止损了 直接提问了 自己也算是真心求问 不过 提问还不真诚 居然自个儿没有先review一下 嗯 不过大妈的回答很大妈的 + +在未来课程 尤其在团队 或者在以后学习中 (学习成长 技能学习-编程等 遇到障碍) +我该如何改进: + +- 厚着脸皮 去除心障 +- 先自个儿用心的尝试 拿出自己的作品(不论是成功的还是失败) 展示出来 + - 比如笔记 + - 比如教程 +- 以上 + 一个用过力而真诚的心呐 + +## MVP-PDCA + +![PDCA cycle](http://openmindclub.qiniudn.com/res/tapes/omooc/omooc2py/151126_6wd4/2000px-PDCA_Cycle.svg.png) +![PDCA process](http://openmindclub.qiniudn.com/res/tapes/omooc/omooc2py/151126_6wd4/1280px-PDCA_Process.png) +![PDCA improvement](http://openmindclub.qiniudn.com/res/tapes/omooc/omooc2py/151126_6wd4/PDCA_EN.JPG) + +什么是PDCA: + +- Plan 拟定计划 +- Do 执行 +- Check 检查反馈 +- Act 根据反馈进行修正 + +这是一个螺旋式上升的过程 这不只是一个循环 嗯 + +在前5w 自己是没有按着这样的过程来得 虽然拟定计划 但总是在执行的时候 偏离计划 没有快速的 Check 和 Act 不符合 **最小原则**以及**最小代价解决问题** + +但从第六周慢下来之后 完成程序的过程中 意识需要PDCA来解决无论是一个任务或是项目的问题 + +因为第六周是极简日志交互微信公众号版本的开发 本地编辑好代码之后 每一次push到SAE上 挺麻烦的 每次都要输入邮箱和密码 虽然git设置了输入一次以后就无须输入 但这只局限在15分钟之内 得到Check或反馈所需要的时间是比较长的 +后来 看到 大妈的教程中 有本地测试 这下 直接可以直接在本次测试好 得到反馈时间比以前更快了 这样ACT会更快 节省开发时间 + +未来如何改进: + +- 如7w任务 7w任务也是需要多次调试的 再推送到Qpython + - [x] plan + - [x] do + - [] 找到那个最小的Check方法 大妈已经提供了 fabric 不能视而不见 + - 再 Act + +## 牛人和挫人 + +- 牛人 路径依赖 +- 挫人 习得性无助 + +以上 大妈 高桥流幻灯 大字 + +恩 + +牛人为什么牛 因为他有自己的秘密武器 依赖自己的路径 而这个路径是长年刻意练习获得的 + +挫人为什么挫 因为 自我实现的预言 自认为恩习得性无助 +其实 我之前写自己论文的过程中就出现了习得性无助的思想 脑子只想着困难 自己根本不去动手做 真的如那条狗一样 这种无益的思想自个儿喂的多了 就习得性无助了 + +如何打破 + +对我而言 其实还是心障 + +- 没有挫人 只有懒人 +- 只要去做 just do it just do it do it do ...... it +- 只要智力正常 坚持去做 一年不行 2年 3年 ... 年轻人 急什么是吧 + +在do 行的过程中 记住 + +> 嗜欲深者 天机浅 + +以上 1h 完成 + +Saturday, 28. November 2015 11:29AM diff --git a/4Learn/README.md b/4Learn/README.md new file mode 100644 index 000000000..1d7b7bbfc --- /dev/null +++ b/4Learn/README.md @@ -0,0 +1,21 @@ +# 人如何学习的 + +《构建之法》 中 作者提到人是如何学习的 + +- **知识体系** 自己构建 +- 人的**认知模型改变** 需要循序渐进 (更多需要去了解认知科学相关领域内容) +- **提问** 帮助构建 知识体系 +- **沉浸** 学习 全身心投入是学习的关键 要有一定工作量 + +Learn By Doing + +- **关注情景** 在各种情境中 + + 实践 + + 提问 + + 总结 + +---------- + +## ( ̄▽ ̄) ## + +10/9/2015 Build \ No newline at end of file diff --git a/5Review/0w.md b/5Review/0w.md new file mode 100644 index 000000000..88196a2c9 --- /dev/null +++ b/5Review/0w.md @@ -0,0 +1,37 @@ +# 0w + +来到 week 0 jump into python programming + +## 3点 + +> Python 是拿来用的 +初级知识 需要对高级知识深入了解才能真正深入了解 + +编程是在表述web的世界,python 作为一种编程语言,就是用来表述这个web世界的。python不用学,要去用,全身心**沉浸**。在用的过程中累积自己的经验,别人想夺也夺不走。这个学习过程是螺旋式上升的过程。 + +所以開智的编程课,是每周一个任务来让我去习得编程思维,虽然课程已经结束,已经形成一种用的思维,算是一种实用主义来习得编程思维。 + +> 以最小代价解决问题 + +大妈以自己的工程师经验告诉:不求完美解决问题,乃是及格或者是42%,并且是以最小代价的方式。 +在我的课程经历中,发现自己会下意识的去完美解决问题,这个开头就**忽视**和没有明白**以最小代价解决问题**啊! + +> 最大的错误: +1- 花了太多时间学习那些**不是特别需要**的东西上 +2- 没有**立即**开始写代码 + +web的世界中,别人有时候是为了炫耀,将一堆不是特别需要的资料放在那里,不要迷失在这些东西上。立即开始写代码! + +## 改进 + +1. 避免最大错误,**立即**开始写代码![ImgBookNote](https://github.com/JeremiahZhang/ImgBookNote) +2. 最小代价解决问题 + + 60% 不求完美先 + - 参考 aJie 童鞋的 [问题的问题](https://ajiea.gitbooks.io/miss-python-and-the-holy-geeks/content/2015-11-29.html) +3. 其他如**立即**开始写论文 + +Thursday, 21. January 2016 11:35AM + + + + diff --git a/5Review/10w.md b/5Review/10w.md new file mode 100644 index 000000000..97697f017 --- /dev/null +++ b/5Review/10w.md @@ -0,0 +1 @@ +# 10w diff --git a/5Review/1w.md b/5Review/1w.md new file mode 100644 index 000000000..3a8c5831d --- /dev/null +++ b/5Review/1w.md @@ -0,0 +1 @@ +# 1w diff --git a/5Review/2w.md b/5Review/2w.md new file mode 100644 index 000000000..a8720ee93 --- /dev/null +++ b/5Review/2w.md @@ -0,0 +1 @@ +# 2w diff --git a/5Review/3w.md b/5Review/3w.md new file mode 100644 index 000000000..fc3bc9ab2 --- /dev/null +++ b/5Review/3w.md @@ -0,0 +1 @@ +# 3w diff --git a/5Review/4w.md b/5Review/4w.md new file mode 100644 index 000000000..45bd6a0f4 --- /dev/null +++ b/5Review/4w.md @@ -0,0 +1 @@ +# 4w diff --git a/5Review/5w.md b/5Review/5w.md new file mode 100644 index 000000000..405c08bc4 --- /dev/null +++ b/5Review/5w.md @@ -0,0 +1 @@ +# 5w diff --git a/5Review/6w.md b/5Review/6w.md new file mode 100644 index 000000000..b439dcec0 --- /dev/null +++ b/5Review/6w.md @@ -0,0 +1 @@ +# 6w diff --git a/5Review/7w.md b/5Review/7w.md new file mode 100644 index 000000000..d401f1689 --- /dev/null +++ b/5Review/7w.md @@ -0,0 +1 @@ +# 7w diff --git a/5Review/8w.md b/5Review/8w.md new file mode 100644 index 000000000..749f09e39 --- /dev/null +++ b/5Review/8w.md @@ -0,0 +1 @@ +# 8w diff --git a/5Review/9w.md b/5Review/9w.md new file mode 100644 index 000000000..764a45e11 --- /dev/null +++ b/5Review/9w.md @@ -0,0 +1 @@ +# 9w diff --git a/5Review/README.md b/5Review/README.md new file mode 100644 index 000000000..73eb204a6 --- /dev/null +++ b/5Review/README.md @@ -0,0 +1,16 @@ +# Review + +## 缘起 + +開智PY2编程课早就已经结束,虽然自己最后的项目没有做好,但一路走过来,有自己的课程记录gitbook,还有大妈ZQ的语音记录,来重新review一下,记录在此。得逼这自己进步。 + +## 怎么做 + +- 阅读gitbook记录 +- 回听大妈语音记录 + +## 怎么输出 + +- 3点关键触动 +- 如何改进 具体的 +- 反馈(push 到 OMOOC 微信群) \ No newline at end of file diff --git a/README.md b/README.md index 7bf92a499..537fff037 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,43 @@ -# 开智学院 编程课程 Python 入门班 私人教程 -~ 提供给学员一个有序的开始,作为脚手架,作为起点 +为什么有这本教程 +======= +本书作为在开智学院编程课程学习文档书籍 记录自己的学习 经历 体验 -提供: +学习是个积累的过程 踏踏实实地走 路上难免跌倒 入坑 +记录自己的学习过程 是一个输出的过程 是巩固的过程 是自我反馈激励的过程 +所以有了这本书 持续成长 看见成长的自己 + +- 明确一点 应该是**教程** 能教会6个月前 那个不会的自己 `*`增补 + +## 作者 + +- [入学三问](https://github.com/JeremiahZhang/gopython/blob/master/PY-StarTrek-Prepare/2015-9-10-T2-%E5%85%A5%E5%AD%A6%E4%B8%89%E9%97%AE.md) +- 新浪微博:[@雷雨Jeremiah](http://weibo.com/1784386944/profile?topnav=1&wvr=6) +- Github:[@JeremiahZhang](https://github.com/JeremiahZhang) +- 个人博客:[Renew Mind Lab](http://jeremiahzhang.github.io/) + + +## 目标 +- 探索未知领域, Self Mining +- 习得编程思维 + - 像程序员或黑客一样解决问题 + +## ( ̄▽ ̄) +[如何成为一名黑客](http://translations.readthedocs.org/en/latest/hacker_howto.html#id3) +> 你必须建立对于自己学习能力的信念——就算你掌握的知识不足以解决当前的问题,如果你从问题的一小部分下手并从中学习,你将学到足够的知识用来解决下一部分——以此类推,直到整个问题都被你解决为止。 + +[The Glider: A Universal Hacker Emblem ](http://www.catb.org/~esr/hacker-emblem/) + +![Glider](http://www.catb.org/~esr/hacker-emblem/glider.png) + +---------- + +## Renew ## +Transfered From [Python Beginner Guide For Leiyu](https://jeremiahzhang.gitbooks.io/pybeginner/content/) + +10/20/2015 Transfered + +======= +My Awesome Book +======= -- 一个 gitbook 最小图书框架 -- 一个 配合课程每周开发任务的目录树 diff --git a/SUMMARY.md b/SUMMARY.md index 23cbaa094..149d3bb4b 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -1,9 +1,66 @@ # Summary -- [教学反转](0MOOC/README.md) -- [基础旋进](1sTry/README.md) -- [迭代作品](2nDev/README.md) -- [人生首秀](3rDemo/README.md) +- [启航准备](0MOOC/README.md) + - [系统设置](0MOOC/01_system.md) + - [Windows Setting](0MOOC/01_01_win7.md) + - [Transfer to Ubuntu](0MOOC/01_02_ubuntu.md) + - [開发配置](0MOOC/02_config.md) + - [Git](0MOOC/02_01_learngit.md) + - [Gitbook](0MOOC/02_02_gitbook.md) + - [双推](0MOOC/02_03_doublepush.md) + - [ST2与Py环境配置](0MOOC/02_04_SubPy.md) + - [Tmux Explore](0MOOC/02_05_tmux.md) + - [七牛云储](0MOOC/02_06_qiniu.md) + - [zsh](0MOOC/02_07_zsh.md) +- [航行日志](1sTry/README.md) + - [week_0](1sTry/week_0.md) + - [42行代码](1sTry/0wd0-task-42-code-fundamental-problem.md) + - [从源开始](1sTry/0wd1-reflection.md) + - [Sth New](1sTry/0wd2-New-things-to-learn.md) + - [苟日新](1sTry/0wd3-step-by-step-4_7-8.md) + - [week_1](1sTry/week_1.md) + - [Google Search](1sTry/1wd5_go_google.md) + - [week_4](1sTry/week_4.md) + - [Package Star](1sTry/packagestar.md) + - [Input and Output Star](1sTry/InOutputStar.md) +- [航行发现](2nDev/README.md) + - [StarTrek01 日记交互CLI](2nDev/week01_interact.md) + - [StarTrek02 自动登录网页-芝麻星](2nDev/week02_Gowechat.md) + - [StarTrek03 日志交互GUI](2nDev/week02_LogGUI.md) + - [StarTrek04 日志交互NET版](2nDev/week03_net_way.md) + - [StarTrek05 日志交互WEB版](2nDev/week04_web_way.md) + - [StarTrek06 日志交互公网版](2nDev/week05_task.md) + - [Trek06 开发笔记](2nDev/week05_paas.md) + - [Trek06 搭建自己设想应用](2nDev/week05_idea.md) + - [Trek06 本周感想](2nDev/week05_thought.md) + - [StarTrek07 日志交互微信公众号版](2nDev/week06_wechat.md) + - [Trek07 慢下来了](2nDev/week06_thought.md) + - [StarTrek08 日志交互移动版](2nDev/week07_qpy.md) +- [星际大会](3rDemo/README.md) +- [Renew Mind in Galaxy](4Learn/README.md) + - [Star 開智编程入门体验](4Learn/2015-9-26-HiddenMission.md) + - [Star Renewminding](4Learn/0wd4-OpenCourse-Note.md) + - [Star 警惕-笨功夫](4Learn/2wd0_stupidtime.md) + - [Star 破壁与坚持](4Learn/3wd3_monthreview.md) + - [Star 深呼吸-换换脑子](4Learn/4wd4_Rbrain.md) + - [Star 从行动开始](4Learn/4wd5_renewminding.md) + - [Star Upgrade](4Learn/5wd4_upgrade.md) + - [Mind Trek](4Learn/5wd4_mindtrek.md) + - [Star 去除心障](4Learn/6wd4_brokenthewall.md) +- [Review](5Review/README.md) + - [0w](5Review/0w.md) + - [1w](5Review/1w.md) + - [2w](5Review/2w.md) + - [3w](5Review/3w.md) + - [4w](5Review/4w.md) + - [5w](5Review/5w.md) + - [6w](5Review/6w.md) + - [7w](5Review/7w.md) + - [8w](5Review/8w.md) + - [9w](5Review/9w.md) + - [10w](5Review/10w.md) - [代码 (_src)](_src/README.md) - [素材 (draft)](draft/README.md) + [教程该怎么写](draft/how2tutorial.md) - [有关](ABOUT.md) + +**** \ No newline at end of file diff --git a/_image/01webhello.jpg b/_image/01webhello.jpg new file mode 100644 index 000000000..90eb4a826 Binary files /dev/null and b/_image/01webhello.jpg differ diff --git a/_image/02webnewitemjpg.jpg b/_image/02webnewitemjpg.jpg new file mode 100644 index 000000000..541dc5244 Binary files /dev/null and b/_image/02webnewitemjpg.jpg differ diff --git a/_image/03webnewitemback2.jpg b/_image/03webnewitemback2.jpg new file mode 100644 index 000000000..44d4f004b Binary files /dev/null and b/_image/03webnewitemback2.jpg differ diff --git a/_image/04webserverresult.jpg b/_image/04webserverresult.jpg new file mode 100644 index 000000000..add6d9c65 Binary files /dev/null and b/_image/04webserverresult.jpg differ diff --git a/_image/CLI01html.jpg b/_image/CLI01html.jpg new file mode 100644 index 000000000..3a84badaf Binary files /dev/null and b/_image/CLI01html.jpg differ diff --git a/_image/CLI02htmlcontent.jpg b/_image/CLI02htmlcontent.jpg new file mode 100644 index 000000000..ddd34c1d3 Binary files /dev/null and b/_image/CLI02htmlcontent.jpg differ diff --git a/_image/CLI03htmltextarea.jpg b/_image/CLI03htmltextarea.jpg new file mode 100644 index 000000000..ba5f27eaa Binary files /dev/null and b/_image/CLI03htmltextarea.jpg differ diff --git a/_image/CLI04htmlencode.jpg b/_image/CLI04htmlencode.jpg new file mode 100644 index 000000000..5bf942a53 Binary files /dev/null and b/_image/CLI04htmlencode.jpg differ diff --git a/_image/CLI05htmlprint.jpg b/_image/CLI05htmlprint.jpg new file mode 100644 index 000000000..59aba89d3 Binary files /dev/null and b/_image/CLI05htmlprint.jpg differ diff --git a/_image/CLI06sentdata.jpg b/_image/CLI06sentdata.jpg new file mode 100644 index 000000000..a1c71d8c6 Binary files /dev/null and b/_image/CLI06sentdata.jpg differ diff --git a/_image/CLI07result.jpg b/_image/CLI07result.jpg new file mode 100644 index 000000000..356c55cdb Binary files /dev/null and b/_image/CLI07result.jpg differ diff --git a/_image/Gui.JPG b/_image/Gui.JPG new file mode 100644 index 000000000..649a8080e Binary files /dev/null and b/_image/Gui.JPG differ diff --git a/_image/Tmux_001.jpg b/_image/Tmux_001.jpg new file mode 100644 index 000000000..7d6cb7734 Binary files /dev/null and b/_image/Tmux_001.jpg differ diff --git a/_image/Tmux_002.jpg b/_image/Tmux_002.jpg new file mode 100644 index 000000000..4fe12e68b Binary files /dev/null and b/_image/Tmux_002.jpg differ diff --git a/_image/Tmux_003.jpg b/_image/Tmux_003.jpg new file mode 100644 index 000000000..8386ba85e Binary files /dev/null and b/_image/Tmux_003.jpg differ diff --git a/_image/Tmux_004.jpg b/_image/Tmux_004.jpg new file mode 100644 index 000000000..d64dd1e74 Binary files /dev/null and b/_image/Tmux_004.jpg differ diff --git a/_image/gitbublogin.JPG b/_image/gitbublogin.JPG new file mode 100644 index 000000000..dca13b222 Binary files /dev/null and b/_image/gitbublogin.JPG differ diff --git a/_image/google.JPG b/_image/google.JPG new file mode 100644 index 000000000..af33cf291 Binary files /dev/null and b/_image/google.JPG differ diff --git a/_image/netbeta1.png b/_image/netbeta1.png new file mode 100644 index 000000000..7de2953f6 Binary files /dev/null and b/_image/netbeta1.png differ diff --git a/_image/try_pastlog.png b/_image/try_pastlog.png new file mode 100644 index 000000000..941e25add Binary files /dev/null and b/_image/try_pastlog.png differ diff --git a/_src/modelpy.py b/_src/modelpy.py new file mode 100644 index 000000000..b3446c927 --- /dev/null +++ b/_src/modelpy.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +#!/usr/bin/env python +# 头部声明 +'''文件说明 +作者信息 +版本自述 + ... +''' +# 全局引用 +import sys +# 全局变量 +PATH = "/path/2/work dir" +# 函式撰写区 +def foo(): + pass + return None + +# 自检区 +if __name__ == '__main__': + if 1 != len(sys.argv): #从0开始,本身是1个;3指的是2个参数 + print '''Usage: +$ python main..py + ''' + else: + foo() \ No newline at end of file diff --git a/_src/om2py0w/0wd0/Q.md b/_src/om2py0w/0wd0/Q.md new file mode 100644 index 000000000..04a6c71b0 --- /dev/null +++ b/_src/om2py0w/0wd0/Q.md @@ -0,0 +1,42 @@ +# Q # + +## 环境 ## + +- win7 +- ST2 + - sublimeREPL F5运行 + - fibo.fib2(100) 没有结果 +- python 2.7.8 + +### 分析 ### + +- 环境问题 ST2 与Python 用 shell 来解决一下 + +---------- + +## 教会我自己 那42行代码 ## + +![42 coder](http://wiki.woodpecker.org.cn/moin/ZqQuickIntoPy?action=AttachFile&do=get&target=coffeeghost-q-in-py.png) + +- mian() 主函数 + + print 使用 + + string + + string % (value, value) % string interpolation + + string + ... % (string concatenation) + + string repeation + + 函数调用 与 matlab 类似 a = f(10) and so on + + 内置函数使用 + + 计数 + + 从0开始 + + lists + + 循环 + + for 循环 + + 条件 + + `if :` + + `elif:` + + `else:` + + 布尔运算 ++ 调试主函数 + + + diff --git a/_src/om2py0w/0wd0/__init__.py b/_src/om2py0w/0wd0/__init__.py new file mode 100644 index 000000000..907aee669 --- /dev/null +++ b/_src/om2py0w/0wd0/__init__.py @@ -0,0 +1,2 @@ +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/_src/om2py0w/0wd0/coffeeghost-q-in-py.png b/_src/om2py0w/0wd0/coffeeghost-q-in-py.png new file mode 100644 index 000000000..e99d49020 Binary files /dev/null and b/_src/om2py0w/0wd0/coffeeghost-q-in-py.png differ diff --git a/_src/om2py0w/0wd0/main.py b/_src/om2py0w/0wd0/main.py new file mode 100644 index 000000000..332332ef6 --- /dev/null +++ b/_src/om2py0w/0wd0/main.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# 注释 +# 第一行注释为了可以中文输出 如 print just like this print down +# print "我爱python编程 我在学好编程的路上" +import os +# fibo.fib(100) # according to py 2.7.10 doc +# fibo.fib2(1000) # here is no answer in ST2 how to slove + +def main(): # 主程序 主“函数” 注意下一行的缩进 注意()中没有参数 + # refer to the name “main” is just a convention not a requirement + print 'Hello World!' # 这里是英文 就用 单 '' + + print "This is Alice's greeting." # 这里出现了 Alice's 以免混淆 就用 双 "" + print 'This Bob\'s greeting.' + # 这里bob's 在写的时候 加了\' 以区分 就可以用 单'' 可以与上一行对比 + + foo(5, 10) # 调用函数 foo + + print '=' * 10 # print 字符串 可以用 这样的形式 嗯 + print '0' * 30 + + print 'Current working directory is ' + os.getcwd() # os木块调用 注意 这里中间为 + + # 用 % 试过出错 --- % 不能将两个参数连起来 如下 + # TypeError: not all arguments converted during string formatting + counter = 0 + counter += 1 # 计数 + + food = ['apple', 'oranges', 'cats'] # list 字符串 + + for i in food: # for 循环 + print i # 看看 i 嗯 这里就是 和下面一样 + print 'I like to eat ' + i # + 与 % 区别 + + print 'Count to ten: ' + for i in range(10): # 爱从0开始 + print i + +def foo(param1, secondParam): + res = param1 + secondParam + + print '%s plus %s is equal to %s' % (param1, secondParam, res) # %s 代表string + + if res < 50: + print 'foo' + + elif (res >= 50) and ((param1 == 42) or (secondParam == 24)): # and or 布尔运算 + print 'bar' + + else: + print 'moo' # know + + return res # thsi is a one line comment. 不明白return 我去掉这行 也可以运行 + # The return statement returns with a value from a function. + # return without an expression argument returns None. + # Falling off the end of a function also returns None + '''A multi- +line string, but can also be a multi-line comment.''' # 多行注释 用'''comment''' + +if __name__ == '__main__': # 这个原来不知道是什么 将其注释掉 发现 + main() # 没有任何结果产生 说明 是调用主函数 main() + # 为什么会这样 本来就这样吧 + # 执行程序 cause a script to run main() + # 单独使用 无法使用 OK 就是放在脚本结尾的 + +# 我自己人工能得出结果 +# 编写注意 def if for 这种需要最后 : 不要遗漏 +# print 多种形式 尤其是 '%s plus %s is equal to %s' % (param1, secondParam, res) \ No newline at end of file diff --git a/_src/om2py0w/0wd0/mainresult.py b/_src/om2py0w/0wd0/mainresult.py new file mode 100644 index 000000000..4b2317dc8 --- /dev/null +++ b/_src/om2py0w/0wd0/mainresult.py @@ -0,0 +1,27 @@ +Hello World! +This is Alice's greeting. +This Bob's greeting. +5 plus 10 is equal to 15 +foo +========== +000000000000000000000000000000 +Current working directory is E:\BaiduYunPan\gitbook\pybeginner\_src\om2py0w\0wex0 +apple +I like to eat apple +oranges +I like to eat oranges +cats +I like to eat cats +Count to ten: +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 + +***Repl Closed*** diff --git a/_src/om2py0w/0wd0/print.py b/_src/om2py0w/0wd0/print.py new file mode 100644 index 000000000..7d537886f --- /dev/null +++ b/_src/om2py0w/0wd0/print.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +print "i like Python Star Trek" +print "我爱Python星际迷航之探索" +print "hello world" +print "中国" +# 这里是注释 +# debug的时候 请倒过来 检查 +# 倒着读 代码 避免大脑跟随每一段代码 \ No newline at end of file diff --git a/_src/om2py4w/4wex0/__init__.py b/_src/om2py0w/0wd1/__init__.py similarity index 100% rename from _src/om2py4w/4wex0/__init__.py rename to _src/om2py0w/0wd1/__init__.py diff --git a/_src/om2py0w/0wd1/imshow b/_src/om2py0w/0wd1/imshow new file mode 100644 index 000000000..7cc7b9788 --- /dev/null +++ b/_src/om2py0w/0wd1/imshow @@ -0,0 +1,3 @@ +from PIL import Image +im = Image.open('nar.png') +im.rotate(45).show() \ No newline at end of file diff --git a/_src/om2py0w/0wd1/main.py b/_src/om2py0w/0wd1/main.py new file mode 100644 index 000000000..c1e706279 --- /dev/null +++ b/_src/om2py0w/0wd1/main.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# using PIL https://pypi.python.org/pypi/Pillow/2.2.1 +# fork this https://github.com/JiYouMCC/python-show-me-the-code/blob/e0c7c1c37ccba38671078e0b0ff6238992a11499/0000/0000.py +from PIL import Image +from PIL import ImageFont +from PIL import ImageDraw + +def write_number(image_file_name_in_current_dir, number = 1): + img = Image.open(image_file_name_in_current_dir) # open image + font_size = img.size[0] if img.size[0] < img.size[1] else img.size[1] # img.size(width, height) + font_size = font_size / 4 # 字体 数字 尺寸 取小 + number_txt = str(number) + ' ' if number < 100 else '99+' # 数字文本 + + font = ImageFont.truetype("arial.ttf", size = font_size) # creat a font object + if font.getsize(number_txt)[0] > img.size[0] or font.getsize(number_txt)[1] > img.size[1]: + return img # font.getsize(text) -> (width, height) 字比图片大了 不行 + position = img.size[0] - font.getsize(number_txt)[0] # 字体位置 + ImageDraw.Draw(img).text((position, 0), number_txt, (255, 0, 0), font) + # draw.text(position, string, options) + return img + +# need an image 'nar.png' +write_number('nar.png').save('nar_result.png') # 调用 并 保存 与 当前文件夹 +# if number > 100, shows '99+' +write_number('nar.png', 100).save('nar_result100.png') \ No newline at end of file diff --git a/_src/om2py0w/0wd1/nar.png b/_src/om2py0w/0wd1/nar.png new file mode 100644 index 000000000..ba0f59e15 Binary files /dev/null and b/_src/om2py0w/0wd1/nar.png differ diff --git a/_src/om2py0w/0wd1/nar_result.png b/_src/om2py0w/0wd1/nar_result.png new file mode 100644 index 000000000..672db2dcb Binary files /dev/null and b/_src/om2py0w/0wd1/nar_result.png differ diff --git a/_src/om2py0w/0wd1/nar_result100.png b/_src/om2py0w/0wd1/nar_result100.png new file mode 100644 index 000000000..c7b837ed9 Binary files /dev/null and b/_src/om2py0w/0wd1/nar_result100.png differ diff --git a/_src/om2py0w/0wd1/result.png b/_src/om2py0w/0wd1/result.png new file mode 100644 index 000000000..672db2dcb Binary files /dev/null and b/_src/om2py0w/0wd1/result.png differ diff --git a/_src/om2py0w/0wd1/result100.png b/_src/om2py0w/0wd1/result100.png new file mode 100644 index 000000000..c7b837ed9 Binary files /dev/null and b/_src/om2py0w/0wd1/result100.png differ diff --git a/_src/om2py5w/5wex0/__init__.py b/_src/om2py0w/0wd2/__init__.py similarity index 100% rename from _src/om2py5w/5wex0/__init__.py rename to _src/om2py0w/0wd2/__init__.py diff --git a/_src/om2py0w/0wd2/ask_ok_test.py b/_src/om2py0w/0wd2/ask_ok_test.py new file mode 100644 index 000000000..c5123c274 --- /dev/null +++ b/_src/om2py0w/0wd2/ask_ok_test.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +def ask_ok(prompt, retries = 4, complaint = 'yes or no, please!'): + while True: + ok = raw_input(prompt) + if ok in ('y', 'ye','yes'): # in + return True # return + if ok in ('n', 'no', 'nop', 'nope'): + break + retries = retries - 1 + if retries < 0: + raise IOError('Refuseink user') + print complaint + +# ask_ok('你真的想退出么?', 2000) + +def ask_over(prompt): + while True: + over = raw_input(prompt) + print over + if over in ('yes', 'ye', 'y'): + break + elif over in ('n', 'no', 'nop', 'nope'): + + diff --git a/_src/om2py4w/4wex0/main.py b/_src/om2py0w/0wd2/main.py similarity index 100% rename from _src/om2py4w/4wex0/main.py rename to _src/om2py0w/0wd2/main.py diff --git a/_src/om2py0w/0wd2/tutor00num.py b/_src/om2py0w/0wd2/tutor00num.py new file mode 100644 index 000000000..9741aa09b --- /dev/null +++ b/_src/om2py0w/0wd2/tutor00num.py @@ -0,0 +1,27 @@ +# -*- coding:utf-8 -*- +print 17 / 3 # int / int -> int +print 17 / 3.0 # int / float -> float +print 17 // 3.000 # 舍弃小数部分 +print 17 % 3 # 整除 +print 5*3 + 2 # mess + +print '*' * 20 +print 'powers of numbers' +print 5 ** 2 +print 2 ** 7 + +print '*' * 20 +print 'assign a value to a variable' +width = 20 +height = 5 * 9 +print width * height + +print '*' * 20 +tax = 12.5 / 100 +print tax +price = 100.50 +print price +print price * tax + +price += price +print price \ No newline at end of file diff --git a/_src/om2py0w/0wd2/tutor01string.py b/_src/om2py0w/0wd2/tutor01string.py new file mode 100644 index 000000000..8966edbd3 --- /dev/null +++ b/_src/om2py0w/0wd2/tutor01string.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +the_world_is_flat = 1 +if the_world_is_flat : + print 'Be careful not to fall off!' + +# this is the 1st comment +spam = 1 # and this is the 2nd comment + # ... and now a 3rd! +text = "# this is not a comment because it's inside quotes." +text_diff = '# this is not a comment because it\'s inside quotes. Ple notice the diff' +print spam +print text +print text_diff + +print '*' * 20 +print 'i need to defined a variable before assign a value' + +print '*' * 20 +print 'c:\user\name' # note this +print r'c:\user\name' # notice the r before + +print '*' * 20 +print """"\ +Usage: thingy [options] # muti-line to comment like this + -h Display this usage message + -H hostname Hostname to connect to +""" + +print '*' * 20 +print 'Jesus' + 'love' * 3 + 'you' + 'forever' +print 'py' 'thon' + +print '*' * 20 +print 'concatenate variables and a literal use +' +prefix = 'py' +print prefix + 'thon' + +print '*' * 20 +word = 'python' +print word +print "1st letter of python: " + word[-6] +print 'the last letter of python: ' + word[-1] +print '1st letter of python: ' + word[0] +print 'the last letter of python: ' + word[5] +print "'1st letter of python: + word[6] is wrong because out of range" +print "please notice the above!" + +print '*' * 20 +print word[0:2] +print "here 2 index is exclued" +print word[2:5] +print 'same as above' + +print '*' * 20 +print word[:2] +print "same as above. but notice the below" +print word[4:] +print "here 4 is include!" + +print '*' * 20 +print "word[42] is out of range but this one below is useful" +print word[2:42] +print word[42:] + +print '*' * 20 +print "python string cannot changed like this" +print "word[0] = J but like this below" +print 'J' + word[1:] # this is just concatenate +s = 'ilovejesus' +print len(s) +ss = 'i love jesus ' +print len(ss) \ No newline at end of file diff --git a/_src/om2py0w/0wd2/tutor02unicodestring.py b/_src/om2py0w/0wd2/tutor02unicodestring.py new file mode 100644 index 000000000..61e84bc3a --- /dev/null +++ b/_src/om2py0w/0wd2/tutor02unicodestring.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +print u'hello world!' +print u'hello\u0020world !' +print u'adc' +print str(u'adc') +# encode +# unicode \ No newline at end of file diff --git a/_src/om2py0w/0wd2/tutor03Lists.py b/_src/om2py0w/0wd2/tutor03Lists.py new file mode 100644 index 000000000..2b7a774ee --- /dev/null +++ b/_src/om2py0w/0wd2/tutor03Lists.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +squares = [1, 4, 9, 16, 25] +print squares +# tri = [1 8 27] so this is invalid syntax 语法错了 就像英文语法错误一样 he girl +print squares[0] +print squares[-1] + +print "*" * 20 +print squares[-3:] + +print '-' * 20 +print squares[:] + +print '>o<'*10 +squares = squares + [36, 49, 64, 81, 100] +print squares + +print "it...then... 执行意图 + >= 3倍" +cubes = [1, 8, 27, 65, 125] # did you fine the wrong num? +print cubes +print 'change 65 -> 64' +cubes[3] = 4 ** 3 +print cubes +print 'o-o' * 10 +print 'append means what like this' +cubes.append(216) +print cubes +cubes.append(6 ** 3) # append() items at the end of the list +print cubes +cubes.append(7 **3) +print cubes +print '()' * 10 +print 'if python then coding' +letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g'] +print letters +letters[2:5] = ['C', 'D', 'E'] +print letters +letters[2:5] = [] +print letters +letters[:] = [] +print letters +letters =['a', 'b', 'c', 'd'] +print len(letters) +inner_letter_list = ['a', 'b', 'c'] +inner_num_list = ['1', '2', '3'] +mess_letter_num_list_lists = [inner_letter_list, inner_num_list] +print mess_letter_num_list_lists +print mess_letter_num_list_lists[0] +print mess_letter_num_list_lists[0][1] +print '模仿 实践 创意' +print "have fun in python star trek" \ No newline at end of file diff --git a/_src/om2py0w/0wd2/tutor03ListsResult.py b/_src/om2py0w/0wd2/tutor03ListsResult.py new file mode 100644 index 000000000..cd3e7f63e --- /dev/null +++ b/_src/om2py0w/0wd2/tutor03ListsResult.py @@ -0,0 +1,32 @@ +[1, 4, 9, 16, 25] +1 +25 +******************** +[9, 16, 25] +-------------------- +[1, 4, 9, 16, 25] +>o<>o<>o<>o<>o<>o<>o<>o<>o<>o< +[1, 4, 9, 16, 25, 36, 49, 64, 81, 100] +it...then... 执行意图 + >= 3倍 +[1, 8, 27, 65, 125] +change 65 -> 64 +[1, 8, 27, 64, 125] +o-oo-oo-oo-oo-oo-oo-oo-oo-oo-o +append means what like this +[1, 8, 27, 64, 125, 216] +[1, 8, 27, 64, 125, 216, 216] +[1, 8, 27, 64, 125, 216, 216, 343] +()()()()()()()()()() +if python then coding +['a', 'b', 'c', 'd', 'e', 'f', 'g'] +['a', 'b', 'C', 'D', 'E', 'f', 'g'] +['a', 'b', 'f', 'g'] +[] +4 +[['a', 'b', 'c'], ['1', '2', '3']] +['a', 'b', 'c'] +b +模仿 实践 创意 +have fun in python star trek + +***Repl Closed*** diff --git a/_src/om2py0w/0wd2/tutor04-1if.py b/_src/om2py0w/0wd2/tutor04-1if.py new file mode 100644 index 000000000..e4adf3bf0 --- /dev/null +++ b/_src/om2py0w/0wd2/tutor04-1if.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +x = int(raw_input('please enter an interger: ')) +if x < 0: + x = 0 + print "负数则变为0" +elif x == 0: + print "零" +elif x == 1: + print 'single' +else: + print 'more' + +print 'I love python! ' +print '-' * 20 +words = ['cat', 'window', 'defenestrate'] +for w in words: + print w, len(w) +print '-' * 20 +print 'for statement' +for w in words[:]: + if len(w) > 6: + words.insert(0,w) # 将字长大于6的放入words list中的第一个 + elif len(w) < 6: + words.insert(4,w) +print words +print 'the length of the words list now is %s' % len(words) # len()是num 所以 不能用 + 而是 % 来 +print 'the length of the words list now is', len(words) # same as above diff --git a/_src/om2py0w/0wd2/tutor04-3range.py b/_src/om2py0w/0wd2/tutor04-3range.py new file mode 100644 index 000000000..9cd37ad99 --- /dev/null +++ b/_src/om2py0w/0wd2/tutor04-3range.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +x = range(10) +print x +y = range(5, 10) +print 'please notice the range,', y +z = range(1, 10, 3) +print z +a = range(-10, -100, -30) +print a + +aa = ['Mary', 'had', 'a', 'litter', 'lamb'] +for i in range(len(aa)): + print i, aa[i] +print '# 使用 enumerate 枚举 same as above let see' +for i, v in enumerate(['Mary', 'had', 'a', 'litter', 'lamb']): + print i, v +print 'Lets see another thats fun' +for i, v in enumerate(['I', 'LOVE', 'PYTHON']): + print i, v \ No newline at end of file diff --git a/_src/om2py0w/0wd2/tutor04-4break.py b/_src/om2py0w/0wd2/tutor04-4break.py new file mode 100644 index 000000000..c332a4b60 --- /dev/null +++ b/_src/om2py0w/0wd2/tutor04-4break.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +print 2 % 2 +for n in range(2, 13): + for x in range(2, n): + if n % x == 0: + print n, 'equals', x, '*', n/x + break # break 的是 最近的for statement 此处是 for x... OR while loop而不是 if + # 要不 12 = 2*6 还有 12 = 3 * 4 or 12=4*3 + # so break() breaks out of the smallest enclosing for or while LOOP + else: # here else: clause belongs to the for loop not if + # 这是突破我以前认识的 不同之处 + # 没有找到因子 + # for loop else clause + print n, 'is a prime number 素数' +for num in range(2,10): + if num % 2 == 0: + print '找到一个偶数', num + # continue # 继续的下一次 for loop + break # 若用break 则for loop 结束了 + print '找到一个数', num + pass # pass statement doing noting 可以用来comment 就像我现在做的一样 + +while True: + pass # busy wait for keyboard interrupt (ctrl+c) +class MyEmptyClass: + pass # this is commonly used to creating minimal classes + # 什么是最小 class 再学习class 的时候用 关注 +def initlog(*args): + pass # remember to implement this! 我不知道这个用法是什么的? diff --git a/_src/om2py0w/0wd2/tutor04-6def.py b/_src/om2py0w/0wd2/tutor04-6def.py new file mode 100644 index 000000000..13ac47df5 --- /dev/null +++ b/_src/om2py0w/0wd2/tutor04-6def.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +def fib(n): + a, b = 0, 1 + while a < n: + print a, # , 是为了一行输出 + a, b = b, a+b + pass +print fib(2000) +f = fib +print f(100) + +def fib2(n): # 输出list 形式结果 斐波那契数列 + result = [] + a, b = 0, 1 + while a < n: + result.append(a) + a, b = b, a+b + return result +f100 = fib2(100) +print f100 \ No newline at end of file diff --git a/_src/om2py0w/0wd2/tutor04-7-1-default-argument-values.py b/_src/om2py0w/0wd2/tutor04-7-1-default-argument-values.py new file mode 100644 index 000000000..874ca44ba --- /dev/null +++ b/_src/om2py0w/0wd2/tutor04-7-1-default-argument-values.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +print 'argument values' +def ask_ok(prompt, retries=4, complaint='yes or no, please!'): + while True: + ok = raw_input(prompt) + if ok in ('y', 'ye','yes'): # in + return True # return + if ok in ('n', 'no', 'nop', 'nope'): + return False + retries = retries - 1 + if retries < 0: + raise IOError('Refuseink user') + print complaint +ask_ok('你真的想退出么?', 2000) + +print "-o-"*10 +"""The defult valeus are evaluated at the point of function definition in +the defining scope, so that the codes below will print 5""" +i = 5 +def f(arg = i): + print arg + pass +i = 6 +print f() +"""调用的时候 输出还是5 默认是5 第一次i = 5 +调用的时候没有输入参数 所以默认还是5""" +print f(10) +print f() +print 'Important Warning: the default value is evaluated only one' +'''以上 此时 默认还是先前的 i=5 +arg =i ''' + +print "-o-"*10 +print "请注意 这里 mutable object such as a list,dictionary, or instances of most classes" +def f(a, L=[]): + L.append(a) + return L +print f(1) +print f(2) +print f(3) + +print "---"*10 +print "then look at this below can you understand what does it do?" +def f(a, L=None): + if L is None: + L = [] + L.append(a) + return L + # when i change return L to single return + # I understand what does it mean when return +"""return without an expression argument returns +None.""" +print f(1) +print f(2) +print f(3) \ No newline at end of file diff --git a/_src/om2py0w/0wd2/tutor04-7-2-keywords-argument-values.py b/_src/om2py0w/0wd2/tutor04-7-2-keywords-argument-values.py new file mode 100644 index 000000000..0d4212b78 --- /dev/null +++ b/_src/om2py0w/0wd2/tutor04-7-2-keywords-argument-values.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +def parrot(voltage, state = 'a stiff', action = 'voom', type = 'Norwegian Blue'): + print "-- This parrot wouldnot", action, + print "if you put", voltage, "volts through it." + print "-- Lovely plumage, the", type + print "-- It's", state, "!" +parrot(1000) # 根据位置 确定参数 +print "+"*20 +parrot(voltage=1000) # 根据 1 个 keyword arg +print "-"*20 +parrot(voltage=1000000, action='V00000000M') # 根据2个keyword arg +print "+"*20 +parrot(action='V00000000M', voltage=1000000) # 根据2个keyword arg 顺序可以改变 +print "-"*20 +parrot('a million', 'bereft of life', 'jump') # 3个位置 arg +print '+'*20 +parrot('a thousand', state = 'pushing up the daisies') # 1个位置arg 1个keyword arg +print '+'*20 +# parrot() +# error parrot(voltage=5.0, 'dead') +parrot(voltage=5, state = 'dead') +print '+'*20 +# error parrot(110, voltage=220) + +def cheeseshop(kind, *arguments, **keywords): + print '-- do you have any', kind, "?" + print "-- I am sorry, we're all out of", kind + for arg in arguments: + print arg + print '-' * 40 + print keywords + keys = sorted(keywords.keys()) # 将keywords中的字母按首字母排序的 a b c d等顺序 + for kw in keys: + print kw, ":", keywords[kw] +cheeseshop("limburger", + "it's very runny, sir.", "It's really very, Very runny, sir.", + shopkeeper = 'Michael Palin', client = 'John Cleese', sketch = 'cheese shop sketch') + +# 现在我已经理解了 这里的coding \ No newline at end of file diff --git a/_src/om2py0w/0wd2/tutor04Fob.py b/_src/om2py0w/0wd2/tutor04Fob.py new file mode 100644 index 000000000..711381f09 --- /dev/null +++ b/_src/om2py0w/0wd2/tutor04Fob.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# 下面是斐波那契数列 +# 一个数前两个数的和 开始的两个数为 1, 1 +a, b = 0, 1 +while b < 10: + print b + a, b = b, a + b + pass + +i = 256*256 +print 'the vaules of i is', i +print 'the values of i is\n', i \ No newline at end of file diff --git a/_src/om2py0w/0wd3/tutor-4-7-2-dictionary.py b/_src/om2py0w/0wd3/tutor-4-7-2-dictionary.py new file mode 100644 index 000000000..c449e5b92 --- /dev/null +++ b/_src/om2py0w/0wd3/tutor-4-7-2-dictionary.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +a = dict(one=1, two=2, three=3) # values containing Numeric types +f = dict(one=1, three=3, two=2) +b = {'one': 1, 'two': 2, 'three': 3} # +c = dict(zip(['one', 'two', 'three'], [1, 2, 3])) +d = dict([('two', 2), ('one', 1), ('three', 3)]) # lists +e = dict({'three': 3, 'one': 1, 'two': 2}) # dictionarys +g = dict({1: 'one', 2: 'two', 3: 'three'}) + +# position change but the same +# so the keyword is important in dic +if a == b == c == d == e == f: + print 'true' +else: + print 'false' + +if f == g: + print 'true' +else: + print '0' + +print len(a) +print len(d) + +# 什么是dict 中的key key到底是什么? +# d[key] +print d['one'] +print """then i know key in dict d is 'one', 'two', 'three', +but one, two, three is not the key """ +print "*" * 20 +print a['one'] +print "a[one] is not ok, cause NameERROR" +a['one'] += 1 +print a['one'] +print "*" * 20 +class Counter(dict): + def __missing__(self, key): + return 0 +ccc = Counter() +print ccc['red'] +ccc['red'] += 1 +print ccc['red'] + + +print '*' * 20 +for kw in a: + print kw + +print '*' * 20 +keys = sorted(a) # 这里的sort 是按照key的首字母 +for kw2 in keys: + print kw2 + +print 'x' * 20 +dishes = {'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500} +keys = dishes.viewkeys() # 可以从字面来理解 viewkeys +values = dishes.viewvalues() # viewvalues() +n = 0 +for val in values: + n += val +print(n) +print n + +print '*' * 20 +# keys and valus are iterated over in the same order +print list(keys) +print list(values) + +print '*' * 20 +# view objects are dynamic and reflect dict changes +del dishes['eggs'] +del dishes['sausage'] +print list(keys) # the values and keys are del at the same time +print list(values) # 从这里可以看出 动态的变化 values 是跟着变化的 \ No newline at end of file diff --git a/_src/om2py0w/0wd3/tutor-4-7-4-arbitrary.py b/_src/om2py0w/0wd3/tutor-4-7-4-arbitrary.py new file mode 100644 index 000000000..7337924e0 --- /dev/null +++ b/_src/om2py0w/0wd3/tutor-4-7-4-arbitrary.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +def write_multiple_item(file, separator, *args): # arbitrary argument Lists + file.write(separator.join(args)) +print '*' * 20 +print range(3,6) +print "this below is a list do you know?" +args = [3, 6] +print args +print range(*args) +print "can you see the meaning of this code above? yes" + +print '*' * 20 +def parrot(voltage, state='a stiff', action='voom'): + print "this parrot would not", action, + print 'if you put', voltage, 'volts through it.', + print 'E\'s', state, '!' +d = {"voltage": "FOUR million", "state": "bleedin' demised", "action": "V00M"} +parrot(d) +parrot(**d) + +print """ ** operator can deliver keyword +arguments in dictionaries""" + +print '*' * 20 +print "lets know the lambda expressions" +def make_incrementor(n): + return lambda x: x + n +f = make_incrementor(42) +print f(0) +print f(1) +print f(42) + +def make_sum(n): + return lambda a, b: a + b + n +ff = make_sum(42) +print ff(0, 0) +print ff(1, 1) + +print '*' * 20 +pairs = [(2, 'two'), (1, 'one'), (3, 'three'), (4, 'four')] +print pairs +pairs.sort(key=lambda pair: pair[0]) # I think pair[0] mean the 1st value in lists +print pairs # 此处就是按 list中的 ()中 第一位 value 排序 按数字 +pairs.sort(key=lambda pair: pair[1]) # 此处 ()中 字母排序 abcde的顺序 +print pairs +print "print pair[1] display error: name 'pair' is not defined" \ No newline at end of file diff --git a/_src/om2py0w/0wd3/tutor-4-7-6-Doc-Strings.py b/_src/om2py0w/0wd3/tutor-4-7-6-Doc-Strings.py new file mode 100644 index 000000000..e7bcebccc --- /dev/null +++ b/_src/om2py0w/0wd3/tutor-4-7-6-Doc-Strings.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +def my_function(): + """ Do nothing, but document it. + + No, really, it doesnot do anything. + """ + pass +print my_function.__doc__ + +print "*" * 20 +print """文本说明 + + 1-简洁明了 第一行是summary + 2-多行显示 中间空行 + 3-第三行开始正文 + +end +""" +if a in b : + pass \ No newline at end of file diff --git a/_src/om2py0w/0wd6/search_taobao.py b/_src/om2py0w/0wd6/search_taobao.py new file mode 100644 index 000000000..d4cffb9b7 --- /dev/null +++ b/_src/om2py0w/0wd6/search_taobao.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# ref to https://wp-lai.gitbooks.io/learn-python/content/0MOOC/taobao.html +# 代码所在文件夹 进行命令行输入 python search_taobao.py keyword-1 keyword-2 ... +# win 可在 shell中使用 +import sys +import webbrowser + +keywords = sys.argv[1:] # argument from your keyword-1 and keywords-2 etc + +url = "https://s.taobao.com/search?q=" # search url from taobao +for i in keywords: + url += i + "+" + print url +url = url[:-1] # remove the last "+" +webbrowser.open(url) +print "mission completed" \ No newline at end of file diff --git a/_src/om2py0w/0wd6/search_taobao_error.py b/_src/om2py0w/0wd6/search_taobao_error.py new file mode 100644 index 000000000..297965409 --- /dev/null +++ b/_src/om2py0w/0wd6/search_taobao_error.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# 在win shell 中 无法打开 +import os +import sys + +keywords = sys.argv[1:] + +url = "https://s.taobao.com/search?q=" +for i in keywords: + url += i + "+" +url = url[:-1] +os.system.open(url) \ No newline at end of file diff --git a/_src/om2py0w/0wd6/test_learn.py b/_src/om2py0w/0wd6/test_learn.py new file mode 100644 index 000000000..5d0989495 --- /dev/null +++ b/_src/om2py0w/0wd6/test_learn.py @@ -0,0 +1,2 @@ +ani = "cat" +print ani[:0] \ No newline at end of file diff --git a/_src/om2py0w/0wex0/__init__.py b/_src/om2py0w/0wex0/__init__.py index e69de29bb..907aee669 100644 --- a/_src/om2py0w/0wex0/__init__.py +++ b/_src/om2py0w/0wex0/__init__.py @@ -0,0 +1,2 @@ +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/_src/om2py5w/5wex0/main.py b/_src/om2py0w/0wex0/aa.txt similarity index 100% rename from _src/om2py5w/5wex0/main.py rename to _src/om2py0w/0wex0/aa.txt diff --git a/_src/om2py0w/0wex0/conti_test.py b/_src/om2py0w/0wex0/conti_test.py new file mode 100644 index 000000000..cf48cf63c --- /dev/null +++ b/_src/om2py0w/0wex0/conti_test.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +import sys +from time import localtime, strftime + +reload(sys) # 必须 reload +sys.setdefaultencoding('utf-8') + +script_statement = """ # 脚本说明: + + 1.这是每日日志书写脚本 + 2.请按照提示进行 日志书写 + + 祝您书写愉快! + """ + +print(script_statement.encode(sys.stdout.encoding)) + +diary_name = raw_input("请输入日志名: ".encode(sys.stdout.encoding)) + '.txt' + +yes_list = ['yes', 'yep', 'ye', 'y', 'YES','YEP', 'YE', 'Y'] +no_list = ['no', 'n', 'NO', 'N'] + +def ask_date(prompt): + ok = raw_input(prompt) + if ok in yes_list: + ur_date = strftime("%Y-%m-%d %H:%M:%S", localtime()) # 这里可以调用模块 time 参考 + if ok in no_list: + ur_date = "\n" + return ur_date + +writer = open(diary_name, "w") # if the textfile exist it'll be erased + +done = False +textInput = "" + +while (done == False): + nextInput = raw_input("Please input ur words: ") + "\n" + if nextInput == "end" + "\n": # must add "\n" + inputDate = ask_date("Do you want add date, yes or no?") + writer.write('\n' + inputDate) + break + else: + textInput += nextInput + print nextInput + writer.write(nextInput) # write into textfile.txt and start a new line + +writer.close() + +print ("Here is ur diary: " + textInput) \ No newline at end of file diff --git a/_src/om2py0w/0wex0/diary_beta_2.py b/_src/om2py0w/0wex0/diary_beta_2.py new file mode 100644 index 000000000..f17e960bd --- /dev/null +++ b/_src/om2py0w/0wex0/diary_beta_2.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +import sys, os, glob +from time import localtime, strftime + +reload(sys) # 必须 reload +sys.setdefaultencoding('utf-8') + +def main(): + keywords = sys.argv[1:] # 调用外部数据 + for words in keywords: + print words, + print "\n" + + read_diary() + + print welcome_statement.__doc__ + + write_diary() + +def welcome_statement(): + """ # Dear, welcome abord in Diary Ship + + 1. Please input your words as following prompt + 2. Wanna Leave? Please input the word "end" alone + 3. Thanks, Hope you have fun in writing. > < + + Let's Start. GO + """ + +def read_diary(): + current_dir = os.getcwd() # 打印之前日志 + os.chdir(current_dir) + + for file in glob.glob("*.txt"): + print(file) + file_content = open(file, "r") + print file_content.read() + "\n" + +def write_diary(): + done = False + textInput = "" + + diary_name = raw_input("请输入日志名: ".encode(sys.stdout.encoding)) + ".txt" + diary_writer = open(diary_name, "w") + + while (done==False): + nextInput = raw_input("Please input your diary words: ") + if nextInput == "end": + inputDate = ask_date("Wanna add diary date time? yes or no! ") + diary_writer.write("\n" + inputDate) + break + else: + textInput += nextInput + "\n" + diary_writer.write(nextInput + "\n") + + diary_writer.close() + print ("Here is your " + inputDate +" diary: " + textInput) + +def ask_date(prompt): # 是否添加时间 + yes_list = ['yes', 'yep', 'ye', 'y', 'YES','YEP', 'YE', 'Y'] + no_list = ['no', 'n', 'NO', 'N'] + + ok = raw_input(prompt) + if ok in yes_list: + your_datetime = strftime("%Y-%m-%d %H:%M:%S", localtime()) + if ok in no_list: + your_datetime = "\n" + + return your_datetime + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/_src/om2py0w/0wex0/diary_final_test.txt b/_src/om2py0w/0wex0/diary_final_test.txt new file mode 100644 index 000000000..3dac56fe8 --- /dev/null +++ b/_src/om2py0w/0wex0/diary_final_test.txt @@ -0,0 +1,3 @@ +һռǽ + +2015-10-20 11:57:15 \ No newline at end of file diff --git a/_src/om2py0w/0wex0/diary_name.txt b/_src/om2py0w/0wex0/diary_name.txt new file mode 100644 index 000000000..58a4522a4 --- /dev/null +++ b/_src/om2py0w/0wex0/diary_name.txt @@ -0,0 +1,3 @@ + beta2.0 + +2015-10-20 16:11:18 \ No newline at end of file diff --git a/_src/om2py0w/0wex0/find_and_print_files_test.py b/_src/om2py0w/0wex0/find_and_print_files_test.py new file mode 100644 index 000000000..de30d2dce --- /dev/null +++ b/_src/om2py0w/0wex0/find_and_print_files_test.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +import sys, os, glob +# print sys.getdefaultencoding() +reload(sys) # 必须 reload +sys.setdefaultencoding('utf-8') + +current_dir = os.getcwd() +print type(current_dir) +os.chdir(current_dir) + +for file in glob.glob("*.txt"): + print(file) + file_content = open(file, "r") + print file_content.read() \ No newline at end of file diff --git a/_src/om2py0w/0wex0/input_test.py b/_src/om2py0w/0wex0/input_test.py new file mode 100644 index 000000000..a5acf65d0 --- /dev/null +++ b/_src/om2py0w/0wex0/input_test.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +s = raw_input("--> ") +print (s, type(s)) + + +name = input("what's your name? Please include your name into quotes: ") +print ("nice to meet you " + name + "!") + + +age = raw_input("ur age?") +print ("so you are already " + str(age) + " years old, " + name + "!") + +ur_diary = raw_input("Plase input your diary: ") + +print type(ur_diary) +print ("here is your diary:" + ur_diary) \ No newline at end of file diff --git a/_src/om2py0w/0wex0/invoking_test.py b/_src/om2py0w/0wex0/invoking_test.py new file mode 100644 index 000000000..97e540a77 --- /dev/null +++ b/_src/om2py0w/0wex0/invoking_test.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +import sys +reload(sys) # 必须 reload +sys.setdefaultencoding('utf-8') +print ('我爱python'.encode(sys.stdout.encoding)) +keywords = sys.argv[:] +for words in keywords: + print words, \ No newline at end of file diff --git a/_src/om2py0w/0wex0/main.py b/_src/om2py0w/0wex0/main.py index e69de29bb..c9ef496d1 100644 --- a/_src/om2py0w/0wex0/main.py +++ b/_src/om2py0w/0wex0/main.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +import sys, os, glob +from time import localtime, strftime + +reload(sys) # 必须 reload +sys.setdefaultencoding('utf-8') + +yes_list = ['yes', 'yep', 'ye', 'y', 'YES','YEP', 'YE', 'Y'] +no_list = ['no', 'n', 'NO', 'N'] + +def main(): + keywords = sys.argv[1:] # 调用外部数据 + for words in keywords: + print words, + print "\n" + + read_diary() + + script_statement = """ # 脚本说明: + + 1.这是每日日志书写脚本 + 2.首先打印的是脚本所在文件夹所有日志(txt格式)名称及内容 + 3.请按照提示进行 日志书写 + 4.想要结束日志写作?最后一行请单独输入:end + + 祝您书写愉快! + """ + + print(script_statement.encode(sys.stdout.encoding) + "\n") + + write_diary() + +def read_diary(): + current_dir = os.getcwd() # 打印之前日志 + os.chdir(current_dir) + + for file in glob.glob("*.txt"): + print(file) + file_content = open(file, "r") + print file_content.read() + "\n" + +def write_diary(): + done = False + textInput = "" + + diary_name = raw_input("请输入日志名: ".encode(sys.stdout.encoding)) + ".txt" + diary_writer = open(diary_name, "w") + + while (done==False): + nextInput = raw_input("Please input your diary words: ") + if nextInput == "end": + inputDate = ask_date("Wanna add diary date time? yes or no! ") + diary_writer.write("\n" + inputDate) + break + else: + textInput += nextInput + "\n" + diary_writer.write(nextInput + "\n") + + diary_writer.close() + print ("Here is your " + inputDate +" diary: " + textInput) + +def ask_date(prompt): # 是否添加时间 + ok = raw_input(prompt) + if ok in yes_list: + your_datetime = strftime("%Y-%m-%d %H:%M:%S", localtime()) + if ok in no_list: + your_datetime = "\n" + + return your_datetime + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/_src/om2py0w/0wex0/main_test.py b/_src/om2py0w/0wex0/main_test.py new file mode 100644 index 000000000..09a4e93e8 --- /dev/null +++ b/_src/om2py0w/0wex0/main_test.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +import sys, os, glob +# print sys.getdefaultencoding() +reload(sys) # 必须 reload +sys.setdefaultencoding('utf-8') + +# ===========调用脚本 外部数据 中英文 +keywords = sys.argv[1:] # The list of command line arguments passed to a Python script +# 命令行中 除脚本名 以外的所有参数都保存在 keywords 中 + +print keywords +print type(keywords) # 查看keyword 它是list 输入英文 print 英文 + +for words in keywords: + # words = unicode(words, "ascii") + # print words + # print type(words) + print words, + +current_dir = os.getcwd() +print type(current_dir) +os.chdir(current_dir) + +for file in glob.glob("*.txt"): + print(file) + file_content = open(file, "r") + print file_content.read() + +print """Congratulations: You have invoked the script main.py + +-- I love coding in Python! +-- Do you love it? + +Have fun in Python Star Trek!""" # + +# name = raw_input("请输入日志名: ".encode(sys.stdin.encoding)) + '.txt' # succeed +name = raw_input("请输入日志名: ".encode(sys.stdout.encoding)) + '.txt' # succeed +# name = raw_input("请输入日志名: ".decode('utf-8')) + ".txt" +# name = raw_input("Glad to write today's diary" +# "\n Please give the name first:") + '.txt' +# 以上 提示中文 是乱码 pshell 会出现乱码 cat 我的日志.txt +# 以上 这输入中文名 也是乱码 +# 待解决 第二日 reboot之后 就自然解决了 amazing +# win 的 reboot 真是强 + +yes_list = ['yes', 'yep', 'ye', 'y', 'YES','YEP', 'YE', 'Y'] +no_list = ['no', 'n', 'NO', 'N'] + +def ask_date(prompt, retries=3, friendly_warn='Please input yes or no'): + ok = raw_input(prompt) + if ok in yes_list: + ur_date = raw_input("Please add date: ") # 这里可以调用模块 time + if ok in no_list: + ur_date = "\n" + + retries += -1 + if retries < 0: + raise IOError('Refuselink user') + print friendly_warn + + return ur_date + +writer = open(name, "w") # if the textfile exist it'll be erased + +done = False +textInput = "" + +while (done == False): + nextInput = raw_input("Please input ur words: ") + if nextInput == "end": + inputDate = ask_date("Do you want add date, yes or no?") + writer.write('\n' + inputDate) + break + else: + textInput += nextInput + print nextInput + writer.write(nextInput + "\n") # write into textfile.txt and start a new line + +writer.close() + +print ("Here is ur diary: " + textInput) + +# ==================== 调用脚本 及其关键字 + + +# 交互 +# ========== 调用 python脚本 +# ========== 中文输入 +# 在 pshell cmd line 中输入中文 如 我爱python +# 问题: 中文 我爱 这部分 输出的是 一种编码形式 解决之 +# 分析: 传递参数(中文字符)后 再次输出 可能没有解码还原为中文字符 +# 方案: google search 原来 在最初运用了 utf-8 编码 输出的时候 没有 unicoding +# keywords = unicode(keywords, "utf-8") +# 但是不能解码list error need string or buffer +# utf 解码错误 +# words = words.defcode("ascii").encode("utf-8") +# 在没有修改默认编码是 以上设置 也是错的 +# 参考 http://www.kryptosx.info/archives/391.html +# ===========持续交互 +# while +# =========== 输出文件 +# - 换行 +# - 日期 +# - 中文编码 +# + 中文编码问题 +# + name = raw_input("请输入日志名: ") + '.txt' 在pshell中 乱码 +# + 分析 +# UnicodeDecodeError: 'utf8' codec can't decode byte 0xb2 in position 0: invalid start byte +# 本身 sys.setdefaultencoding('utf-8') 是utf-8 编码的 在pshell中 raw_input("请输入日志名:") 中 +# 双引号中的 请输入日志名 出现乱码 璇疯緭鍏ユ棩蹇楀悕 +# =========== 回读文本 +# - 自动将过往日志打印? +# http://stackoverflow.com/questions/3964681/find-all-files-in-directory-with-extension-txt-with-python +# + 如何找到日志文件 +# + 如何打开 +# + 如何读取 +# + 如何输出日志 +# + 中文OK么 \ No newline at end of file diff --git a/_src/om2py0w/0wex0/open_read_test.py b/_src/om2py0w/0wex0/open_read_test.py new file mode 100644 index 000000000..4643e4d10 --- /dev/null +++ b/_src/om2py0w/0wex0/open_read_test.py @@ -0,0 +1,10 @@ +import os, glob + +current_dir = os.getcwd() +print type(current_dir) +os.chdir(current_dir) + +for file in glob.glob("*.txt"): + print(file) + file_content = open(file, "r") + print file_content.read() \ No newline at end of file diff --git a/_src/om2py0w/0wex0/test ask date.txt b/_src/om2py0w/0wex0/test ask date.txt new file mode 100644 index 000000000..ee2aabdd3 --- /dev/null +++ b/_src/om2py0w/0wex0/test ask date.txt @@ -0,0 +1 @@ +ֻ diff --git a/_src/om2py0w/0wex0/test.txt b/_src/om2py0w/0wex0/test.txt new file mode 100644 index 000000000..76305d6db --- /dev/null +++ b/_src/om2py0w/0wex0/test.txt @@ -0,0 +1,5 @@ +timeʹò +ϴʹõʱʱ +βʹñʱ + +2015-10-20 10:16:27 \ No newline at end of file diff --git a/_src/om2py0w/0wex0/test_str.txt b/_src/om2py0w/0wex0/test_str.txt new file mode 100644 index 000000000..dd7ca6b91 --- /dev/null +++ b/_src/om2py0w/0wex0/test_str.txt @@ -0,0 +1,3 @@ +this is file to test string and find "\n" + +2015-10-20 11:29:11 \ No newline at end of file diff --git a/_src/om2py0w/0wex1/.twill-history b/_src/om2py0w/0wex1/.twill-history new file mode 100644 index 000000000..a4d171f89 --- /dev/null +++ b/_src/om2py0w/0wex1/.twill-history @@ -0,0 +1,5 @@ +? +go http://www.slashdot.org/ +show +showforms +showhistory diff --git a/_src/om2py0w/0wex1/main.py b/_src/om2py0w/0wex1/main.py index e69de29bb..ee52f55cd 100644 --- a/_src/om2py0w/0wex1/main.py +++ b/_src/om2py0w/0wex1/main.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from twill.commands import * + +from twill.commands import * +go("http://www.python.org/") +showforms() \ No newline at end of file diff --git a/_src/om2py0w/0wex1/twill_browser.py b/_src/om2py0w/0wex1/twill_browser.py new file mode 100644 index 000000000..0953a1975 --- /dev/null +++ b/_src/om2py0w/0wex1/twill_browser.py @@ -0,0 +1,7 @@ +from twill import get_browser +b = get_browser() + +b.go("http://www.python.org/") +b.showforms() + +# To talk to the Web browser directly, call the get_browser function: \ No newline at end of file diff --git a/_src/om2py0w/0wex1/twill_test.py b/_src/om2py0w/0wex1/twill_test.py new file mode 100644 index 000000000..3fc97aa8d --- /dev/null +++ b/_src/om2py0w/0wex1/twill_test.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# import all of the commands in commands.py and use them directly from Python +from twill.commands import * + +from twill.commands import * +go("http://www.python.org/") +showforms() \ No newline at end of file diff --git a/_src/om2py0w/README.md b/_src/om2py0w/README.md index 1511d4a76..167686605 100644 --- a/_src/om2py0w/README.md +++ b/_src/om2py0w/README.md @@ -1,7 +1,27 @@ # OMOOC.py 周任务代码试作 -## 0w +- 系统 win7 +- 命令行使用Powershell工具 +- python version 2.7.10 + +## 0w 日记交互系统 - py 调用 + - 拷贝 or clone 代码 [main.py](https://github.com/JeremiahZhang/pybeginner/blob/master/_src/om2py0w/0wex0/main.py) 到您的文件目录中 比如 `usr/om2py0w/0wex1/` + - win 7 系统 Powershell 打开上面的文件目录 使用 `cd` + - 执行 `python mian().py 我爱 python` + + pshell 第一行就打印 外部数据(关键字) `我爱 python` - CLI: + 交互 + + 在上一步调用脚本 打印外部数据后 会显示脚本说明 请按着脚本说明进行 + + 按住说明进行 日志写作(文件自动保存为txt格式) 每写完一行 回车进行下一行书写 + + 如何结束? 单行输入 `end` + + 会询问是否添加日期时间,请输入以下list中的任何一个英文字符或字符串 比如 `yes` 或者 `no` + + 代表添加日期时间的string list: `yes_list = ['yes', 'yep', 'ye', 'y', 'YES','YEP', 'YE', 'Y']` + + 代表不添加日期时间string list: `no_list = ['no', 'n', 'NO', 'N']` + + 最后会打印你刚刚写作的所有内容(不包括时间) + + shell中打开文件 `cat diary_name.txt` 会打印日志内容 + + 演示 + ![演示](https://raw.githubusercontent.com/JeremiahZhang/pybeginner/master/_image/05_readme_pre.JPG) +- [更新](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py0w/0wex0/diary_beta_2.py) +- [开发日志](https://jeremiahzhang.gitbooks.io/omooc2py/content/2nDev/week01_interact.html) diff --git a/_src/om2py6w/6wex2/__init__.py b/_src/om2py1w/1wd3/__init__.py similarity index 100% rename from _src/om2py6w/6wex2/__init__.py rename to _src/om2py1w/1wd3/__init__.py diff --git a/_src/om2py1w/1wd3/list_compre.py b/_src/om2py1w/1wd3/list_compre.py new file mode 100644 index 000000000..c004adc48 --- /dev/null +++ b/_src/om2py1w/1wd3/list_compre.py @@ -0,0 +1,87 @@ +# -8- coding: utf-8 -*- +from math import pi +print '*'*30 +print "List comprehensions" +squares = [] +for x in range(10): + squares.append(x**2) + +print squares + +squares = [x**2 for x in range(10)] +print squares + +squares = map(lambda x: x**2, range(10)) + +a = [(x, y) for x in [1, 2, 3] for y in [3, 1, 4] if x != y] +print a + +combs = [] +for x in [1, 2, 3]: + for y in [3, 1, 4]: + if x != y: + combs.append((x, y)) + + +print combs + +print "如果有嵌套的话 list 注意()使用" + +vec = [-4, -2, 0, 2, 4] +a = [x*2 for x in vec] +print a +b = [x for x in vec if x >= 0] +print b +c = [abs(x) for x in vec] +print c + +freshfruit = [' banana', ' loganberry', ' passion fruit ' ] +d = [weapon.strip() for weapon in freshfruit] +print d + +print """string.strip(s[, characters]) + + 1- Return a copy of the string with leading and trailing characters removed. + 2- If chars is omitted or None, whitespace characters are removed. + 3- If given and not None, chars must be a string; + the characters in the string will be stripped from the both ends of the string this method is called on. + + """ +e = [(x, x**2) for x in range(6)] +# e = [ x, x**2 for x in range(6)] is error +print e + +vec = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] +f = [num for elem in vec for num in elem] # 两层嵌套 list in list 相当于matrix +print f + +string_pi = [str(round(pi, i)) for i in range(1, 6)] +print string_pi + +matrix = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], +] + +sth = [[row[i] for row in matrix] for i in range(4)] # 从 i 循环先开始 +# row[i] in matrix +print sth + +equal_sth = [] +for i in range(4): + equal_sth.append([row[i] for row in matrix]) # 注意这里面的嵌套 + +print equal_sth + +equal_sth_2 = [] +for i in range(4): + equal_row = [] + for row in matrix: + equal_row.append(row[i]) + equal_sth_2.append(equal_row) + +print equal_sth_2 + +print zip(matrix) #zip 替代了上面的所有 +print zip(*matrix) \ No newline at end of file diff --git a/_src/om2py1w/1wd3/list_test.py b/_src/om2py1w/1wd3/list_test.py new file mode 100644 index 000000000..f66bd5d09 --- /dev/null +++ b/_src/om2py1w/1wd3/list_test.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +from collections import deque + +a = [1, 2 ,3 , 4] +x = [-1, 0] +a.append(x) # x is list so list x inside list a +print a +print a.index(x) +print a.pop() # removes and returns the last item in list a +print """ list.pop([i]) can returns the item + the square bracket denote the parameter + is opitional +""" + +print a +a.remove(4) +print a +print a.count(2) + +b = [-1, 0, 3, -3] +print "b:" + str(b) +b.sort() # sorted item +print "b.sort(): " + str(b) +b.reverse() # reverse +print "b.reverse(): " + str(b) +print "List objects and the method" +print """> method insert remove sort modify the list + and have no return value printed-- + they return the default None. + + list.insert + list.remove + list.sort + list.append + + + funny list! + cool ha! + """ + +print "-"*30 +print "## Using List as Stacks 堆客栈" +print "three peeple have lived in stack 客栈" +stack = [3, 4, 5] +print stack.append(6) +print "入客栈 订房 先订先住" +print "6 is the first " + str(stack.append(6)) +print "7 is the second" + str(stack.append(7)) +print stack + +print "出客栈 退房了 后订 先退" +print stack.pop() +print stack.pop() +print stack + +print "*"*30 +print "using lists as queues" +queue = deque(["Eric", "John", "Michael"]) +queue.append("Teery") +queue.append("Graham") +print queue.popleft() +print queue +print queue.popleft() +print queue + +queue = deque(['Eric', 'John', 'Michael']) +queue.append('Teery') +print queue +print "Dont care the "" or the '' in the list" + +print '*'*30 +print "Functional Programming Tools" + +def f(x): return x%3==0 or x%5==0 +def g(x): + if x%3==0 or x%5==0: + return g + +print filter(f, range(2, 25)) +print filter(g, range(2, 25)) + +print type(range(2,5)) + +print '*'*30 +print "map(function, sequence)" +def cube(x): return x*x*x + +a = map(cube, range(1, 11)) +print a + +print '*'*30 +print "reduce(function, sequence)" + +def add(x,y): return x+y + +b = reduce(add, range(1, 11)) +print b + +print '*'*30 +print "a third argument can be passed to indicate the starting value" + +def sum(seq): + def add(x,y): return x+y + return reduce(add, seq, 1) + +c = sum(range(1, 11)) +print c +d = sum([]) +print d \ No newline at end of file diff --git a/_src/om2py1w/1wd3/main.py b/_src/om2py1w/1wd3/main.py new file mode 100644 index 000000000..d5889a0f2 --- /dev/null +++ b/_src/om2py1w/1wd3/main.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +a = [1, 2 ,3 , 4] +x = [-1, 0] +a.append(x) # x is list so list x inside list a +print a +print a.index(x) +print a.pop() # removes and returns the last item in list a +print """ list.pop([i]) can returns the item + the square bracket denote the parameter + is opitional +""" + +print a +a.remove(4) +print a +print a.count(2) + +b = [-1, 0, 3, -3] +print "b:" + str(b) +b.sort() # sorted item +print "b.sort(): " + str(b) +b.reverse() # reverse +print "b.reverse(): " + str(b) \ No newline at end of file diff --git a/_src/om2py1w/1wd3_list.md b/_src/om2py1w/1wd3_list.md new file mode 100644 index 000000000..3df7121ff --- /dev/null +++ b/_src/om2py1w/1wd3_list.md @@ -0,0 +1,23 @@ +# Date Structures List + +- list method +- 用于 + - 进出`客栈` + - `排队` 结合 `collenctions`模块的`deque` + - `函数工具` `filter(function, seque)`, `map(function, seque)`, `reduce(function, seque)` +- 更多理解 + - 直接用list 简化 一个循环赋值 + - `squares = [x**2 for x in range(10)]` + - 嵌套 注意主次 加() + - 矩阵表示 提取 + - zip() +- PS `lambda` expression 相当于 一个函数 + +> the expression lambda arguments: expression yields a function object. + + lambda_expr ::= "lambda" [parameter_list]: expression + + def name(arguments): + return expression + +[代码1wd3](https://github.com/JeremiahZhang/OMOOC2py/tree/master/_src/om2py1w/1wd3) \ No newline at end of file diff --git a/_src/om2py1w/1wd4/dict_test.py b/_src/om2py1w/1wd4/dict_test.py new file mode 100644 index 000000000..c14fe1566 --- /dev/null +++ b/_src/om2py1w/1wd4/dict_test.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# dicts are index by keys, +# are not like sequence indexed by numbers +# what +# - keys in dict are immutable +# - 字符和数字都可使是keys +# - 包括字符or数字ortuple的tuple也可使是keys +# - 但是若tuple包含mutable object 则不可以用作 keys +# - 所以 list 是不能用作 key的 +# - 配对的 男:女 +# - 如果后面添加keys 存在了 则会覆盖掉(遗忘以前的) +# .... +# keys() +# sorted() 排序 + +tel = {'jack':137, "sape":9801} +tel['guido'] = 9366 +print tel +print tel["jack"] + +del tel['sape'] +print tel +print tel.keys() + +dict([('jeremy', 13798019366), ('jesus', 5201314)]) +print dict +print {x:x**2 for x in (2, 4, 6)} +dict(jeremy=13798019366, jesus=5201314) \ No newline at end of file diff --git a/_src/om2py1w/1wd4/loop_test.py b/_src/om2py1w/1wd4/loop_test.py new file mode 100644 index 000000000..0166781ae --- /dev/null +++ b/_src/om2py1w/1wd4/loop_test.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# 枚举 enumerate() +import math +for i, v in enumerate(['tic', 'tac', 'toe']): + print i, v + +questions = ['name', 'quest', 'fav color'] +answers = ['jesus', 'the holy father', 'red'] +for q, a in zip(questions, answers): + print 'what is your {0}? Its {1}.'.format(q, a) # 配对输出 nice + +for i in reversed(xrange(1,10,2)): + print i + +basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana'] +for f in sorted(set(basket)): + print f + +bible = {'lord': 'jesus', 'saint': 'paul'} +for b, v in bible.iteritems(): + print b, v + +raw_data =[56.2, float('NaN'), 51.7, 55.3, 52.5, float('NaN'), 47.8] +filtered_data = [] +for value in raw_data: + if not math.isnan(value): + filtered_data.append(value) + +print filtered_data + +# in # not in check out the values +# is # is not check out the object +# compares + +print (1, 2, 3) < (1, 2, 4) +print [1, 2, 3] < [1, 2, 4] +print "ABC" < "C" < "Pascal" < 'python' +print (1, 2, 3, 4) < (1, 2, 4) +print (1, 2, ('aa', 'ab')) < (1, 2, ("abc", 'a')) \ No newline at end of file diff --git a/_src/om2py1w/1wd4/sets_test.py b/_src/om2py1w/1wd4/sets_test.py new file mode 100644 index 000000000..a30907234 --- /dev/null +++ b/_src/om2py1w/1wd4/sets_test.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +import math +# sets have no duplicate elements, USE in +# - membership testing +# - eliminating duplicate entries. +# set() +# set(list) + +basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana'] +fruit = set(basket) # no duplicate +print fruit +print 'orange' in fruit +print 'berry' in fruit + +apple = set('appleappleappleapple') +bear = set('bearbearbear') +print apple +print bear + +print apple - bear + +print apple|bear +print apple&bear +print apple^bear + +a = {x for x in 'apple' if x not in "bear"} +print a \ No newline at end of file diff --git a/_src/om2py1w/1wd4/tuple_test.py b/_src/om2py1w/1wd4/tuple_test.py new file mode 100644 index 000000000..833ae507d --- /dev/null +++ b/_src/om2py1w/1wd4/tuple_test.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +a = [-1, 1, 66.25, 333, 333, 1234.5] +print a.count(333) + +del a[0] +print a + +a.pop() +print a + +del a[:1] +print a +del a + +t = 12345, 54321, 'hello!' +print t + +u = t, (1, 2, 3, 4, 5) +print u + +a = [t, (1, 2, 3, 4, 5)] +print a # tuples in list + +# tuples are immutables + +s = [1, 2, 3] # but lists are mutable like this +s[0] = 4 +print s + +# but tuples can contain mutable object +list1 = [1, 2, 3] +list2 = [3, 2, 1] + +v = (list1, list2) +print v + +empty = () +print len(empty) + +singleton = "hello", # tuple +print len(singleton) +print singleton + +singleton = "hello" # string +print len(singleton) +print singleton + +cool = "i", "love", "python!" +print cool +x = "i" +y = "love" +z = "python!" +x, y, z = cool +print cool # cool is the same as above \ No newline at end of file diff --git a/_src/om2py1w/1wd4_del_tuple_set_dict_loops.md b/_src/om2py1w/1wd4_del_tuple_set_dict_loops.md new file mode 100644 index 000000000..538c7dce1 --- /dev/null +++ b/_src/om2py1w/1wd4_del_tuple_set_dict_loops.md @@ -0,0 +1,11 @@ +# Data Stracture # + +Python tutorial 5.2-5.8 + +- del statement +- tuples and sequences +- sets +- dictionaries +- Looping Tech +- Conditions +- Comparing Sequences and Other Types \ No newline at end of file diff --git a/_src/om2py1w/1wd5autologin/ex01.py b/_src/om2py1w/1wd5autologin/ex01.py new file mode 100644 index 000000000..05ed146f3 --- /dev/null +++ b/_src/om2py1w/1wd5autologin/ex01.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 _*- +from selenium import webdriver + + +print """ # function: + + 1- open a new Firefox browser + 2- load the webpage at the given URL + + HAVE FUN + +""" + +browser = webdriver.Firefox() +browser.get("http://seleniumhq.org/") \ No newline at end of file diff --git a/_src/om2py1w/1wd5autologin/go_facebook.py b/_src/om2py1w/1wd5autologin/go_facebook.py new file mode 100644 index 000000000..1a66dbb8a --- /dev/null +++ b/_src/om2py1w/1wd5autologin/go_facebook.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from selenium import webdriver +from selenium.webdriver.support import ui +from selenium.webdriver.common.keys import Keys + +def page_is_loaded(driver): + return driver.find_element_by_tag_name("body") != None + +driver = webdriver.Firefox() +driver.get("https://facebook.com/") + +wait = ui.WebDriverWait(driver, 10) +wait.until(page_is_loaded) + +email_field = driver.find_element_by_id("email") +email_field.send_keys("user_email@email.com") # user_email + +password_field = driver.find_element_by_id("pass") +password_field.send_keys("your_password") # your password +password_field.send_keys(Keys.RETURN) + +print """Mission Completed! + + Find your new things in Facebook! + + See you later! + " \ No newline at end of file diff --git a/_src/om2py1w/1wd5autologin/go_google.py b/_src/om2py1w/1wd5autologin/go_google.py new file mode 100644 index 000000000..83ce716a2 --- /dev/null +++ b/_src/om2py1w/1wd5autologin/go_google.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +import sys +from selenium import webdriver +from selenium.webdriver.common.keys import Keys + +reload(sys) # must reload +sys.setdefaultencoding('utf-8') # default encoding + + +print """ Explain: + ========== + + * open a new Firefox browser + * load the google homepage + * search for "your_keywords" + + ========== +""" +your_keywords = sys.argv[1:] + +browser = webdriver.Firefox() + +browser.get("http://www.google.com") + +search_elem = browser.find_element_by_name('q') + +for keywords in your_keywords: + search_elem.send_keys(keywords + " ") + +search_elem.send_keys(Keys.RETURN) + +print "Mission Completed!" \ No newline at end of file diff --git a/_src/om2py1w/1wd5autologin/go_openmind.py b/_src/om2py1w/1wd5autologin/go_openmind.py new file mode 100644 index 000000000..18d09c7e9 --- /dev/null +++ b/_src/om2py1w/1wd5autologin/go_openmind.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 _*- +from selenium import webdriver +from selenium.webdriver.support import ui +from selenium.webdriver.common.keys import Keys + +def page_is_loaded(driver): + return driver.find_element_by_tag_name("body") != None + +driver = webdriver.Firefox() +driver.get('http://www.iomooc.com/') + +wait = ui.WebDriverWait(driver, 10) +wait.until(page_is_loaded) + +email_field = driver.find_element_by_id("email") +email_field.send_keys("youremail@email.com") # user_email + +password_field = driver.find_element_by_id("password") +password_field.send_keys("yourpassword") # your password + +login_field = driver.find_element_by_id("login") +login_field.send_keys(Keys.RETURN) + +print "Mission Completed! Go Openmind and then Renew Mind!--->" \ No newline at end of file diff --git a/_src/om2py1w/1wd5autologin/go_wechat.py b/_src/om2py1w/1wd5autologin/go_wechat.py new file mode 100644 index 000000000..034458f61 --- /dev/null +++ b/_src/om2py1w/1wd5autologin/go_wechat.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from selenium import webdriver +from selenium.webdriver.support import ui +from selenium.webdriver.common.keys import Keys + +def page_is_loaded(driver): + return driver.find_element_by_tag_name("body") != None + +driver = webdriver.Firefox() +driver.get('https://mp.weixin.qq.com/') + +wait = ui.WebDriverWait(driver, 10) +wait.until(page_is_loaded) + +wechat_account = driver.find_element_by_id("account") +wechat_account.send_keys("user@email.com") # input your email address or your wechat account + +your_password = driver.find_element_by_id("pwd") +your_password.send_keys("balabalabala") # your password instead of balabalabala +your_password.send_keys(Keys.RETURN) + +print "Mission Completed! Write your new article.-->" \ No newline at end of file diff --git a/_src/om2py1w/1wd5autologin/iomooc.py b/_src/om2py1w/1wd5autologin/iomooc.py new file mode 100644 index 000000000..d851bedde --- /dev/null +++ b/_src/om2py1w/1wd5autologin/iomooc.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 _*- +from selenium import webdriver +from selenium.webdriver.support import ui +from selenium.webdriver.common.keys import Keys + +def page_is_loaded(driver): + return driver.find_element_by_tag_name("body") != None + +driver = webdriver.Firefox() +driver.get('http://www.iomooc.com/') + +wait = ui.WebDriverWait(driver, 10) +wait.until(page_is_loaded) + +github_login_button = driver.find_element_by_link_text("GitHub") +github_login_button.send_keys(Keys.RETURN) + +# in github sign in +email_field = driver.find_element_by_id("login_field") +email_field.send_keys("balabala@email.com") # user_email + +password_field = driver.find_element_by_id("password") +password_field.send_keys("balabala") # your password +password_field.send_keys(Keys.RETURN) + +print "Mission Completed! Go Openmind and then Renew Mind!--->" \ No newline at end of file diff --git a/_src/om2py1w/1wd5autologin/python_org_search.py b/_src/om2py1w/1wd5autologin/python_org_search.py new file mode 100644 index 000000000..e0af67a7f --- /dev/null +++ b/_src/om2py1w/1wd5autologin/python_org_search.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +from selenium import webdriver +from selenium.webdriver.common.keys import Keys + +driver = webdriver.Firefox() +driver.get("http://www.python.org") +assert "Python" in driver.title + +elem = driver.find_element_by_name("q") +elem.send_keys("pycon") +elem.send_keys(Keys.RETURN) +assert "No results found." not in driver.page_source +driver.close() \ No newline at end of file diff --git a/_src/om2py1w/1wd5autologin/test.py b/_src/om2py1w/1wd5autologin/test.py new file mode 100644 index 000000000..fd8d45535 --- /dev/null +++ b/_src/om2py1w/1wd5autologin/test.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +import sys +reload(sys) # 必须 reload +sys.setdefaultencoding('utf-8') # 默认编码 + +keywords = sys.argv[1:] +print type(keywords) # list + +print (sys.argv[0]) # str + +your_keywords = sys.argv[1:] + +for keywords in your_keywords: + print type(keywords) + print keywords \ No newline at end of file diff --git a/_src/om2py2w/2wex0/Tk_event.py b/_src/om2py2w/2wex0/Tk_event.py new file mode 100644 index 000000000..d6bacaf7f --- /dev/null +++ b/_src/om2py2w/2wex0/Tk_event.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from Tkinter import * + +root = Tk() + +def callback(event): + print "clicked at", event.x, event.y + +frame = Frame(root, width=100, height=100) +frame.bind("", callback) +frame.pack() + +root.mainloop() + +# mouse click \ No newline at end of file diff --git a/_src/om2py2w/2wex0/Tk_event_bind_keyboard.py b/_src/om2py2w/2wex0/Tk_event_bind_keyboard.py new file mode 100644 index 000000000..c5fdb0e6d --- /dev/null +++ b/_src/om2py2w/2wex0/Tk_event_bind_keyboard.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from Tkinter import * + +root = Tk() + +def key(event): + print "pressed", repr(event.char) + +def callback(event): + frame.focus_set() + print "clicked at", event.x, event.y + +frame = Frame(root, width=100, height=100) +frame.bind("", key) +frame.bind("", callback) +frame.pack() + +root.mainloop() \ No newline at end of file diff --git a/_src/om2py2w/2wex0/Tk_test.py b/_src/om2py2w/2wex0/Tk_test.py new file mode 100644 index 000000000..ec4d03bb9 --- /dev/null +++ b/_src/om2py2w/2wex0/Tk_test.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from Tkinter import * + +class Application(Frame): + def say_hi(self): + print "hi there, everyone!" + pass + + def createWidgets(self): + self.QUIT = Button(self) + self.QUIT["text"] = "QUIT" + self.QUIT["fg"] = "red" + self.QUIT["command"] = self.quit + + self.QUIT.pack({"side": "left"}) + + self.hi_there = Button(self) + self.hi_there["text"] = "hello", + self.hi_there["command"] = self.say_hi + + self.hi_there.pack({"side": "left"}) + + def __init__(self, master=None): + Frame.__init__(self, master) + self.pack() + self.createWidgets() + +root = Tk() +app = Application(master=root) +app.mainloop() +root.destroy() diff --git a/_src/om2py2w/2wex0/Tk_test_value.py b/_src/om2py2w/2wex0/Tk_test_value.py new file mode 100644 index 000000000..cc5e77567 --- /dev/null +++ b/_src/om2py2w/2wex0/Tk_test_value.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +from Tkinter import * + +def print_contents(self, event): + print "hi. contents of entry is now ---->", \ + self.contents.get() + +class App(Frame): + def __init__(self, master=None): + Frame.__init__(self, master) + self.pack() + + self.entrythingy = Entry() + self.entrythingy.pack() + # app variable + self.contents = StringVar() + # set is to some value + self.contents.set("this is a variable") + # tell the entry widget to watch this var + self.entrythingy["textvariable"] = self.contents + # add here we get a callback when the user hits return. + # well have the program prtin out the values of the + # app variable whnen the user hits return + self.entrythingy.bind('', self.print_contents) + +# 24.1.6.4. Coupling Widget Variables +# - get() +# - set() +# StringVar +# IntVar +# DoubleVar +# BooleanVar +# variable +# textvariable \ No newline at end of file diff --git a/_src/om2py2w/2wex0/Tk_test_windows.py b/_src/om2py2w/2wex0/Tk_test_windows.py new file mode 100644 index 000000000..9c35c6118 --- /dev/null +++ b/_src/om2py2w/2wex0/Tk_test_windows.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from Tkinter import * + +class App(Frame): + def __init__(self, master=None): + Frame.__init__(self, master) + self.pack() + +# my app +myapp = App() +# here are method calls to the window manager class + +myapp.master.title("My Do-Nothing Application!") +myapp.master.maxsize(1000, 400) + +# start app +myapp.mainloop() \ No newline at end of file diff --git a/_src/om2py2w/2wex0/app_windows.py b/_src/om2py2w/2wex0/app_windows.py new file mode 100644 index 000000000..e98ee9ede --- /dev/null +++ b/_src/om2py2w/2wex0/app_windows.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from Tkinter import * +# refer to http://effbot.org/tkinterbook/tkinter-application-windows.htm +def callback(): + print "called the callback!" + +root = Tk() + +# top = Toplevel() +menu = Menu(root) +root.config(menu=menu) + +filemenu = Menu(menu) +menu.add_cascade(label="file", menu=filemenu) +filemenu.add_command(label="New", command=callback) +filemenu.add_command(label="Open...", command=callback) +filemenu.add_separator() +filemenu.add_command(label="Exit", command=callback) + +helpmenu = Menu(menu) +menu.add_cascade(label="Help", menu=helpmenu) +helpmenu.add_command(label="About...", command=callback) + +# create the toolbar +toolbar = Frame(root) + +b = Button(toolbar, text="new", width=6, command=callback) +b.pack(side=LEFT, padx=2, pady=2) + +b = Button(toolbar, text="open", width=6, command=callback) +b.pack(side=LEFT, padx=2, pady=2) + +toolbar.pack(side=TOP, fill=X) + +# status bars +status = Label(root, text="", bd=1, relief=SUNKEN, anchor=W) +status.pack(side=BOTTOM, fill=X) + +root.mainloop() \ No newline at end of file diff --git a/_src/om2py2w/2wex0/bring_back_test.py b/_src/om2py2w/2wex0/bring_back_test.py new file mode 100644 index 000000000..33b440b60 --- /dev/null +++ b/_src/om2py2w/2wex0/bring_back_test.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +import Tkinter as tk + +root = tk.Tk() + +root.geometry("300x300") + +frame1 = tk.Frame(root) +frame2 = tk.Frame(frame1) +frame1.pack() +frame2.pack() + +menubar = tk.Menu(frame1) +menubar.add_command(label="bring back!", command=frame2.pack) +menubar.add_command(label="Quit", command=root.destroy) + +tk.Button(frame2, text="Forget only frame2", command=frame2.pack_forget).pack() +tk.Label(frame2, text="Label on frame2").pack() + +root.config(menu=menubar) + +root.mainloop() \ No newline at end of file diff --git a/_src/om2py2w/2wex0/button_widget.py b/_src/om2py2w/2wex0/button_widget.py new file mode 100644 index 000000000..4e5ad0c26 --- /dev/null +++ b/_src/om2py2w/2wex0/button_widget.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from Tkinter import * + +master = Tk() + +"""b = Button(master, text="click me!", image=pattern, compound=CENTER) +b.pack() +you need to define the pattern""" + +"""b = Button(master, text="here is your diary button test!", + anchor=W, justify=LEFT, padx=2) +b.pack()""" + +"""f = Frame(master, height=32, width=32) +f.pack_propagate(0) # dont shrink +f.pack() + +b = Button(f, text="Sure!") +b.pack(fill=BOTH, expand=1)""" + +"""def callback(): + print "click!""" + +"""b = Button(master, text="OK", command=callback) +b.pack() +b = Button(master, text="Help", state=DISABLED) +b.pack()""" +mainloop() \ No newline at end of file diff --git a/_src/om2py2w/2wex0/entry_delete_test.txt b/_src/om2py2w/2wex0/entry_delete_test.txt new file mode 100644 index 000000000..9e018b378 --- /dev/null +++ b/_src/om2py2w/2wex0/entry_delete_test.txt @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/_src/om2py2w/2wex0/entry_widget.py b/_src/om2py2w/2wex0/entry_widget.py new file mode 100644 index 000000000..0837f8723 --- /dev/null +++ b/_src/om2py2w/2wex0/entry_widget.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +from Tkinter import * + +master = Tk() + +content = StringVar() +e = Entry(master, textvariable=content) +e.pack() + +def callback(): + print content.get() + e.delete(0, END) + +input_button = Button(master, text="Enter", width=10, command=callback) +input_button.pack() + +quit_buttion = Button(master, text="Quit", fg="red", width=10, command=master.quit) +quit_buttion.pack() + +mainloop() + + +# user and password +"""def makeentry(parent, caption, width=None, **options): + Label(parent, text=caption).pack(side=LEFT) + entry = Entry(parent, **options) + if width: + entry.config(width=width) + entry.pack(side=LEFT) + return entry + +user = makeentry(master, "User Name:", 10) +password = makeentry(master, "password:", 10, show="*") + +mainloop()""" + +"""e = Entry(master) +e.pack() + +e.focus_set() + +def callback(): + print e.get() + +b = Button(master, text="get", width=10, command=callback) +b.pack() + +mainloop()""" + + +""" +your_words = StringVar() +your_diary_text = Entry(master, textvariable=your_words) +your_diary_text.pack() + +your_words.set("Welcome! Please write!") +s = your_words.get() + +mainloop() +""" \ No newline at end of file diff --git a/_src/om2py2w/2wex0/entry_widget_advance.py b/_src/om2py2w/2wex0/entry_widget_advance.py new file mode 100644 index 000000000..36cbccba1 --- /dev/null +++ b/_src/om2py2w/2wex0/entry_widget_advance.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from Tkinter import * + +master = Tk() + +def makeentry(parent, caption, width=None, **options): + Label(parent, text=caption).pack(side=LEFT) + entry = Entry(parent, **options) + if width: + entry.config(width=width) + entry.pack(side=LEFT) + return entry + +user = makeentry(master, "User Name:", 10) +password = makeentry(master, "password:", 10, show="*") + +mainloop() \ No newline at end of file diff --git a/_src/om2py2w/2wex0/hello_Tk.py b/_src/om2py2w/2wex0/hello_Tk.py new file mode 100644 index 000000000..174029d8d --- /dev/null +++ b/_src/om2py2w/2wex0/hello_Tk.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from Tkinter import * +# import every thing from Tkinter +root = Tk() +# to initialize Tkinter need to creat a TK root widget + +w = Label(root, text="Hello, world!") # Label widget +w.pack() +""" # pack method + + This tells it to size itself to fit the given text, + and make itself visible. """ + +root.mainloop() # start root widget \ No newline at end of file diff --git a/_src/om2py2w/2wex0/hello_Tk_again.py b/_src/om2py2w/2wex0/hello_Tk_again.py new file mode 100644 index 000000000..e037e1ebb --- /dev/null +++ b/_src/om2py2w/2wex0/hello_Tk_again.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +from Tkinter import * + +class App: + def __init__(self, master): # 构造函数 __init__ method + # a parent widget (the master) + # 1 frame widget + frame = Frame(master) # local variable frame + frame.pack() # call the pack method to make the frame visible + + # 2 button widget as the children of the frame widget + self.button = Button( + frame, text="QUIT", fg="red", command=frame.quit + ) # command can be a methon and a function + self.button.pack(side=LEFT) + + self.hi_there = Button(frame, text="Hello", command=self.say_hi) + self.hi_there.pack(side=LEFT) + + def say_hi(self): + print "hi there, jeremiah!" + +root = Tk() # creates a Tk root widget + +app = App(root) # one instance of the App class using the root widget as its parent + +root.mainloop() # start +root.destroy() # optional it explicitly destroys the main window +# when the event loop is terminated. + +self.button = Button(frame, {"text": "QUIT", "fg": "red", "command": frame.quit}) +self.button.pack({"side": LEFT}) + +"""The mainloop call enters the Tk event loop, +in which the application will stay +until the quit method is called +(just click the QUIT button), +or the window is closed.""" \ No newline at end of file diff --git a/_src/om2py2w/2wex0/listbox_widget.py b/_src/om2py2w/2wex0/listbox_widget.py new file mode 100644 index 000000000..f88c762cd --- /dev/null +++ b/_src/om2py2w/2wex0/listbox_widget.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from Tkinter import * +import sys, os, glob + + +master = Tk() +# use list box to print the past logs +past_logs = Listbox(master, width=100, height=20) + +past_logs.pack() + +past_logs.insert(END, "Here is your past logs:--->") + +def read_diary(): + current_dir = os.getcwd() # 打印之前日志 + os.chdir(current_dir) + + for file in glob.glob("*.txt"): + past_logs.insert(END, "Log_name:--->" + file) + file_content = open(file, "r") + past_logs.insert(END, "Log_content:---> " + file_content.read() + "\n") + file_content.close() + +# read_diary() +read_diary() + +mainloop() + +# 调用函数 打印过去的日志 在listbox中 + +"""listbox = Listbox(master) +listbox.pack() + +listbox.insert(END, "a list entry") + +for item in ["one", "two", "three", "four"]: + listbox.insert(END, item) + +mainloop()""" \ No newline at end of file diff --git a/_src/om2py2w/2wex0/main.py b/_src/om2py2w/2wex0/main.py index e69de29bb..e8eee1d92 100644 --- a/_src/om2py2w/2wex0/main.py +++ b/_src/om2py2w/2wex0/main.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +from Tkinter import * +import sys, os, glob + +reload(sys) +sys.setdefaultencoding('utf-8') # encoding Chinese + +class Application(Frame): + + def __init__(self, master): + Frame.__init__(self, master) + + self.readme_Tag = False + self.printLogs_Tag = False + self.newLog_Tag = False + + self.pack() + self.createMenus(master) + + def createScrollbar(self): + self.scrollbar = Scrollbar(self, orient=VERTICAL) + self.scrollbar.pack(side=RIGHT, fill=Y) + + def createText(self): + self.text = Text(self, width=1000, height=400, + yscrollcommand=self.scrollbar.set) + self.text.focus_force() # keybord 光标在Text上 + self.text.pack() + self.text.delete(1.0, END) + self.scrollbar.config(command=self.text.yview) + + def printLogs(self): + + self.printLogs_Tag = True + + if self.readme_Tag: + self.text1.pack_forget() + if self.newLog_Tag: + self.text.pack_forget() + self.scrollbar.pack_forget() + + self.createScrollbar() + self.createText() + + self.text.insert(END, "Here is your past logs:--->" + "\n") + + current_dir = os.getcwd() # print past logs + os.chdir(current_dir) + + for file in glob.glob("*.txt"): + self.text.insert(END, "Log_name:--->" + file + "\n") + file_content = open(file, "r+") + self.text.insert(END, "Log_content:---> " + file_content.read() + "\n \n") + file_content.close() + + def newLog(self): + + self.newLog_Tag = True + if self.readme_Tag: + self.text1.pack_forget() + + if self.printLogs_Tag: + self.text.pack_forget() + self.scrollbar.pack_forget() + + self.createScrollbar() + self.createText() + + def dialog(self): + + top = self.top = Toplevel() + + Label(top, text="Input your log name->").grid(row=0) + + self.log_name = Entry(top) + self.log_name.focus_force() + self.log_name.grid(row=0, column=1) + + self.log_name.bind("", self.ok) + + def ok(self, event): + self.name = self.log_name.get().encode(sys.stdin.encoding) + ".txt" + self.content = self.text.get("1.0", END) + self.top.destroy() + + def save(self): + + self.dialog() + self.wait_window(self) + + log_writer = open(self.name, "a+") + + log_writer.write(self.content) + log_writer.close() + + def createMenus(self, master): + + self.menu = Menu(self) + master.config(menu=self.menu) + + self.filemenu = Menu(self) + self.menu.add_cascade(label="file", menu=self.filemenu) + self.filemenu.add_command(label="PastLogs", command=self.printLogs) + self.filemenu.add_command(label="New", command=self.newLog) + self.filemenu.add_command(label="Save", command=self.save) + self.filemenu.add_separator() + self.filemenu.add_command(label="Exit", command=self.cancel) + + self.helpmenu = Menu(self) + self.menu.add_cascade(label="Help", menu=self.helpmenu) + self.helpmenu.add_command(label="Guide", command=self.readme) + + def cancel(self): + self.master.destroy() + + def readme(self): + + if self.newLog_Tag: + self.text.pack_forget() + self.scrollbar.pack_forget() + + self.readme_Tag = True + + self.text1= Text(self, height=1000, width=400) + + help =""" + 1-Please click PastLogs Menu in the filemenu to see the logs you have wrote. + 2-Then Click the New Menu in the filemenu to write your new log + 3-Please Click the Save Menu if Want to save your log which you have just worte + 4-Exit menu to exit + 5-Guide menu in the Help Menu to read the Diary Guide + """ + self.text1.insert(END, help, "color") + + self.text1.pack() + +def main(): + + master = Tk() + master.title("Diary App") + master.geometry("1000x400") + + statement = Label(master, text="Dear Friend! Welcome!") + statement.pack(side=TOP, fill=X) + + app = Application(master) + app.mainloop() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/_src/om2py2w/2wex0/main_test.py b/_src/om2py2w/2wex0/main_test.py new file mode 100644 index 000000000..14ee18cb5 --- /dev/null +++ b/_src/om2py2w/2wex0/main_test.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from Tkinter import * + +def main(): + + class App: + """Dear here is your diary system! Start writing!--->""" + def __init__(self,master=None): + + frame = Frame(master) + frame.pack() + +# GUI 标题 + diary = Tk() + diary_app = App(master=diary) + + diary_app.master.title("Writing for Loving, Learning and Sharing") + diary_app.master.maxsize(1000,400) + + diary_app.mainloop() + diary.destroy() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/_src/om2py2w/2wex0/mini_app.py b/_src/om2py2w/2wex0/mini_app.py new file mode 100644 index 000000000..01a4cca3d --- /dev/null +++ b/_src/om2py2w/2wex0/mini_app.py @@ -0,0 +1,17 @@ +#!/C:/Python27 python +import Tkinter as tk + +class Application(tk.Frame): + def __init__(self, master=None): + tk.Frame.__init__(self, master) + self.grid() + self.createWidget() + + def createWidget(self): + self.quitButton = tk.Button(self, text="Quit", + command=self.quit) + self.quitButton.grid() + +app = Application() +app.master.title("sample app") +app.mainloop() \ No newline at end of file diff --git a/_src/om2py2w/2wex0/new test.txt b/_src/om2py2w/2wex0/new test.txt new file mode 100644 index 000000000..bf432827b --- /dev/null +++ b/_src/om2py2w/2wex0/new test.txt @@ -0,0 +1,4 @@ +ok +test +rigth + diff --git a/_src/om2py2w/2wex0/new.txt b/_src/om2py2w/2wex0/new.txt new file mode 100644 index 000000000..56a6051ca --- /dev/null +++ b/_src/om2py2w/2wex0/new.txt @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/_src/om2py2w/2wex0/ok bind.txt b/_src/om2py2w/2wex0/ok bind.txt new file mode 100644 index 000000000..603764370 --- /dev/null +++ b/_src/om2py2w/2wex0/ok bind.txt @@ -0,0 +1 @@ +bind ok in ok entry diff --git a/_src/om2py2w/2wex0/past_log_test.txt b/_src/om2py2w/2wex0/past_log_test.txt new file mode 100644 index 000000000..e491dfa4c --- /dev/null +++ b/_src/om2py2w/2wex0/past_log_test.txt @@ -0,0 +1 @@ +test 2015-10-27 \ No newline at end of file diff --git a/_src/om2py2w/2wex0/std_dialogs.py b/_src/om2py2w/2wex0/std_dialogs.py new file mode 100644 index 000000000..12a647fa1 --- /dev/null +++ b/_src/om2py2w/2wex0/std_dialogs.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +from Tkinter import * +import os +import tkSimpleDialog + +# refer to http://effbot.org/tkinterbook/tkinter-standard-dialogs.htm + +"""try: + fp = open(filename) +except: + tkMessageBox.showwarning( + "Open file", + "Cannot open this file\n(%s)" % filename + ) + return""" + +# below refer to http://effbot.org/tkinterbook/tkinter-dialog-windows.htm + +class Dialog(Toplevel): + + def __init__(self, parent, title=None): + + Toplevel.__init__(self, parent) + self.transient(parent) + + if title: + self.title(title) + + self.parent = parent + + self.result = None + + body = Frame(self) + self.initial_focus = self.body(body) + body.pack(padx=5, pady=5) + + self.buttonbox() + + self.grab_set() # 解决 主窗口与对话窗口的混乱 + + if not self.initial_focus: + self.initial_focus = self + + self.protocol("WM_DELETE_WINDOW", self.cancel) + + self.geometry("+%d+%d" % (parent.winfo_rootx()+50, + parent.winfo_rooty()+50)) + + self.initial_focus.focus_set() + + self.wait_window(self) + + # construciton hooks + + def body(self, master): + pass + + def buttonbox(self): + box = Frame(self) + + w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE) + w.pack(side=LEFT, padx=5, pady=5) + w = Button(box, text="Cancel", width=10, command=self.cancel) + w.pack(side=LEFT, padx=5, pady=5) + + self.bind("", self.ok) + self.bind("", self.cancel) + + box.pack() + + def ok(self, event=None): + + if not self.validate(): + self.initial_focus.focus_set() + return + + self.withdraw() + self.update_idletasks() + + self.apply() + + self.cancel() + + def cancel(self, event=None): + self.parent.focus_set() + self.destroy() + + # command hooks + + def validate(self): + return 1 # override + + def apply(self): + pass # override + +class MyDialog(tkSimpleDialog.Dialog): + + def body(self, master): + + Label(master, text="First:").grid(row=0) + Label(master, text="Second:").grid(row=1) + + self.e1 = Entry(master) + self.e2 = Entry(master) + + self.e1.grid(row=0, column=1) + self.e2.grid(row=1, column=1) + + return self.e1 + + def apply(self): + first = int(self.e1.get()) + second = int(self.e2.get()) + print first, second + +root = Tk() +Button(root, text="Hello!").pack() +root.update() + +d = MyDialog(root) + +print d.result \ No newline at end of file diff --git a/_src/om2py2w/2wex0/test.txt b/_src/om2py2w/2wex0/test.txt new file mode 100644 index 000000000..1eeef4ace --- /dev/null +++ b/_src/om2py2w/2wex0/test.txt @@ -0,0 +1,8 @@ + +test +ok test +new +10-29 +right +test ok +new diff --git a/_src/om2py2w/2wex0/write_test.txt b/_src/om2py2w/2wex0/write_test.txt new file mode 100644 index 000000000..14c736760 --- /dev/null +++ b/_src/om2py2w/2wex0/write_test.txt @@ -0,0 +1 @@ +this is the write button test \ No newline at end of file diff --git "a/_src/om2py2w/2wex0/\344\270\255\346\226\207.txt" "b/_src/om2py2w/2wex0/\344\270\255\346\226\207.txt" new file mode 100644 index 000000000..89bc960b9 --- /dev/null +++ "b/_src/om2py2w/2wex0/\344\270\255\346\226\207.txt" @@ -0,0 +1,2 @@ +测试中文编码 +行不行 diff --git "a/_src/om2py2w/2wex0/\344\270\255\346\226\207\345\220\215\345\255\227\346\265\213\350\257\2252.txt" "b/_src/om2py2w/2wex0/\344\270\255\346\226\207\345\220\215\345\255\227\346\265\213\350\257\2252.txt" new file mode 100644 index 000000000..ba45e0308 --- /dev/null +++ "b/_src/om2py2w/2wex0/\344\270\255\346\226\207\345\220\215\345\255\227\346\265\213\350\257\2252.txt" @@ -0,0 +1,10 @@ +中文编码名字 +在App中print名字是中文 正常显示 +但是在 文件夹中 名字是乱码 + +这是测试看看可不可以 +中文编码名字 +在App中print名字是中文 正常显示 +但是在 文件夹中 名字是乱码 + +这是测试看看可不可以 diff --git "a/_src/om2py2w/2wex0/\346\266\223\356\205\237\346\236\203\351\215\232\345\266\205\347\223\247\347\274\202\346\240\253\347\210\234.txt" "b/_src/om2py2w/2wex0/\346\266\223\356\205\237\346\236\203\351\215\232\345\266\205\347\223\247\347\274\202\346\240\253\347\210\234.txt" new file mode 100644 index 000000000..f2109432b --- /dev/null +++ "b/_src/om2py2w/2wex0/\346\266\223\356\205\237\346\236\203\351\215\232\345\266\205\347\223\247\347\274\202\346\240\253\347\210\234.txt" @@ -0,0 +1 @@ +测试中文名字编码问题 diff --git a/_src/om2py2w/README.md b/_src/om2py2w/README.md index 3d0591f32..e08b1ada3 100644 --- a/_src/om2py2w/README.md +++ b/_src/om2py2w/README.md @@ -3,5 +3,17 @@ ## 2w - 私人笔记: - + GUI + + GUI - 极简日记交互GUI 代码:https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py2w/2wex0/main.py + + 直接调用 + + GUI中 请点击 help菜单中 有Guide帮助文档 + + File菜单中 + + PastLogs打印过去日志 + + New 新建日志 + + Save 保存日志 + + Exit 退出 应用 + .pynb +- 开发感想 + - 自己给自己挖坑 填得费力啊 + - 得清楚 未知量(开发目标) 是什么 + - 已有的知识有哪些 如何补足 + - 我如何更快更好 实现开发的应用 \ No newline at end of file diff --git a/_src/om2py3w/3wex0/client.py b/_src/om2py3w/3wex0/client.py new file mode 100644 index 000000000..75ce7d9d9 --- /dev/null +++ b/_src/om2py3w/3wex0/client.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +import socket +import sys + +# create dgram udp socket +try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +except socket.error: + print "Failed to create socket" + sys.exit() + +host = "localhost" +port = 8888 + +# print past logs when start +s.sendto("p", (host, port)) +d = s.recvfrom(1024) +reply = d[0] +print "Past logs: ---> \n" + reply + +while 1: + print "Wanna help? Enter h" + msg = raw_input("Enter message to send: ---> ") + + if msg =="e": + sys.exit() + + try : + # send the whole string + s.sendto(msg, (host, port)) # send to serve + + # receive data from server + d = s.recvfrom(1024) + reply = d[0] + addr = d[1] + + print "server reply: ---> " + reply + + except socket.error, msg: + print "error code: " + str(msg[0]) + " message " + msg[1] + sys.exit() diff --git a/_src/om2py3w/3wex0/log test.log~ b/_src/om2py3w/3wex0/log test.log~ new file mode 100644 index 000000000..f77a6de76 --- /dev/null +++ b/_src/om2py3w/3wex0/log test.log~ @@ -0,0 +1,5 @@ + +恩 这是一此车市测试 +就这样 + +2015-10-31 20:17:34 \ No newline at end of file diff --git a/_src/om2py3w/3wex0/server.py b/_src/om2py3w/3wex0/server.py new file mode 100644 index 000000000..2c93c8981 --- /dev/null +++ b/_src/om2py3w/3wex0/server.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +import socket, sys, os, glob + +def main(): + HOST = '' # Symbolic name meaning all available interfaces + PORT = 8888 # Arbitrary non-privileged port + # UDP SOCKET + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + print "socket created! " + except socket.error, msg : + print "failed to created socket. ERROR code : " + str(msg[0]) + " message " + msg + sys.exit() + + # bind socket to localhost and port + try: + s.bind((HOST, PORT)) + except socket.error, msg: + print "bind failed. error code: " + str(msg[0]) + " message " + msg[1] + sys.exit() + + print "socket bind complete" + # communicate with the client + while 1: + # receive from client + d = s.recvfrom(1024) + data = d[0] # client message + addr = d[1] # client address addr = (host, port) + + reply = responses(data) + s.sendto(reply, addr) + + print "message from [ " + addr[0] + ":" + str(addr[1]) + "] is --->" + data.strip() + + s.close() + +def help(): + """ # this is the help doc: + + 1- read past logs? enter:---> p + 2- want leave ? enter:---> e + 3- help doc? enter:---> h + """ + +def responses(mydata): + if mydata =="e": + sys.exit() + elif mydata =="p": + reply = read_diary() + elif mydata == "h": + reply = help.__doc__ + else: + diary_name = "jeremiah_diary.log" + diary_writer = open(diary_name, "a+") + diary_writer.write(mydata + "\n") + diary_writer.close() + reply = "Continue to Write:--->" + return reply + +def read_diary(): + current_dir = os.getcwd() # 打印之前日志 + os.chdir(current_dir) + + filename_plus_content ="" + for file in glob.glob("*.log"): + + file_content = open(file, "r") + diary = file_content.read() + "\n" + filename_plus_content = filename_plus_content + file + "--->: \n" +diary + return filename_plus_content + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/_src/om2py3w/3wex1/client2.py b/_src/om2py3w/3wex1/client2.py new file mode 100644 index 000000000..3a4592628 --- /dev/null +++ b/_src/om2py3w/3wex1/client2.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +import socket +import sys + +# create dgram udp socket +try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +except socket.error: + print "Failed to create socket" + sys.exit() + +host = "localhost02" +port = 8888 + +print "please enter h to see the help.-->" + +while 1: + msg = raw_input("Enter message to send: ---> ") + + try : + # send the whole string + s.sendto(msg, (host, port)) # send to serve + + # receive data from server + d = s.recvfrom(1024) + reply = d[0] + addr = d[1] + + print "server reply: ---> " + reply + + except socket.error, msg: + print "error code: " + str(msg[0]) + " message " + msg[1] + sys.exit() diff --git a/_src/om2py3w/3wex1/diary_client.py b/_src/om2py3w/3wex1/diary_client.py new file mode 100644 index 000000000..c003ec5de --- /dev/null +++ b/_src/om2py3w/3wex1/diary_client.py @@ -0,0 +1,30 @@ +# coding:utf-8 +# Author: Jeremiah Zhang +# Email: zhangleisuda@gmail.com +# beta 1.0 +import socket + +def HELP(): + """ # Dear , Here is the Help Doc: + + 1 Input: p/past , print past logs + """ + +print HELP.__doc__ + +# pastlog_keyword = "p" + +# Creat socket +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + +host_address = ('localhost', 8001) +sock.connect(host_address) +# continue interact +done = False +while done==False: + pastlog_keyword = raw_input("Please write here Dear! --->") + sock.sendto(pastlog_keyword, host_address) + back_message = sock.recvfrom(1024) + print back_message + +sock.close() diff --git a/_src/om2py3w/3wex1/diary_server.py b/_src/om2py3w/3wex1/diary_server.py new file mode 100644 index 000000000..49a405ead --- /dev/null +++ b/_src/om2py3w/3wex1/diary_server.py @@ -0,0 +1,39 @@ +# coding:utf-8 +import socket +import jeremiah_diary + +pastlog_keyword = "p" + +# main +def main(): + # creat + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + host_address = ('localhost', 8001) + # bind + sock.bind(host_address) + + # interact + + while True: + print "Please Wait:---> " + + data = sock.recvfrom(1024) # reveive message from client + print "You have received message from {0}".format(address) + + if data == "p": + past_logs = jeremiah_diary.read_diary() + sock.sendto(past_logs, address) # print past logs + if data == "h": + help_doc = "balabala" + sock.sendto(help_doc, address) + else: + diary_name = "jeremiah_diary.log" + diary_writer = open(diary_name, "a+") + diary_writer.write(data) + back_message = "Continue to Write:--->" + sock.sendto(back_message, address) + # connection.close() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/_src/om2py3w/3wex1/ex1_client.py b/_src/om2py3w/3wex1/ex1_client.py new file mode 100644 index 000000000..c07899b65 --- /dev/null +++ b/_src/om2py3w/3wex1/ex1_client.py @@ -0,0 +1,13 @@ +# coding=utf-8 +# refer https://docs.python.org/2/library/socket.html?highlight=socket#example +import socket + +HOST = 'localhost'# the remote host +PORT = 8001 + +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.connect((HOST, PORT)) +s.sendall('hello, world') +data = s.recv(1024) +s.close() +print "received", repr(data) \ No newline at end of file diff --git a/_src/om2py3w/3wex1/ex1_server.py b/_src/om2py3w/3wex1/ex1_server.py new file mode 100644 index 000000000..75701e3fd --- /dev/null +++ b/_src/om2py3w/3wex1/ex1_server.py @@ -0,0 +1,18 @@ +# coding=utf-8 +# refet to https://docs.python.org/2/library/socket.html?highlight=socket#example +import socket + +HOST = 'localhost' +PORT = 8001 + +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.bind((HOST, PORT)) +s.listen(1) +conn, addr = s.accept() + +print 'connected by', addr +while 1: + data = conn.recv(1024) + if not data: break + conn.sendall(data) +conn.close() \ No newline at end of file diff --git a/_src/om2py3w/3wex1/jeremiah_diary.py b/_src/om2py3w/3wex1/jeremiah_diary.py new file mode 100644 index 000000000..6323c8ea5 --- /dev/null +++ b/_src/om2py3w/3wex1/jeremiah_diary.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +import sys, os, glob +from time import localtime, strftime + +reload(sys) # 必须 reload +sys.setdefaultencoding('utf-8') + +def main(): + + read_diary() + + write_diary() + +def welcome_statement(): + """ # Dear, welcome abord in Diary Ship + + 1. Please input your words as following prompt + 2. Wanna Leave? Please input the word "end" alone + 3. Thanks, Hope you have fun in writing. > < + + Let's Start. GO + """ + +def read_diary(): + + current_dir = os.getcwd() # 打印之前日志 + os.chdir(current_dir) + + filename_plus_content ="" + + for file in glob.glob("*.log"): + # print(file) # this is the file name + file_content = open(file, "r") + diary = file_content.read() + "\n" + filename_plus_content = filename_plus_content + file + "--->: \n" +diary + # print filename_plus_content + return filename_plus_content + +def write_diary(): + done = False + textInput = "" + + diary_name = raw_input("请输入日志名: ".encode(sys.stdout.encoding)) + ".log" + diary_writer = open(diary_name, "w") + + while (done==False): + nextInput = raw_input("Please input your diary words: ") + if nextInput == "end": + inputDate = ask_date("Wanna add diary date time? yes or no! ") + diary_writer.write("\n" + inputDate) + break + else: + textInput += nextInput + "\n" + diary_writer.write(nextInput + "\n") + + diary_writer.close() + print ("Here is your " + inputDate +" diary: " + textInput) + +def ask_date(prompt): # 是否添加时间 + yes_list = ['yes', 'yep', 'ye', 'y', 'YES','YEP', 'YE', 'Y'] + no_list = ['no', 'n', 'NO', 'N'] + + ok = raw_input(prompt) + if ok in yes_list: + your_datetime = strftime("%Y-%m-%d %H:%M:%S", localtime()) + if ok in no_list: + your_datetime = "\n" + + return your_datetime + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/_src/om2py3w/3wex1/udp_server.py b/_src/om2py3w/3wex1/udp_server.py new file mode 100644 index 000000000..bb2a3f588 --- /dev/null +++ b/_src/om2py3w/3wex1/udp_server.py @@ -0,0 +1,9 @@ +# coding=utf-8 +import socket +port = 5000 +s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +s.bind(("", port)) +print "waiting on port: ", port +while 1: + data, addr = s.recvfrom(1024) + print data \ No newline at end of file diff --git a/_src/om2py3w/3wex1/udp_socketserver.py b/_src/om2py3w/3wex1/udp_socketserver.py new file mode 100644 index 000000000..2d98d8841 --- /dev/null +++ b/_src/om2py3w/3wex1/udp_socketserver.py @@ -0,0 +1,22 @@ +# coding=utf-8 +import SocketServer + +class MyUDPHandler(SocketServer.BaseRequestHandler): + """ + This class works similar to the TCP handler class, except that + self.request consists of a pair of data and client socket, and since + there is no connection the client address must be given explicitly + when sending data back via sendto(). + """ + + def handle(self): + data = self.request[0].strip() + socket = self.request[1] + print "{} wrote:".format(self.client_address[0]) + print data + socket.sendto(data.upper(), self.client_address) + +if __name__ == "__main__": + HOST, PORT = "localhost", 9999 + server = SocketServer.UDPServer((HOST, PORT), MyUDPHandler) + server.serve_forever() \ No newline at end of file diff --git a/_src/om2py3w/3wex1/udp_socketserver_client.py b/_src/om2py3w/3wex1/udp_socketserver_client.py new file mode 100644 index 000000000..84d0e897f --- /dev/null +++ b/_src/om2py3w/3wex1/udp_socketserver_client.py @@ -0,0 +1,17 @@ +# coding=utf-8 +import socket +import sys + +HOST, PORT = "localhost", 9999 +data = " ".join(sys.argv[1:]) + +# SOCK_DGRAM is the socket type to use for UDP sockets +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + +# As you can see, there is no connect() call; UDP has no connections. +# Instead, data is directly sent to the recipient via sendto(). +sock.sendto(data + "\n", (HOST, PORT)) +received = sock.recv(1024) + +print "Sent: {}".format(data) +print "Received: {}".format(received) \ No newline at end of file diff --git a/_src/om2py3w/3wex2/fibo.py b/_src/om2py3w/3wex2/fibo.py new file mode 100644 index 000000000..0c47cfd72 --- /dev/null +++ b/_src/om2py3w/3wex2/fibo.py @@ -0,0 +1,19 @@ +# fibonacci +def fib(n): + a, b = 0, 1 + while b < n: + print b, + a, b = b, a+b + +def fib2(n): + result = [] + a, b = 0, 1 + while b < n: + result.append(b) + a, b = b, a+b + return result + +if __name__ == '__main__': + import sys + fib(int(sys.argv[1])) + fib(int(sys.argv[2])) \ No newline at end of file diff --git a/_src/om2py3w/README.md b/_src/om2py3w/README.md index 2b4f27804..77a2484df 100644 --- a/_src/om2py3w/README.md +++ b/_src/om2py3w/README.md @@ -1,7 +1,15 @@ # OMOOC.py 周任务代码试作 -## 3w +## 3w 极简日志网络版 -- 私人笔记: - + UDP 网络服务 - + HTTP 服务端 \ No newline at end of file +执行: + +- 终端调用: 服务端 代码 server.py +- 然后 终端调用: 客户端 代码 client.py + - 自动打印过去日志 + - 然后可以输入 + - h 可以 查看帮助 + - e 退出 + - p 打印过去日志 +- 问题: + - 多客户端书写尚未解决 diff --git a/_src/om2py4w/4wex0/CLI.py b/_src/om2py4w/4wex0/CLI.py new file mode 100644 index 000000000..548df8e56 --- /dev/null +++ b/_src/om2py4w/4wex0/CLI.py @@ -0,0 +1,44 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +import requests +from bs4 import BeautifulSoup +import sys + +def help(): + """ # Welcome This is help doc: + + 1. Quit Please Type : exit /q/quit/Q + 2. See Help Document Type: help/H/h/? + 3. See Diary Histroy Type: hist + + Let's Start. GO""" + +def hist_logs(): + html_doc = requests.get('http://localhost:8010/') # html_doc.text is the content of html + soup = BeautifulSoup(html_doc.text, 'html.parser') # html + soup_textarea = soup.textarea # TYPE is list + textarea_contents_str = soup_textarea.contents[0] + print textarea_contents_str + +def input_logs(yourwords): + data = {'words': yourwords, 'save': 'save'} # two input in write_words.tpl so you must add name='save' item + requests.get('http://localhost:8010/write', params = data) + +def main(): + + print help.__doc__ + + while True: + yourwords = raw_input('Type your words--->$') + + if yourwords in {'exit', 'q', 'quit','Q'}: + sys.exit() + elif yourwords in {'help', 'H','h','?'}: + print help.__doc__ + elif yourwords == 'hist': + hist_logs() + else: + input_logs(yourwords) # write new diarys or logs + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/_src/om2py4w/4wex0/Diary.log~ b/_src/om2py4w/4wex0/Diary.log~ new file mode 100644 index 000000000..e3e09293d --- /dev/null +++ b/_src/om2py4w/4wex0/Diary.log~ @@ -0,0 +1,14 @@ +请刷出所有历史笔记 +我现在在尝试刷出所有历史笔记 +我之前直接在写入笔记之后 读取 发现 无法读取内容 +现在 我在后面添加 打开文件 读取 并 关闭文件 恩这次可以了 +还能么 +还能额 +这个厉害 +cli input +new +ok +get and post +check url +write +write diff --git a/_src/om2py4w/4wex0/bottle.py b/_src/om2py4w/4wex0/bottle.py new file mode 100644 index 000000000..dcd20136a --- /dev/null +++ b/_src/om2py4w/4wex0/bottle.py @@ -0,0 +1,4011 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Bottle is a fast and simple micro-framework for small web applications. It +offers request dispatching (Routes) with URL parameter support, templates, +a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and +template engines - all in a single file and with no dependencies other than the +Python Standard Library. + +Homepage and documentation: http://bottlepy.org/ + +Copyright (c) 2014, Marcel Hellkamp. +License: MIT (see LICENSE for details) +""" + +from __future__ import with_statement +import sys + +__author__ = 'Marcel Hellkamp' +__version__ = '0.13-dev' +__license__ = 'MIT' + +############################################################################### +# Command-line interface ######################################################## +############################################################################### +# INFO: Some server adapters need to monkey-patch std-lib modules before they +# are imported. This is why some of the command-line handling is done here, but +# the actual call to main() is at the end of the file. + + +def _cli_parse(args): + from optparse import OptionParser + parser = OptionParser( + usage="usage: %prog [options] package.module:app") + opt = parser.add_option + opt("--version", action="store_true", help="show version number.") + opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") + opt("-s", "--server", default='wsgiref', help="use SERVER as backend.") + opt("-p", "--plugin", action="append", help="install additional plugin/s.") + opt("-c", "--conf", action="append", metavar="FILE", + help="load config values from FILE.") + opt("-C", "--param", action="append", metavar="NAME=VALUE", + help="override config values.") + opt("--debug", action="store_true", help="start server in debug mode.") + opt("--reload", action="store_true", help="auto-reload on file changes.") + opts, args = parser.parse_args(args[1:]) + + return opts, args, parser + + +def _cli_patch(args): + opts, _, _ = _cli_parse(args) + if opts.server: + if opts.server.startswith('gevent'): + import gevent.monkey + gevent.monkey.patch_all() + elif opts.server.startswith('eventlet'): + import eventlet + eventlet.monkey_patch() + + +if __name__ == '__main__': + _cli_patch(sys.argv) + +############################################################################### +# Imports and Python 2/3 unification ########################################### +############################################################################### + + +import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\ + os, re, tempfile, threading, time, warnings + +from types import FunctionType +from datetime import date as datedate, datetime, timedelta +from tempfile import TemporaryFile +from traceback import format_exc, print_exc +from unicodedata import normalize + +# inspect.getargspec was removed in Python 3.6, use +# Signature-based version where we can (Python 3.3+) +try: + from inspect import signature + def getargspec(func): + params = signature(func).parameters + args, varargs, keywords, defaults = [], None, None, [] + for name, param in params.items(): + if param.kind == param.VAR_POSITIONAL: + varargs = name + elif param.kind == param.VAR_KEYWORD: + keywords = name + else: + args.append(name) + if param.default is not param.empty: + defaults.append(param.default) + return (args, varargs, keywords, tuple(defaults) or None) +except ImportError: + from inspect import getargspec + +try: + from simplejson import dumps as json_dumps, loads as json_lds +except ImportError: # pragma: no cover + try: + from json import dumps as json_dumps, loads as json_lds + except ImportError: + try: + from django.utils.simplejson import dumps as json_dumps, loads as json_lds + except ImportError: + + def json_dumps(data): + raise ImportError( + "JSON support requires Python 2.6 or simplejson.") + + json_lds = json_dumps + +# We now try to fix 2.5/2.6/3.1/3.2 incompatibilities. +# It ain't pretty but it works... Sorry for the mess. + +py = sys.version_info +py3k = py >= (3, 0, 0) +py25 = py < (2, 6, 0) +py31 = (3, 1, 0) <= py < (3, 2, 0) + +# Workaround for the missing "as" keyword in py3k. +def _e(): + return sys.exc_info()[1] + +# Workaround for the "print is a keyword/function" Python 2/3 dilemma +# and a fallback for mod_wsgi (resticts stdout/err attribute access) +try: + _stdout, _stderr = sys.stdout.write, sys.stderr.write +except IOError: + _stdout = lambda x: sys.stdout.write(x) + _stderr = lambda x: sys.stderr.write(x) + +# Lots of stdlib and builtin differences. +if py3k: + import http.client as httplib + import _thread as thread + from urllib.parse import urljoin, SplitResult as UrlSplitResult + from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote + urlunquote = functools.partial(urlunquote, encoding='latin1') + from http.cookies import SimpleCookie + from collections import MutableMapping as DictMixin + import pickle + from io import BytesIO + from configparser import ConfigParser, Error as ConfigParserError + basestring = str + unicode = str + json_loads = lambda s: json_lds(touni(s)) + callable = lambda x: hasattr(x, '__call__') + imap = map + + def _raise(*a): + raise a[0](a[1]).with_traceback(a[2]) +else: # 2.x + import httplib + import thread + from urlparse import urljoin, SplitResult as UrlSplitResult + from urllib import urlencode, quote as urlquote, unquote as urlunquote + from Cookie import SimpleCookie + from itertools import imap + import cPickle as pickle + from StringIO import StringIO as BytesIO + from ConfigParser import SafeConfigParser as ConfigParser, \ + Error as ConfigParserError + if py25: + msg = "Python 2.5 support may be dropped in future versions of Bottle." + warnings.warn(msg, DeprecationWarning) + from UserDict import DictMixin + + def next(it): + return it.next() + + bytes = str + else: # 2.6, 2.7 + from collections import MutableMapping as DictMixin + unicode = unicode + json_loads = json_lds + eval(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec')) + + +# Some helpers for string/byte handling +def tob(s, enc='utf8'): + return s.encode(enc) if isinstance(s, unicode) else bytes(s) + + +def touni(s, enc='utf8', err='strict'): + if isinstance(s, bytes): + return s.decode(enc, err) + else: + return unicode(s or ("" if s is None else s)) + + +tonat = touni if py3k else tob + +# 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense). +# 3.1 needs a workaround. +if py31: + from io import TextIOWrapper + + class NCTextIOWrapper(TextIOWrapper): + def close(self): + pass # Keep wrapped buffer open. + + +# A bug in functools causes it to break if the wrapper is an instance method +def update_wrapper(wrapper, wrapped, *a, **ka): + try: + functools.update_wrapper(wrapper, wrapped, *a, **ka) + except AttributeError: + pass + +# These helpers are used at module level and need to be defined first. +# And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. + + +def depr(message, strict=False): + warnings.warn(message, DeprecationWarning, stacklevel=3) + + +def makelist(data): # This is just too handy + if isinstance(data, (tuple, list, set, dict)): + return list(data) + elif data: + return [data] + else: + return [] + + +class DictProperty(object): + """ Property that maps to a key in a local dict-like attribute. """ + + def __init__(self, attr, key=None, read_only=False): + self.attr, self.key, self.read_only = attr, key, read_only + + def __call__(self, func): + functools.update_wrapper(self, func, updated=[]) + self.getter, self.key = func, self.key or func.__name__ + return self + + def __get__(self, obj, cls): + if obj is None: return self + key, storage = self.key, getattr(obj, self.attr) + if key not in storage: storage[key] = self.getter(obj) + return storage[key] + + def __set__(self, obj, value): + if self.read_only: raise AttributeError("Read-Only property.") + getattr(obj, self.attr)[self.key] = value + + def __delete__(self, obj): + if self.read_only: raise AttributeError("Read-Only property.") + del getattr(obj, self.attr)[self.key] + + +class cached_property(object): + """ A property that is only computed once per instance and then replaces + itself with an ordinary attribute. Deleting the attribute resets the + property. """ + + def __init__(self, func): + self.__doc__ = getattr(func, '__doc__') + self.func = func + + def __get__(self, obj, cls): + if obj is None: return self + value = obj.__dict__[self.func.__name__] = self.func(obj) + return value + + +class lazy_attribute(object): + """ A property that caches itself to the class object. """ + + def __init__(self, func): + functools.update_wrapper(self, func, updated=[]) + self.getter = func + + def __get__(self, obj, cls): + value = self.getter(cls) + setattr(cls, self.__name__, value) + return value + +############################################################################### +# Exceptions and Events ######################################################## +############################################################################### + + +class BottleException(Exception): + """ A base class for exceptions used by bottle. """ + pass + +############################################################################### +# Routing ###################################################################### +############################################################################### + + +class RouteError(BottleException): + """ This is a base class for all routing related exceptions """ + + +class RouteReset(BottleException): + """ If raised by a plugin or request handler, the route is reset and all + plugins are re-applied. """ + + +class RouterUnknownModeError(RouteError): + + pass + + +class RouteSyntaxError(RouteError): + """ The route parser found something not supported by this router. """ + + +class RouteBuildError(RouteError): + """ The route could not be built. """ + + +def _re_flatten(p): + """ Turn all capturing groups in a regular expression pattern into + non-capturing groups. """ + if '(' not in p: + return p + return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', lambda m: m.group(0) if + len(m.group(1)) % 2 else m.group(1) + '(?:', p) + + +class Router(object): + """ A Router is an ordered collection of route->target pairs. It is used to + efficiently match WSGI requests against a number of routes and return + the first target that satisfies the request. The target may be anything, + usually a string, ID or callable object. A route consists of a path-rule + and a HTTP method. + + The path-rule is either a static path (e.g. `/contact`) or a dynamic + path that contains wildcards (e.g. `/wiki/`). The wildcard syntax + and details on the matching order are described in docs:`routing`. + """ + + default_pattern = '[^/]+' + default_filter = 're' + + #: The current CPython regexp implementation does not allow more + #: than 99 matching groups per regular expression. + _MAX_GROUPS_PER_PATTERN = 99 + + def __init__(self, strict=False): + self.rules = [] # All rules in order + self._groups = {} # index of regexes to find them in dyna_routes + self.builder = {} # Data structure for the url builder + self.static = {} # Search structure for static routes + self.dyna_routes = {} + self.dyna_regexes = {} # Search structure for dynamic routes + #: If true, static routes are no longer checked first. + self.strict_order = strict + self.filters = { + 're': lambda conf: (_re_flatten(conf or self.default_pattern), + None, None), + 'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))), + 'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))), + 'path': lambda conf: (r'.+?', None, None) + } + + def add_filter(self, name, func): + """ Add a filter. The provided function is called with the configuration + string as parameter and must return a (regexp, to_python, to_url) tuple. + The first element is a string, the last two are callables or None. """ + self.filters[name] = func + + rule_syntax = re.compile('(\\\\*)' + '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)' + '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)' + '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') + + def _itertokens(self, rule): + offset, prefix = 0, '' + for match in self.rule_syntax.finditer(rule): + prefix += rule[offset:match.start()] + g = match.groups() + if len(g[0]) % 2: # Escaped wildcard + prefix += match.group(0)[len(g[0]):] + offset = match.end() + continue + if prefix: + yield prefix, None, None + name, filtr, conf = g[4:7] if g[2] is None else g[1:4] + yield name, filtr or 'default', conf or None + offset, prefix = match.end(), '' + if offset <= len(rule) or prefix: + yield prefix + rule[offset:], None, None + + def add(self, rule, method, target, name=None): + """ Add a new rule or replace the target for an existing rule. """ + anons = 0 # Number of anonymous wildcards found + keys = [] # Names of keys + pattern = '' # Regular expression pattern with named groups + filters = [] # Lists of wildcard input filters + builder = [] # Data structure for the URL builder + is_static = True + + for key, mode, conf in self._itertokens(rule): + if mode: + is_static = False + if mode == 'default': mode = self.default_filter + mask, in_filter, out_filter = self.filters[mode](conf) + if not key: + pattern += '(?:%s)' % mask + key = 'anon%d' % anons + anons += 1 + else: + pattern += '(?P<%s>%s)' % (key, mask) + keys.append(key) + if in_filter: filters.append((key, in_filter)) + builder.append((key, out_filter or str)) + elif key: + pattern += re.escape(key) + builder.append((None, key)) + + self.builder[rule] = builder + if name: self.builder[name] = builder + + if is_static and not self.strict_order: + self.static.setdefault(method, {}) + self.static[method][self.build(rule)] = (target, None) + return + + try: + re_pattern = re.compile('^(%s)$' % pattern) + re_match = re_pattern.match + except re.error: + raise RouteSyntaxError("Could not add Route: %s (%s)" % + (rule, _e())) + + if filters: + + def getargs(path): + url_args = re_match(path).groupdict() + for name, wildcard_filter in filters: + try: + url_args[name] = wildcard_filter(url_args[name]) + except ValueError: + raise HTTPError(400, 'Path has wrong format.') + return url_args + elif re_pattern.groupindex: + + def getargs(path): + return re_match(path).groupdict() + else: + getargs = None + + flatpat = _re_flatten(pattern) + whole_rule = (rule, flatpat, target, getargs) + + if (flatpat, method) in self._groups: + if DEBUG: + msg = 'Route <%s %s> overwrites a previously defined route' + warnings.warn(msg % (method, rule), RuntimeWarning) + self.dyna_routes[method][ + self._groups[flatpat, method]] = whole_rule + else: + self.dyna_routes.setdefault(method, []).append(whole_rule) + self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1 + + self._compile(method) + + def _compile(self, method): + all_rules = self.dyna_routes[method] + comborules = self.dyna_regexes[method] = [] + maxgroups = self._MAX_GROUPS_PER_PATTERN + for x in range(0, len(all_rules), maxgroups): + some = all_rules[x:x + maxgroups] + combined = (flatpat for (_, flatpat, _, _) in some) + combined = '|'.join('(^%s$)' % flatpat for flatpat in combined) + combined = re.compile(combined).match + rules = [(target, getargs) for (_, _, target, getargs) in some] + comborules.append((combined, rules)) + + def build(self, _name, *anons, **query): + """ Build an URL by filling the wildcards in a rule. """ + builder = self.builder.get(_name) + if not builder: + raise RouteBuildError("No route with that name.", _name) + try: + for i, value in enumerate(anons): + query['anon%d' % i] = value + url = ''.join([f(query.pop(n)) if n else f for (n, f) in builder]) + return url if not query else url + '?' + urlencode(query) + except KeyError: + raise RouteBuildError('Missing URL argument: %r' % _e().args[0]) + + def match(self, environ): + """ Return a (target, url_args) tuple or raise HTTPError(400/404/405). """ + verb = environ['REQUEST_METHOD'].upper() + path = environ['PATH_INFO'] or '/' + + if verb == 'HEAD': + methods = ['PROXY', verb, 'GET', 'ANY'] + else: + methods = ['PROXY', verb, 'ANY'] + + for method in methods: + if method in self.static and path in self.static[method]: + target, getargs = self.static[method][path] + return target, getargs(path) if getargs else {} + elif method in self.dyna_regexes: + for combined, rules in self.dyna_regexes[method]: + match = combined(path) + if match: + target, getargs = rules[match.lastindex - 1] + return target, getargs(path) if getargs else {} + + # No matching route found. Collect alternative methods for 405 response + allowed = set([]) + nocheck = set(methods) + for method in set(self.static) - nocheck: + if path in self.static[method]: + allowed.add(verb) + for method in set(self.dyna_regexes) - allowed - nocheck: + for combined, rules in self.dyna_regexes[method]: + match = combined(path) + if match: + allowed.add(method) + if allowed: + allow_header = ",".join(sorted(allowed)) + raise HTTPError(405, "Method not allowed.", Allow=allow_header) + + # No matching route and no alternative method found. We give up + raise HTTPError(404, "Not found: " + repr(path)) + + +class Route(object): + """ This class wraps a route callback along with route specific metadata and + configuration and applies Plugins on demand. It is also responsible for + turing an URL path rule into a regular expression usable by the Router. + """ + + def __init__(self, app, rule, method, callback, + name=None, + plugins=None, + skiplist=None, **config): + #: The application this route is installed to. + self.app = app + #: The path-rule string (e.g. ``/wiki/``). + self.rule = rule + #: The HTTP method as a string (e.g. ``GET``). + self.method = method + #: The original callback with no plugins applied. Useful for introspection. + self.callback = callback + #: The name of the route (if specified) or ``None``. + self.name = name or None + #: A list of route-specific plugins (see :meth:`Bottle.route`). + self.plugins = plugins or [] + #: A list of plugins to not apply to this route (see :meth:`Bottle.route`). + self.skiplist = skiplist or [] + #: Additional keyword arguments passed to the :meth:`Bottle.route` + #: decorator are stored in this dictionary. Used for route-specific + #: plugin configuration and meta-data. + self.config = ConfigDict().load_dict(config) + + @cached_property + def call(self): + """ The route callback with all plugins applied. This property is + created on demand and then cached to speed up subsequent requests.""" + return self._make_callback() + + def reset(self): + """ Forget any cached values. The next time :attr:`call` is accessed, + all plugins are re-applied. """ + self.__dict__.pop('call', None) + + def prepare(self): + """ Do all on-demand work immediately (useful for debugging).""" + self.call + + def all_plugins(self): + """ Yield all Plugins affecting this route. """ + unique = set() + for p in reversed(self.app.plugins + self.plugins): + if True in self.skiplist: break + name = getattr(p, 'name', False) + if name and (name in self.skiplist or name in unique): continue + if p in self.skiplist or type(p) in self.skiplist: continue + if name: unique.add(name) + yield p + + def _make_callback(self): + callback = self.callback + for plugin in self.all_plugins(): + try: + if hasattr(plugin, 'apply'): + callback = plugin.apply(callback, self) + else: + callback = plugin(callback) + except RouteReset: # Try again with changed configuration. + return self._make_callback() + if not callback is self.callback: + update_wrapper(callback, self.callback) + return callback + + def get_undecorated_callback(self): + """ Return the callback. If the callback is a decorated function, try to + recover the original function. """ + func = self.callback + func = getattr(func, '__func__' if py3k else 'im_func', func) + closure_attr = '__closure__' if py3k else 'func_closure' + while hasattr(func, closure_attr) and getattr(func, closure_attr): + attributes = getattr(func, closure_attr) + func = attributes[0].cell_contents + + # in case of decorators with multiple arguments + if not isinstance(func, FunctionType): + # pick first FunctionType instance from multiple arguments + func = filter(lambda x: isinstance(x, FunctionType), + map(lambda x: x.cell_contents, attributes)) + func = list(func)[0] # py3 support + return func + + def get_callback_args(self): + """ Return a list of argument names the callback (most likely) accepts + as keyword arguments. If the callback is a decorated function, try + to recover the original function before inspection. """ + return getargspec(self.get_undecorated_callback())[0] + + def get_config(self, key, default=None): + """ Lookup a config field and return its value, first checking the + route.config, then route.app.config.""" + for conf in (self.config, self.app.config): + if key in conf: return conf[key] + return default + + def __repr__(self): + cb = self.get_undecorated_callback() + return '<%s %r %r>' % (self.method, self.rule, cb) + +############################################################################### +# Application Object ########################################################### +############################################################################### + + +class Bottle(object): + """ Each Bottle object represents a single, distinct web application and + consists of routes, callbacks, plugins, resources and configuration. + Instances are callable WSGI applications. + + :param catchall: If true (default), handle all exceptions. Turn off to + let debugging middleware handle exceptions. + """ + + def __init__(self, catchall=True, autojson=True): + #: A :class:`ConfigDict` for app specific configuration. + self.config = ConfigDict() + self.config._on_change = functools.partial(self.trigger_hook, 'config') + self.config.meta_set('autojson', 'validate', bool) + self.config.meta_set('catchall', 'validate', bool) + self.config['catchall'] = catchall + self.config['autojson'] = autojson + + #: A :class:`ResourceManager` for application files + self.resources = ResourceManager() + + self.routes = [] # List of installed :class:`Route` instances. + self.router = Router() # Maps requests to :class:`Route` instances. + self.error_handler = {} + + # Core plugins + self.plugins = [] # List of installed plugins. + if self.config['autojson']: + self.install(JSONPlugin()) + self.install(TemplatePlugin()) + + #: If true, most exceptions are caught and returned as :exc:`HTTPError` + catchall = DictProperty('config', 'catchall') + + __hook_names = 'before_request', 'after_request', 'app_reset', 'config' + __hook_reversed = 'after_request' + + @cached_property + def _hooks(self): + return dict((name, []) for name in self.__hook_names) + + def add_hook(self, name, func): + """ Attach a callback to a hook. Three hooks are currently implemented: + + before_request + Executed once before each request. The request context is + available, but no routing has happened yet. + after_request + Executed once after each request regardless of its outcome. + app_reset + Called whenever :meth:`Bottle.reset` is called. + """ + if name in self.__hook_reversed: + self._hooks[name].insert(0, func) + else: + self._hooks[name].append(func) + + def remove_hook(self, name, func): + """ Remove a callback from a hook. """ + if name in self._hooks and func in self._hooks[name]: + self._hooks[name].remove(func) + return True + + def trigger_hook(self, __name, *args, **kwargs): + """ Trigger a hook and return a list of results. """ + return [hook(*args, **kwargs) for hook in self._hooks[__name][:]] + + def hook(self, name): + """ Return a decorator that attaches a callback to a hook. See + :meth:`add_hook` for details.""" + + def decorator(func): + self.add_hook(name, func) + return func + + return decorator + + def mount(self, prefix, app, **options): + """ Mount an application (:class:`Bottle` or plain WSGI) to a specific + URL prefix. Example:: + + root_app.mount('/admin/', admin_app) + + :param prefix: path prefix or `mount-point`. If it ends in a slash, + that slash is mandatory. + :param app: an instance of :class:`Bottle` or a WSGI application. + + All other parameters are passed to the underlying :meth:`route` call. + """ + + segments = [p for p in prefix.split('/') if p] + if not segments: raise ValueError('Empty path prefix.') + path_depth = len(segments) + + def mountpoint_wrapper(): + try: + request.path_shift(path_depth) + rs = HTTPResponse([]) + + def start_response(status, headerlist, exc_info=None): + if exc_info: + _raise(*exc_info) + rs.status = status + for name, value in headerlist: + rs.add_header(name, value) + return rs.body.append + + body = app(request.environ, start_response) + rs.body = itertools.chain(rs.body, body) if rs.body else body + return rs + finally: + request.path_shift(-path_depth) + + options.setdefault('skip', True) + options.setdefault('method', 'PROXY') + options.setdefault('mountpoint', {'prefix': prefix, 'target': app}) + options['callback'] = mountpoint_wrapper + + self.route('/%s/<:re:.*>' % '/'.join(segments), **options) + if not prefix.endswith('/'): + self.route('/' + '/'.join(segments), **options) + + def merge(self, routes): + """ Merge the routes of another :class:`Bottle` application or a list of + :class:`Route` objects into this application. The routes keep their + 'owner', meaning that the :data:`Route.app` attribute is not + changed. """ + if isinstance(routes, Bottle): + routes = routes.routes + for route in routes: + self.add_route(route) + + def install(self, plugin): + """ Add a plugin to the list of plugins and prepare it for being + applied to all routes of this application. A plugin may be a simple + decorator or an object that implements the :class:`Plugin` API. + """ + if hasattr(plugin, 'setup'): plugin.setup(self) + if not callable(plugin) and not hasattr(plugin, 'apply'): + raise TypeError("Plugins must be callable or implement .apply()") + self.plugins.append(plugin) + self.reset() + return plugin + + def uninstall(self, plugin): + """ Uninstall plugins. Pass an instance to remove a specific plugin, a type + object to remove all plugins that match that type, a string to remove + all plugins with a matching ``name`` attribute or ``True`` to remove all + plugins. Return the list of removed plugins. """ + removed, remove = [], plugin + for i, plugin in list(enumerate(self.plugins))[::-1]: + if remove is True or remove is plugin or remove is type(plugin) \ + or getattr(plugin, 'name', True) == remove: + removed.append(plugin) + del self.plugins[i] + if hasattr(plugin, 'close'): plugin.close() + if removed: self.reset() + return removed + + def reset(self, route=None): + """ Reset all routes (force plugins to be re-applied) and clear all + caches. If an ID or route object is given, only that specific route + is affected. """ + if route is None: routes = self.routes + elif isinstance(route, Route): routes = [route] + else: routes = [self.routes[route]] + for route in routes: + route.reset() + if DEBUG: + for route in routes: + route.prepare() + self.trigger_hook('app_reset') + + def close(self): + """ Close the application and all installed plugins. """ + for plugin in self.plugins: + if hasattr(plugin, 'close'): plugin.close() + + def run(self, **kwargs): + """ Calls :func:`run` with the same parameters. """ + run(self, **kwargs) + + def match(self, environ): + """ Search for a matching route and return a (:class:`Route` , urlargs) + tuple. The second value is a dictionary with parameters extracted + from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" + return self.router.match(environ) + + def get_url(self, routename, **kargs): + """ Return a string that matches a named route """ + scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/' + location = self.router.build(routename, **kargs).lstrip('/') + return urljoin(urljoin('/', scriptname), location) + + def add_route(self, route): + """ Add a route object, but do not change the :data:`Route.app` + attribute.""" + self.routes.append(route) + self.router.add(route.rule, route.method, route, name=route.name) + if DEBUG: route.prepare() + + def route(self, + path=None, + method='GET', + callback=None, + name=None, + apply=None, + skip=None, **config): + """ A decorator to bind a function to a request URL. Example:: + + @app.route('/hello/') + def hello(name): + return 'Hello %s' % name + + The ```` part is a wildcard. See :class:`Router` for syntax + details. + + :param path: Request path or a list of paths to listen to. If no + path is specified, it is automatically generated from the + signature of the function. + :param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of + methods to listen to. (default: `GET`) + :param callback: An optional shortcut to avoid the decorator + syntax. ``route(..., callback=func)`` equals ``route(...)(func)`` + :param name: The name for this route. (default: None) + :param apply: A decorator or plugin or a list of plugins. These are + applied to the route callback in addition to installed plugins. + :param skip: A list of plugins, plugin classes or names. Matching + plugins are not installed to this route. ``True`` skips all. + + Any additional keyword arguments are stored as route-specific + configuration and passed to plugins (see :meth:`Plugin.apply`). + """ + if callable(path): path, callback = None, path + plugins = makelist(apply) + skiplist = makelist(skip) + + def decorator(callback): + if isinstance(callback, basestring): callback = load(callback) + for rule in makelist(path) or yieldroutes(callback): + for verb in makelist(method): + verb = verb.upper() + route = Route(self, rule, verb, callback, + name=name, + plugins=plugins, + skiplist=skiplist, **config) + self.add_route(route) + return callback + + return decorator(callback) if callback else decorator + + def get(self, path=None, method='GET', **options): + """ Equals :meth:`route`. """ + return self.route(path, method, **options) + + def post(self, path=None, method='POST', **options): + """ Equals :meth:`route` with a ``POST`` method parameter. """ + return self.route(path, method, **options) + + def put(self, path=None, method='PUT', **options): + """ Equals :meth:`route` with a ``PUT`` method parameter. """ + return self.route(path, method, **options) + + def delete(self, path=None, method='DELETE', **options): + """ Equals :meth:`route` with a ``DELETE`` method parameter. """ + return self.route(path, method, **options) + + def patch(self, path=None, method='PATCH', **options): + """ Equals :meth:`route` with a ``PATCH`` method parameter. """ + return self.route(path, method, **options) + + def error(self, code=500): + """ Decorator: Register an output handler for a HTTP error code""" + + def wrapper(handler): + self.error_handler[int(code)] = handler + return handler + + return wrapper + + def default_error_handler(self, res): + return tob(template(ERROR_PAGE_TEMPLATE, e=res)) + + def _handle(self, environ): + path = environ['bottle.raw_path'] = environ['PATH_INFO'] + if py3k: + environ['PATH_INFO'] = path.encode('latin1').decode('utf8', 'ignore') + + def _inner_handle(): + # Maybe pass variables as locals for better performance? + try: + route, args = self.router.match(environ) + environ['route.handle'] = route + environ['bottle.route'] = route + environ['route.url_args'] = args + return route.call(**args) + except HTTPResponse: + return _e() + except RouteReset: + route.reset() + return _inner_handle() + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception: + if not self.catchall: raise + stacktrace = format_exc() + environ['wsgi.errors'].write(stacktrace) + return HTTPError(500, "Internal Server Error", _e(), stacktrace) + + try: + out = None + environ['bottle.app'] = self + request.bind(environ) + response.bind() + self.trigger_hook('before_request') + out = _inner_handle() + return out; + finally: + if isinstance(out, HTTPResponse): + out.apply(response) + self.trigger_hook('after_request') + + def _cast(self, out, peek=None): + """ Try to convert the parameter into something WSGI compatible and set + correct HTTP headers when possible. + Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like, + iterable of strings and iterable of unicodes + """ + + # Empty output is done here + if not out: + if 'Content-Length' not in response: + response['Content-Length'] = 0 + return [] + # Join lists of byte or unicode strings. Mixed lists are NOT supported + if isinstance(out, (tuple, list))\ + and isinstance(out[0], (bytes, unicode)): + out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' + # Encode unicode strings + if isinstance(out, unicode): + out = out.encode(response.charset) + # Byte Strings are just returned + if isinstance(out, bytes): + if 'Content-Length' not in response: + response['Content-Length'] = len(out) + return [out] + # HTTPError or HTTPException (recursive, because they may wrap anything) + # TODO: Handle these explicitly in handle() or make them iterable. + if isinstance(out, HTTPError): + out.apply(response) + out = self.error_handler.get(out.status_code, + self.default_error_handler)(out) + return self._cast(out) + if isinstance(out, HTTPResponse): + out.apply(response) + return self._cast(out.body) + + # File-like objects. + if hasattr(out, 'read'): + if 'wsgi.file_wrapper' in request.environ: + return request.environ['wsgi.file_wrapper'](out) + elif hasattr(out, 'close') or not hasattr(out, '__iter__'): + return WSGIFileWrapper(out) + + # Handle Iterables. We peek into them to detect their inner type. + try: + iout = iter(out) + first = next(iout) + while not first: + first = next(iout) + except StopIteration: + return self._cast('') + except HTTPResponse: + first = _e() + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except: + if not self.catchall: raise + first = HTTPError(500, 'Unhandled exception', _e(), format_exc()) + + # These are the inner types allowed in iterator or generator objects. + if isinstance(first, HTTPResponse): + return self._cast(first) + elif isinstance(first, bytes): + new_iter = itertools.chain([first], iout) + elif isinstance(first, unicode): + encoder = lambda x: x.encode(response.charset) + new_iter = imap(encoder, itertools.chain([first], iout)) + else: + msg = 'Unsupported response type: %s' % type(first) + return self._cast(HTTPError(500, msg)) + if hasattr(out, 'close'): + new_iter = _closeiter(new_iter, out.close) + return new_iter + + def wsgi(self, environ, start_response): + """ The bottle WSGI-interface. """ + try: + out = self._cast(self._handle(environ)) + # rfc2616 section 4.3 + if response._status_code in (100, 101, 204, 304)\ + or environ['REQUEST_METHOD'] == 'HEAD': + if hasattr(out, 'close'): out.close() + out = [] + start_response(response._status_line, response.headerlist) + return out + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except: + if not self.catchall: raise + err = '

Critical error while processing request: %s

' \ + % html_escape(environ.get('PATH_INFO', '/')) + if DEBUG: + err += '

Error:

\n
\n%s\n
\n' \ + '

Traceback:

\n
\n%s\n
\n' \ + % (html_escape(repr(_e())), html_escape(format_exc())) + environ['wsgi.errors'].write(err) + headers = [('Content-Type', 'text/html; charset=UTF-8')] + start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info()) + return [tob(err)] + + def __call__(self, environ, start_response): + """ Each instance of :class:'Bottle' is a WSGI application. """ + return self.wsgi(environ, start_response) + + def __enter__(self): + """ Use this application as default for all module-level shortcuts. """ + default_app.push(self) + return self + + def __exit__(self, exc_type, exc_value, traceback): + default_app.pop() + + def __setattr__(self, name, value): + if name in self.__dict__: + raise AttributeError("Attribute %s already defined. Plugin conflict?" % name) + self.__dict__[name] = value + + +############################################################################### +# HTTP and WSGI Tools ########################################################## +############################################################################### + + +class BaseRequest(object): + """ A wrapper for WSGI environment dictionaries that adds a lot of + convenient access methods and properties. Most of them are read-only. + + Adding new attributes to a request actually adds them to the environ + dictionary (as 'bottle.request.ext.'). This is the recommended + way to store and access request-specific data. + """ + + __slots__ = ('environ', ) + + #: Maximum size of memory buffer for :attr:`body` in bytes. + MEMFILE_MAX = 102400 + + def __init__(self, environ=None): + """ Wrap a WSGI environ dictionary. """ + #: The wrapped WSGI environ dictionary. This is the only real attribute. + #: All other attributes actually are read-only properties. + self.environ = {} if environ is None else environ + self.environ['bottle.request'] = self + + @DictProperty('environ', 'bottle.app', read_only=True) + def app(self): + """ Bottle application handling this request. """ + raise RuntimeError('This request is not connected to an application.') + + @DictProperty('environ', 'bottle.route', read_only=True) + def route(self): + """ The bottle :class:`Route` object that matches this request. """ + raise RuntimeError('This request is not connected to a route.') + + @DictProperty('environ', 'route.url_args', read_only=True) + def url_args(self): + """ The arguments extracted from the URL. """ + raise RuntimeError('This request is not connected to a route.') + + @property + def path(self): + """ The value of ``PATH_INFO`` with exactly one prefixed slash (to fix + broken clients and avoid the "empty path" edge case). """ + return '/' + self.environ.get('PATH_INFO', '').lstrip('/') + + @property + def method(self): + """ The ``REQUEST_METHOD`` value as an uppercase string. """ + return self.environ.get('REQUEST_METHOD', 'GET').upper() + + @DictProperty('environ', 'bottle.request.headers', read_only=True) + def headers(self): + """ A :class:`WSGIHeaderDict` that provides case-insensitive access to + HTTP request headers. """ + return WSGIHeaderDict(self.environ) + + def get_header(self, name, default=None): + """ Return the value of a request header, or a given default value. """ + return self.headers.get(name, default) + + @DictProperty('environ', 'bottle.request.cookies', read_only=True) + def cookies(self): + """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT + decoded. Use :meth:`get_cookie` if you expect signed cookies. """ + cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values() + return FormsDict((c.key, c.value) for c in cookies) + + def get_cookie(self, key, default=None, secret=None): + """ Return the content of a cookie. To read a `Signed Cookie`, the + `secret` must match the one used to create the cookie (see + :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing + cookie or wrong signature), return a default value. """ + value = self.cookies.get(key) + if secret and value: + dec = cookie_decode(value, secret) # (key, value) tuple or None + return dec[1] if dec and dec[0] == key else default + return value or default + + @DictProperty('environ', 'bottle.request.query', read_only=True) + def query(self): + """ The :attr:`query_string` parsed into a :class:`FormsDict`. These + values are sometimes called "URL arguments" or "GET parameters", but + not to be confused with "URL wildcards" as they are provided by the + :class:`Router`. """ + get = self.environ['bottle.get'] = FormsDict() + pairs = _parse_qsl(self.environ.get('QUERY_STRING', '')) + for key, value in pairs: + get[key] = value + return get + + @DictProperty('environ', 'bottle.request.forms', read_only=True) + def forms(self): + """ Form values parsed from an `url-encoded` or `multipart/form-data` + encoded POST or PUT request body. The result is returned as a + :class:`FormsDict`. All keys and values are strings. File uploads + are stored separately in :attr:`files`. """ + forms = FormsDict() + for name, item in self.POST.allitems(): + if not isinstance(item, FileUpload): + forms[name] = item + return forms + + @DictProperty('environ', 'bottle.request.params', read_only=True) + def params(self): + """ A :class:`FormsDict` with the combined values of :attr:`query` and + :attr:`forms`. File uploads are stored in :attr:`files`. """ + params = FormsDict() + for key, value in self.query.allitems(): + params[key] = value + for key, value in self.forms.allitems(): + params[key] = value + return params + + @DictProperty('environ', 'bottle.request.files', read_only=True) + def files(self): + """ File uploads parsed from `multipart/form-data` encoded POST or PUT + request body. The values are instances of :class:`FileUpload`. + + """ + files = FormsDict() + for name, item in self.POST.allitems(): + if isinstance(item, FileUpload): + files[name] = item + return files + + @DictProperty('environ', 'bottle.request.json', read_only=True) + def json(self): + """ If the ``Content-Type`` header is ``application/json``, this + property holds the parsed content of the request body. Only requests + smaller than :attr:`MEMFILE_MAX` are processed to avoid memory + exhaustion. Invalid JSON raises a 400 error response. """ + ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0] + if ctype == 'application/json': + b = self._get_body_string() + if not b: + return None + try: + return json_loads(b) + except (ValueError, TypeError): + raise HTTPError(400, 'Invalid JSON') + return None + + def _iter_body(self, read, bufsize): + maxread = max(0, self.content_length) + while maxread: + part = read(min(maxread, bufsize)) + if not part: break + yield part + maxread -= len(part) + + @staticmethod + def _iter_chunked(read, bufsize): + err = HTTPError(400, 'Error while parsing chunked transfer body.') + rn, sem, bs = tob('\r\n'), tob(';'), tob('') + while True: + header = read(1) + while header[-2:] != rn: + c = read(1) + header += c + if not c: raise err + if len(header) > bufsize: raise err + size, _, _ = header.partition(sem) + try: + maxread = int(tonat(size.strip()), 16) + except ValueError: + raise err + if maxread == 0: break + buff = bs + while maxread > 0: + if not buff: + buff = read(min(maxread, bufsize)) + part, buff = buff[:maxread], buff[maxread:] + if not part: raise err + yield part + maxread -= len(part) + if read(2) != rn: + raise err + + @DictProperty('environ', 'bottle.request.body', read_only=True) + def _body(self): + try: + read_func = self.environ['wsgi.input'].read + except KeyError: + self.environ['wsgi.input'] = BytesIO() + return self.environ['wsgi.input'] + body_iter = self._iter_chunked if self.chunked else self._iter_body + body, body_size, is_temp_file = BytesIO(), 0, False + for part in body_iter(read_func, self.MEMFILE_MAX): + body.write(part) + body_size += len(part) + if not is_temp_file and body_size > self.MEMFILE_MAX: + body, tmp = TemporaryFile(mode='w+b'), body + body.write(tmp.getvalue()) + del tmp + is_temp_file = True + self.environ['wsgi.input'] = body + body.seek(0) + return body + + def _get_body_string(self): + """ read body until content-length or MEMFILE_MAX into a string. Raise + HTTPError(413) on requests that are to large. """ + clen = self.content_length + if clen > self.MEMFILE_MAX: + raise HTTPError(413, 'Request entity too large') + if clen < 0: clen = self.MEMFILE_MAX + 1 + data = self.body.read(clen) + if len(data) > self.MEMFILE_MAX: # Fail fast + raise HTTPError(413, 'Request entity too large') + return data + + @property + def body(self): + """ The HTTP request body as a seek-able file-like object. Depending on + :attr:`MEMFILE_MAX`, this is either a temporary file or a + :class:`io.BytesIO` instance. Accessing this property for the first + time reads and replaces the ``wsgi.input`` environ variable. + Subsequent accesses just do a `seek(0)` on the file object. """ + self._body.seek(0) + return self._body + + @property + def chunked(self): + """ True if Chunked transfer encoding was. """ + return 'chunked' in self.environ.get( + 'HTTP_TRANSFER_ENCODING', '').lower() + + #: An alias for :attr:`query`. + GET = query + + @DictProperty('environ', 'bottle.request.post', read_only=True) + def POST(self): + """ The values of :attr:`forms` and :attr:`files` combined into a single + :class:`FormsDict`. Values are either strings (form values) or + instances of :class:`cgi.FieldStorage` (file uploads). + """ + post = FormsDict() + # We default to application/x-www-form-urlencoded for everything that + # is not multipart and take the fast path (also: 3.1 workaround) + if not self.content_type.startswith('multipart/'): + pairs = _parse_qsl(tonat(self._get_body_string(), 'latin1')) + for key, value in pairs: + post[key] = value + return post + + safe_env = {'QUERY_STRING': ''} # Build a safe environment for cgi + for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): + if key in self.environ: safe_env[key] = self.environ[key] + args = dict(fp=self.body, environ=safe_env, keep_blank_values=True) + if py31: + args['fp'] = NCTextIOWrapper(args['fp'], + encoding='utf8', + newline='\n') + elif py3k: + args['encoding'] = 'utf8' + data = cgi.FieldStorage(**args) + self['_cgi.FieldStorage'] = data #http://bugs.python.org/issue18394 + data = data.list or [] + for item in data: + if item.filename: + post[item.name] = FileUpload(item.file, item.name, + item.filename, item.headers) + else: + post[item.name] = item.value + return post + + @property + def url(self): + """ The full request URI including hostname and scheme. If your app + lives behind a reverse proxy or load balancer and you get confusing + results, make sure that the ``X-Forwarded-Host`` header is set + correctly. """ + return self.urlparts.geturl() + + @DictProperty('environ', 'bottle.request.urlparts', read_only=True) + def urlparts(self): + """ The :attr:`url` string as an :class:`urlparse.SplitResult` tuple. + The tuple contains (scheme, host, path, query_string and fragment), + but the fragment is always empty because it is not visible to the + server. """ + env = self.environ + http = env.get('HTTP_X_FORWARDED_PROTO') \ + or env.get('wsgi.url_scheme', 'http') + host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') + if not host: + # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. + host = env.get('SERVER_NAME', '127.0.0.1') + port = env.get('SERVER_PORT') + if port and port != ('80' if http == 'http' else '443'): + host += ':' + port + path = urlquote(self.fullpath) + return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '') + + @property + def fullpath(self): + """ Request path including :attr:`script_name` (if present). """ + return urljoin(self.script_name, self.path.lstrip('/')) + + @property + def query_string(self): + """ The raw :attr:`query` part of the URL (everything in between ``?`` + and ``#``) as a string. """ + return self.environ.get('QUERY_STRING', '') + + @property + def script_name(self): + """ The initial portion of the URL's `path` that was removed by a higher + level (server or routing middleware) before the application was + called. This script path is returned with leading and tailing + slashes. """ + script_name = self.environ.get('SCRIPT_NAME', '').strip('/') + return '/' + script_name + '/' if script_name else '/' + + def path_shift(self, shift=1): + """ Shift path segments from :attr:`path` to :attr:`script_name` and + vice versa. + + :param shift: The number of path segments to shift. May be negative + to change the shift direction. (default: 1) + """ + script, path = path_shift(self.environ.get('SCRIPT_NAME', '/'), self.path, shift) + self['SCRIPT_NAME'], self['PATH_INFO'] = script, path + + @property + def content_length(self): + """ The request body length as an integer. The client is responsible to + set this header. Otherwise, the real length of the body is unknown + and -1 is returned. In this case, :attr:`body` will be empty. """ + return int(self.environ.get('CONTENT_LENGTH') or -1) + + @property + def content_type(self): + """ The Content-Type header as a lowercase-string (default: empty). """ + return self.environ.get('CONTENT_TYPE', '').lower() + + @property + def is_xhr(self): + """ True if the request was triggered by a XMLHttpRequest. This only + works with JavaScript libraries that support the `X-Requested-With` + header (most of the popular libraries do). """ + requested_with = self.environ.get('HTTP_X_REQUESTED_WITH', '') + return requested_with.lower() == 'xmlhttprequest' + + @property + def is_ajax(self): + """ Alias for :attr:`is_xhr`. "Ajax" is not the right term. """ + return self.is_xhr + + @property + def auth(self): + """ HTTP authentication data as a (user, password) tuple. This + implementation currently supports basic (not digest) authentication + only. If the authentication happened at a higher level (e.g. in the + front web-server or a middleware), the password field is None, but + the user field is looked up from the ``REMOTE_USER`` environ + variable. On any errors, None is returned. """ + basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION', '')) + if basic: return basic + ruser = self.environ.get('REMOTE_USER') + if ruser: return (ruser, None) + return None + + @property + def remote_route(self): + """ A list of all IPs that were involved in this request, starting with + the client IP and followed by zero or more proxies. This does only + work if all proxies support the ```X-Forwarded-For`` header. Note + that this information can be forged by malicious clients. """ + proxy = self.environ.get('HTTP_X_FORWARDED_FOR') + if proxy: return [ip.strip() for ip in proxy.split(',')] + remote = self.environ.get('REMOTE_ADDR') + return [remote] if remote else [] + + @property + def remote_addr(self): + """ The client IP as a string. Note that this information can be forged + by malicious clients. """ + route = self.remote_route + return route[0] if route else None + + def copy(self): + """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """ + return Request(self.environ.copy()) + + def get(self, value, default=None): + return self.environ.get(value, default) + + def __getitem__(self, key): + return self.environ[key] + + def __delitem__(self, key): + self[key] = "" + del (self.environ[key]) + + def __iter__(self): + return iter(self.environ) + + def __len__(self): + return len(self.environ) + + def keys(self): + return self.environ.keys() + + def __setitem__(self, key, value): + """ Change an environ value and clear all caches that depend on it. """ + + if self.environ.get('bottle.request.readonly'): + raise KeyError('The environ dictionary is read-only.') + + self.environ[key] = value + todelete = () + + if key == 'wsgi.input': + todelete = ('body', 'forms', 'files', 'params', 'post', 'json') + elif key == 'QUERY_STRING': + todelete = ('query', 'params') + elif key.startswith('HTTP_'): + todelete = ('headers', 'cookies') + + for key in todelete: + self.environ.pop('bottle.request.' + key, None) + + def __repr__(self): + return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url) + + def __getattr__(self, name): + """ Search in self.environ for additional user defined attributes. """ + try: + var = self.environ['bottle.request.ext.%s' % name] + return var.__get__(self) if hasattr(var, '__get__') else var + except KeyError: + raise AttributeError('Attribute %r not defined.' % name) + + def __setattr__(self, name, value): + if name == 'environ': return object.__setattr__(self, name, value) + key = 'bottle.request.ext.%s' % name + if key in self.environ: + raise AttributeError("Attribute already defined: %s" % name) + self.environ[key] = value + + def __delattr__(self, name, value): + try: + del self.environ['bottle.request.ext.%s' % name] + except KeyError: + raise AttributeError("Attribute not defined: %s" % name) + +def _hkey(s): + return s.title().replace('_', '-') + + +class HeaderProperty(object): + def __init__(self, name, reader=None, writer=str, default=''): + self.name, self.default = name, default + self.reader, self.writer = reader, writer + self.__doc__ = 'Current value of the %r header.' % name.title() + + def __get__(self, obj, _): + if obj is None: return self + value = obj.headers.get(self.name, self.default) + return self.reader(value) if self.reader else value + + def __set__(self, obj, value): + obj.headers[self.name] = self.writer(value) + + def __delete__(self, obj): + del obj.headers[self.name] + + +class BaseResponse(object): + """ Storage class for a response body as well as headers and cookies. + + This class does support dict-like case-insensitive item-access to + headers, but is NOT a dict. Most notably, iterating over a response + yields parts of the body and not the headers. + + :param body: The response body as one of the supported types. + :param status: Either an HTTP status code (e.g. 200) or a status line + including the reason phrase (e.g. '200 OK'). + :param headers: A dictionary or a list of name-value pairs. + + Additional keyword arguments are added to the list of headers. + Underscores in the header name are replaced with dashes. + """ + + default_status = 200 + default_content_type = 'text/html; charset=UTF-8' + + # Header blacklist for specific response codes + # (rfc2616 section 10.2.3 and 10.3.5) + bad_headers = { + 204: set(('Content-Type', )), + 304: set(('Allow', 'Content-Encoding', 'Content-Language', + 'Content-Length', 'Content-Range', 'Content-Type', + 'Content-Md5', 'Last-Modified')) + } + + def __init__(self, body='', status=None, headers=None, **more_headers): + self._cookies = None + self._headers = {} + self.body = body + self.status = status or self.default_status + if headers: + if isinstance(headers, dict): + headers = headers.items() + for name, value in headers: + self.add_header(name, value) + if more_headers: + for name, value in more_headers.items(): + self.add_header(name, value) + + def copy(self, cls=None): + """ Returns a copy of self. """ + cls = cls or BaseResponse + assert issubclass(cls, BaseResponse) + copy = cls() + copy.status = self.status + copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) + if self._cookies: + copy._cookies = SimpleCookie() + copy._cookies.load(self._cookies.output(header='')) + return copy + + def __iter__(self): + return iter(self.body) + + def close(self): + if hasattr(self.body, 'close'): + self.body.close() + + @property + def status_line(self): + """ The HTTP status line as a string (e.g. ``404 Not Found``).""" + return self._status_line + + @property + def status_code(self): + """ The HTTP status code as an integer (e.g. 404).""" + return self._status_code + + def _set_status(self, status): + if isinstance(status, int): + code, status = status, _HTTP_STATUS_LINES.get(status) + elif ' ' in status: + status = status.strip() + code = int(status.split()[0]) + else: + raise ValueError('String status line without a reason phrase.') + if not 100 <= code <= 999: + raise ValueError('Status code out of range.') + self._status_code = code + self._status_line = str(status or ('%d Unknown' % code)) + + def _get_status(self): + return self._status_line + + status = property( + _get_status, _set_status, None, + ''' A writeable property to change the HTTP response status. It accepts + either a numeric code (100-999) or a string with a custom reason + phrase (e.g. "404 Brain not found"). Both :data:`status_line` and + :data:`status_code` are updated accordingly. The return value is + always a status string. ''') + del _get_status, _set_status + + @property + def headers(self): + """ An instance of :class:`HeaderDict`, a case-insensitive dict-like + view on the response headers. """ + hdict = HeaderDict() + hdict.dict = self._headers + return hdict + + def __contains__(self, name): + return _hkey(name) in self._headers + + def __delitem__(self, name): + del self._headers[_hkey(name)] + + def __getitem__(self, name): + return self._headers[_hkey(name)][-1] + + def __setitem__(self, name, value): + self._headers[_hkey(name)] = [value if isinstance(value, unicode) else + str(value)] + + def get_header(self, name, default=None): + """ Return the value of a previously defined header. If there is no + header with that name, return a default value. """ + return self._headers.get(_hkey(name), [default])[-1] + + def set_header(self, name, value): + """ Create a new response header, replacing any previously defined + headers with the same name. """ + self._headers[_hkey(name)] = [value if isinstance(value, unicode) + else str(value)] + + def add_header(self, name, value): + """ Add an additional response header, not removing duplicates. """ + self._headers.setdefault(_hkey(name), []).append( + value if isinstance(value, unicode) else str(value)) + + def iter_headers(self): + """ Yield (header, value) tuples, skipping headers that are not + allowed with the current response status code. """ + return self.headerlist + + @property + def headerlist(self): + """ WSGI conform list of (header, value) tuples. """ + out = [] + headers = list(self._headers.items()) + if 'Content-Type' not in self._headers: + headers.append(('Content-Type', [self.default_content_type])) + if self._status_code in self.bad_headers: + bad_headers = self.bad_headers[self._status_code] + headers = [h for h in headers if h[0] not in bad_headers] + out += [(name, val) for (name, vals) in headers for val in vals] + if self._cookies: + for c in self._cookies.values(): + out.append(('Set-Cookie', c.OutputString())) + if py3k: + return [(k, v.encode('utf8').decode('latin1')) for (k, v) in out] + else: + return [(k, v.encode('utf8') if isinstance(v, unicode) else v) + for (k, v) in out] + + content_type = HeaderProperty('Content-Type') + content_length = HeaderProperty('Content-Length', reader=int) + expires = HeaderProperty( + 'Expires', + reader=lambda x: datetime.utcfromtimestamp(parse_date(x)), + writer=lambda x: http_date(x)) + + @property + def charset(self, default='UTF-8'): + """ Return the charset specified in the content-type header (default: utf8). """ + if 'charset=' in self.content_type: + return self.content_type.split('charset=')[-1].split(';')[0].strip() + return default + + def set_cookie(self, name, value, secret=None, **options): + """ Create a new cookie or replace an old one. If the `secret` parameter is + set, create a `Signed Cookie` (described below). + + :param name: the name of the cookie. + :param value: the value of the cookie. + :param secret: a signature key required for signed cookies. + + Additionally, this method accepts all RFC 2109 attributes that are + supported by :class:`cookie.Morsel`, including: + + :param max_age: maximum age in seconds. (default: None) + :param expires: a datetime object or UNIX timestamp. (default: None) + :param domain: the domain that is allowed to read the cookie. + (default: current domain) + :param path: limits the cookie to a given path (default: current path) + :param secure: limit the cookie to HTTPS connections (default: off). + :param httponly: prevents client-side javascript to read this cookie + (default: off, requires Python 2.6 or newer). + + If neither `expires` nor `max_age` is set (default), the cookie will + expire at the end of the browser session (as soon as the browser + window is closed). + + Signed cookies may store any pickle-able object and are + cryptographically signed to prevent manipulation. Keep in mind that + cookies are limited to 4kb in most browsers. + + Warning: Signed cookies are not encrypted (the client can still see + the content) and not copy-protected (the client can restore an old + cookie). The main intention is to make pickling and unpickling + save, not to store secret information at client side. + """ + if not self._cookies: + self._cookies = SimpleCookie() + + if secret: + value = touni(cookie_encode((name, value), secret)) + elif not isinstance(value, basestring): + raise TypeError('Secret key missing for non-string Cookie.') + + # Cookie size plus options must not exceed 4kb. + if len(name) + len(value) > 3800: + raise ValueError('Content does not fit into a cookie.') + + self._cookies[name] = value + + for key, value in options.items(): + if key == 'max_age': + if isinstance(value, timedelta): + value = value.seconds + value.days * 24 * 3600 + if key == 'expires': + if isinstance(value, (datedate, datetime)): + value = value.timetuple() + elif isinstance(value, (int, float)): + value = time.gmtime(value) + value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) + if key in ('secure', 'httponly') and not value: + continue + self._cookies[name][key.replace('_', '-')] = value + + def delete_cookie(self, key, **kwargs): + """ Delete a cookie. Be sure to use the same `domain` and `path` + settings as used to create the cookie. """ + kwargs['max_age'] = -1 + kwargs['expires'] = 0 + self.set_cookie(key, '', **kwargs) + + def __repr__(self): + out = '' + for name, value in self.headerlist: + out += '%s: %s\n' % (name.title(), value.strip()) + return out + + +def _local_property(): + ls = threading.local() + + def fget(_): + try: + return ls.var + except AttributeError: + raise RuntimeError("Request context not initialized.") + + def fset(_, value): + ls.var = value + + def fdel(_): + del ls.var + + return property(fget, fset, fdel, 'Thread-local property') + + +class LocalRequest(BaseRequest): + """ A thread-local subclass of :class:`BaseRequest` with a different + set of attributes for each thread. There is usually only one global + instance of this class (:data:`request`). If accessed during a + request/response cycle, this instance always refers to the *current* + request (even on a multithreaded server). """ + bind = BaseRequest.__init__ + environ = _local_property() + + +class LocalResponse(BaseResponse): + """ A thread-local subclass of :class:`BaseResponse` with a different + set of attributes for each thread. There is usually only one global + instance of this class (:data:`response`). Its attributes are used + to build the HTTP response at the end of the request/response cycle. + """ + bind = BaseResponse.__init__ + _status_line = _local_property() + _status_code = _local_property() + _cookies = _local_property() + _headers = _local_property() + body = _local_property() + + +Request = BaseRequest +Response = BaseResponse + + +class HTTPResponse(Response, BottleException): + def __init__(self, body='', status=None, headers=None, **more_headers): + super(HTTPResponse, self).__init__(body, status, headers, **more_headers) + + def apply(self, other): + other._status_code = self._status_code + other._status_line = self._status_line + other._headers = self._headers + other._cookies = self._cookies + other.body = self.body + + +class HTTPError(HTTPResponse): + default_status = 500 + + def __init__(self, + status=None, + body=None, + exception=None, + traceback=None, **more_headers): + self.exception = exception + self.traceback = traceback + super(HTTPError, self).__init__(body, status, **more_headers) + +############################################################################### +# Plugins ###################################################################### +############################################################################### + + +class PluginError(BottleException): + pass + + +class JSONPlugin(object): + name = 'json' + api = 2 + + def __init__(self, json_dumps=json_dumps): + self.json_dumps = json_dumps + + def apply(self, callback, _): + dumps = self.json_dumps + if not dumps: return callback + + def wrapper(*a, **ka): + try: + rv = callback(*a, **ka) + except HTTPError: + rv = _e() + + if isinstance(rv, dict): + #Attempt to serialize, raises exception on failure + json_response = dumps(rv) + #Set content type only if serialization successful + response.content_type = 'application/json' + return json_response + elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict): + rv.body = dumps(rv.body) + rv.content_type = 'application/json' + return rv + + return wrapper + + +class TemplatePlugin(object): + """ This plugin applies the :func:`view` decorator to all routes with a + `template` config parameter. If the parameter is a tuple, the second + element must be a dict with additional options (e.g. `template_engine`) + or default variables for the template. """ + name = 'template' + api = 2 + + def setup(self, app): + app.tpl = self + + def apply(self, callback, route): + conf = route.config.get('template') + if isinstance(conf, (tuple, list)) and len(conf) == 2: + return view(conf[0], **conf[1])(callback) + elif isinstance(conf, str): + return view(conf)(callback) + else: + return callback + + +#: Not a plugin, but part of the plugin API. TODO: Find a better place. +class _ImportRedirect(object): + def __init__(self, name, impmask): + """ Create a virtual package that redirects imports (see PEP 302). """ + self.name = name + self.impmask = impmask + self.module = sys.modules.setdefault(name, imp.new_module(name)) + self.module.__dict__.update({ + '__file__': __file__, + '__path__': [], + '__all__': [], + '__loader__': self + }) + sys.meta_path.append(self) + + def find_module(self, fullname, path=None): + if '.' not in fullname: return + packname = fullname.rsplit('.', 1)[0] + if packname != self.name: return + return self + + def load_module(self, fullname): + if fullname in sys.modules: return sys.modules[fullname] + modname = fullname.rsplit('.', 1)[1] + realname = self.impmask % modname + __import__(realname) + module = sys.modules[fullname] = sys.modules[realname] + setattr(self.module, modname, module) + module.__loader__ = self + return module + +############################################################################### +# Common Utilities ############################################################# +############################################################################### + + +class MultiDict(DictMixin): + """ This dict stores multiple values per key, but behaves exactly like a + normal dict in that it returns only the newest value for any given key. + There are special methods available to access the full list of values. + """ + + def __init__(self, *a, **k): + self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items()) + + def __len__(self): + return len(self.dict) + + def __iter__(self): + return iter(self.dict) + + def __contains__(self, key): + return key in self.dict + + def __delitem__(self, key): + del self.dict[key] + + def __getitem__(self, key): + return self.dict[key][-1] + + def __setitem__(self, key, value): + self.append(key, value) + + def keys(self): + return self.dict.keys() + + if py3k: + + def values(self): + return (v[-1] for v in self.dict.values()) + + def items(self): + return ((k, v[-1]) for k, v in self.dict.items()) + + def allitems(self): + return ((k, v) for k, vl in self.dict.items() for v in vl) + + iterkeys = keys + itervalues = values + iteritems = items + iterallitems = allitems + + else: + + def values(self): + return [v[-1] for v in self.dict.values()] + + def items(self): + return [(k, v[-1]) for k, v in self.dict.items()] + + def iterkeys(self): + return self.dict.iterkeys() + + def itervalues(self): + return (v[-1] for v in self.dict.itervalues()) + + def iteritems(self): + return ((k, v[-1]) for k, v in self.dict.iteritems()) + + def iterallitems(self): + return ((k, v) for k, vl in self.dict.iteritems() for v in vl) + + def allitems(self): + return [(k, v) for k, vl in self.dict.iteritems() for v in vl] + + def get(self, key, default=None, index=-1, type=None): + """ Return the most recent value for a key. + + :param default: The default value to be returned if the key is not + present or the type conversion fails. + :param index: An index for the list of available values. + :param type: If defined, this callable is used to cast the value + into a specific type. Exception are suppressed and result in + the default value to be returned. + """ + try: + val = self.dict[key][index] + return type(val) if type else val + except Exception: + pass + return default + + def append(self, key, value): + """ Add a new value to the list of values for this key. """ + self.dict.setdefault(key, []).append(value) + + def replace(self, key, value): + """ Replace the list of values with a single value. """ + self.dict[key] = [value] + + def getall(self, key): + """ Return a (possibly empty) list of values for a key. """ + return self.dict.get(key) or [] + + #: Aliases for WTForms to mimic other multi-dict APIs (Django) + getone = get + getlist = getall + + +class FormsDict(MultiDict): + """ This :class:`MultiDict` subclass is used to store request form data. + Additionally to the normal dict-like item access methods (which return + unmodified data as native strings), this container also supports + attribute-like access to its values. Attributes are automatically de- + or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing + attributes default to an empty string. """ + + #: Encoding used for attribute values. + input_encoding = 'utf8' + #: If true (default), unicode strings are first encoded with `latin1` + #: and then decoded to match :attr:`input_encoding`. + recode_unicode = True + + def _fix(self, s, encoding=None): + if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI + return s.encode('latin1').decode(encoding or self.input_encoding) + elif isinstance(s, bytes): # Python 2 WSGI + return s.decode(encoding or self.input_encoding) + else: + return s + + def decode(self, encoding=None): + """ Returns a copy with all keys and values de- or recoded to match + :attr:`input_encoding`. Some libraries (e.g. WTForms) want a + unicode dictionary. """ + copy = FormsDict() + enc = copy.input_encoding = encoding or self.input_encoding + copy.recode_unicode = False + for key, value in self.allitems(): + copy.append(self._fix(key, enc), self._fix(value, enc)) + return copy + + def getunicode(self, name, default=None, encoding=None): + """ Return the value as a unicode string, or the default. """ + try: + return self._fix(self[name], encoding) + except (UnicodeError, KeyError): + return default + + def __getattr__(self, name, default=unicode()): + # Without this guard, pickle generates a cryptic TypeError: + if name.startswith('__') and name.endswith('__'): + return super(FormsDict, self).__getattr__(name) + return self.getunicode(name, default=default) + + +class HeaderDict(MultiDict): + """ A case-insensitive version of :class:`MultiDict` that defaults to + replace the old value instead of appending it. """ + + def __init__(self, *a, **ka): + self.dict = {} + if a or ka: self.update(*a, **ka) + + def __contains__(self, key): + return _hkey(key) in self.dict + + def __delitem__(self, key): + del self.dict[_hkey(key)] + + def __getitem__(self, key): + return self.dict[_hkey(key)][-1] + + def __setitem__(self, key, value): + self.dict[_hkey(key)] = [value if isinstance(value, unicode) else + str(value)] + + def append(self, key, value): + self.dict.setdefault(_hkey(key), []).append( + value if isinstance(value, unicode) else str(value)) + + def replace(self, key, value): + self.dict[_hkey(key)] = [value if isinstance(value, unicode) else + str(value)] + + def getall(self, key): + return self.dict.get(_hkey(key)) or [] + + def get(self, key, default=None, index=-1): + return MultiDict.get(self, _hkey(key), default, index) + + def filter(self, names): + for name in [_hkey(n) for n in names]: + if name in self.dict: + del self.dict[name] + + +class WSGIHeaderDict(DictMixin): + """ This dict-like class wraps a WSGI environ dict and provides convenient + access to HTTP_* fields. Keys and values are native strings + (2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI + environment contains non-native string values, these are de- or encoded + using a lossless 'latin1' character set. + + The API will remain stable even on changes to the relevant PEPs. + Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one + that uses non-native strings.) + """ + #: List of keys that do not have a ``HTTP_`` prefix. + cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH') + + def __init__(self, environ): + self.environ = environ + + def _ekey(self, key): + """ Translate header field name to CGI/WSGI environ key. """ + key = key.replace('-', '_').upper() + if key in self.cgikeys: + return key + return 'HTTP_' + key + + def raw(self, key, default=None): + """ Return the header value as is (may be bytes or unicode). """ + return self.environ.get(self._ekey(key), default) + + def __getitem__(self, key): + val = self.environ[self._ekey(key)] + if py3k: + if isinstance(val, unicode): + val = val.encode('latin1').decode('utf8') + else: + val = val.decode('utf8') + return val + + def __setitem__(self, key, value): + raise TypeError("%s is read-only." % self.__class__) + + def __delitem__(self, key): + raise TypeError("%s is read-only." % self.__class__) + + def __iter__(self): + for key in self.environ: + if key[:5] == 'HTTP_': + yield _hkey(key[5:]) + elif key in self.cgikeys: + yield _hkey(key) + + def keys(self): + return [x for x in self] + + def __len__(self): + return len(self.keys()) + + def __contains__(self, key): + return self._ekey(key) in self.environ + + +class ConfigDict(dict): + """ A dict-like configuration storage with additional support for + namespaces, validators, meta-data, on_change listeners and more. + """ + + __slots__ = ('_meta', '_on_change') + + def __init__(self): + self._meta = {} + self._on_change = lambda name, value: None + + def load_module(self, path, squash): + """ Load values from a Python module. + :param squash: Squash nested dicts into namespaces by using + load_dict(), otherwise use update() + Example: load_config('my.app.settings', True) + Example: load_config('my.app.settings', False) + """ + config_obj = __import__(path) + obj = dict([(key, getattr(config_obj, key)) + for key in dir(config_obj) if key.isupper()]) + if squash: + self.load_dict(obj) + else: + self.update(obj) + return self + + def load_config(self, filename): + """ Load values from an ``*.ini`` style config file. + + If the config file contains sections, their names are used as + namespaces for the values within. The two special sections + ``DEFAULT`` and ``bottle`` refer to the root namespace (no prefix). + """ + conf = ConfigParser() + conf.read(filename) + for section in conf.sections(): + for key, value in conf.items(section): + if section not in ('DEFAULT', 'bottle'): + key = section + '.' + key + self[key] = value + return self + + def load_dict(self, source, namespace=''): + """ Load values from a dictionary structure. Nesting can be used to + represent namespaces. + + >>> c = ConfigDict() + >>> c.load_dict({'some': {'namespace': {'key': 'value'} } }) + {'some.namespace.key': 'value'} + """ + for key, value in source.items(): + if isinstance(key, basestring): + nskey = (namespace + '.' + key).strip('.') + if isinstance(value, dict): + self.load_dict(value, namespace=nskey) + else: + self[nskey] = value + else: + raise TypeError('Key has type %r (not a string)' % type(key)) + return self + + def update(self, *a, **ka): + """ If the first parameter is a string, all keys are prefixed with this + namespace. Apart from that it works just as the usual dict.update(). + Example: ``update('some.namespace', key='value')`` """ + prefix = '' + if a and isinstance(a[0], basestring): + prefix = a[0].strip('.') + '.' + a = a[1:] + for key, value in dict(*a, **ka).items(): + self[prefix + key] = value + + def setdefault(self, key, value): + if key not in self: + self[key] = value + return self[key] + + def __setitem__(self, key, value): + if not isinstance(key, basestring): + raise TypeError('Key has type %r (not a string)' % type(key)) + value = self.meta_get(key, 'filter', lambda x: x)(value) + if key in self and self[key] is value: + return + self._on_change(key, value) + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + self._on_change(key, None) + dict.__delitem__(self, key) + + def meta_get(self, key, metafield, default=None): + """ Return the value of a meta field for a key. """ + return self._meta.get(key, {}).get(metafield, default) + + def meta_set(self, key, metafield, value): + """ Set the meta field for a key to a new value. This triggers the + on-change handler for existing keys. """ + self._meta.setdefault(key, {})[metafield] = value + if key in self: + self[key] = self[key] + + def meta_list(self, key): + """ Return an iterable of meta field names defined for a key. """ + return self._meta.get(key, {}).keys() + + +class AppStack(list): + """ A stack-like list. Calling it returns the head of the stack. """ + + def __call__(self): + """ Return the current default application. """ + return self[-1] + + def push(self, value=None): + """ Add a new :class:`Bottle` instance to the stack """ + if not isinstance(value, Bottle): + value = Bottle() + self.append(value) + return value + + +class WSGIFileWrapper(object): + def __init__(self, fp, buffer_size=1024 * 64): + self.fp, self.buffer_size = fp, buffer_size + for attr in ('fileno', 'close', 'read', 'readlines', 'tell', 'seek'): + if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr)) + + def __iter__(self): + buff, read = self.buffer_size, self.read + while True: + part = read(buff) + if not part: return + yield part + + +class _closeiter(object): + """ This only exists to be able to attach a .close method to iterators that + do not support attribute assignment (most of itertools). """ + + def __init__(self, iterator, close=None): + self.iterator = iterator + self.close_callbacks = makelist(close) + + def __iter__(self): + return iter(self.iterator) + + def close(self): + for func in self.close_callbacks: + func() + + +class ResourceManager(object): + """ This class manages a list of search paths and helps to find and open + application-bound resources (files). + + :param base: default value for :meth:`add_path` calls. + :param opener: callable used to open resources. + :param cachemode: controls which lookups are cached. One of 'all', + 'found' or 'none'. + """ + + def __init__(self, base='./', opener=open, cachemode='all'): + self.opener = opener + self.base = base + self.cachemode = cachemode + + #: A list of search paths. See :meth:`add_path` for details. + self.path = [] + #: A cache for resolved paths. ``res.cache.clear()`` clears the cache. + self.cache = {} + + def add_path(self, path, base=None, index=None, create=False): + """ Add a new path to the list of search paths. Return False if the + path does not exist. + + :param path: The new search path. Relative paths are turned into + an absolute and normalized form. If the path looks like a file + (not ending in `/`), the filename is stripped off. + :param base: Path used to absolutize relative search paths. + Defaults to :attr:`base` which defaults to ``os.getcwd()``. + :param index: Position within the list of search paths. Defaults + to last index (appends to the list). + + The `base` parameter makes it easy to reference files installed + along with a python module or package:: + + res.add_path('./resources/', __file__) + """ + base = os.path.abspath(os.path.dirname(base or self.base)) + path = os.path.abspath(os.path.join(base, os.path.dirname(path))) + path += os.sep + if path in self.path: + self.path.remove(path) + if create and not os.path.isdir(path): + os.makedirs(path) + if index is None: + self.path.append(path) + else: + self.path.insert(index, path) + self.cache.clear() + return os.path.exists(path) + + def __iter__(self): + """ Iterate over all existing files in all registered paths. """ + search = self.path[:] + while search: + path = search.pop() + if not os.path.isdir(path): continue + for name in os.listdir(path): + full = os.path.join(path, name) + if os.path.isdir(full): search.append(full) + else: yield full + + def lookup(self, name): + """ Search for a resource and return an absolute file path, or `None`. + + The :attr:`path` list is searched in order. The first match is + returend. Symlinks are followed. The result is cached to speed up + future lookups. """ + if name not in self.cache or DEBUG: + for path in self.path: + fpath = os.path.join(path, name) + if os.path.isfile(fpath): + if self.cachemode in ('all', 'found'): + self.cache[name] = fpath + return fpath + if self.cachemode == 'all': + self.cache[name] = None + return self.cache[name] + + def open(self, name, mode='r', *args, **kwargs): + """ Find a resource and return a file object, or raise IOError. """ + fname = self.lookup(name) + if not fname: raise IOError("Resource %r not found." % name) + return self.opener(fname, mode=mode, *args, **kwargs) + + +class FileUpload(object): + def __init__(self, fileobj, name, filename, headers=None): + """ Wrapper for file uploads. """ + #: Open file(-like) object (BytesIO buffer or temporary file) + self.file = fileobj + #: Name of the upload form field + self.name = name + #: Raw filename as sent by the client (may contain unsafe characters) + self.raw_filename = filename + #: A :class:`HeaderDict` with additional headers (e.g. content-type) + self.headers = HeaderDict(headers) if headers else HeaderDict() + + content_type = HeaderProperty('Content-Type') + content_length = HeaderProperty('Content-Length', reader=int, default=-1) + + @cached_property + def filename(self): + """ Name of the file on the client file system, but normalized to ensure + file system compatibility. An empty filename is returned as 'empty'. + + Only ASCII letters, digits, dashes, underscores and dots are + allowed in the final filename. Accents are removed, if possible. + Whitespace is replaced by a single dash. Leading or tailing dots + or dashes are removed. The filename is limited to 255 characters. + """ + fname = self.raw_filename + if not isinstance(fname, unicode): + fname = fname.decode('utf8', 'ignore') + fname = normalize('NFKD', fname) + fname = fname.encode('ASCII', 'ignore').decode('ASCII') + fname = os.path.basename(fname.replace('\\', os.path.sep)) + fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip() + fname = re.sub(r'[-\s]+', '-', fname).strip('.-') + return fname[:255] or 'empty' + + def _copy_file(self, fp, chunk_size=2 ** 16): + read, write, offset = self.file.read, fp.write, self.file.tell() + while 1: + buf = read(chunk_size) + if not buf: break + write(buf) + self.file.seek(offset) + + def save(self, destination, overwrite=False, chunk_size=2 ** 16): + """ Save file to disk or copy its content to an open file(-like) object. + If *destination* is a directory, :attr:`filename` is added to the + path. Existing files are not overwritten by default (IOError). + + :param destination: File path, directory or file(-like) object. + :param overwrite: If True, replace existing files. (default: False) + :param chunk_size: Bytes to read at a time. (default: 64kb) + """ + if isinstance(destination, basestring): # Except file-likes here + if os.path.isdir(destination): + destination = os.path.join(destination, self.filename) + if not overwrite and os.path.exists(destination): + raise IOError('File exists.') + with open(destination, 'wb') as fp: + self._copy_file(fp, chunk_size) + else: + self._copy_file(destination, chunk_size) + +############################################################################### +# Application Helper ########################################################### +############################################################################### + + +def abort(code=500, text='Unknown Error.'): + """ Aborts execution and causes a HTTP error. """ + raise HTTPError(code, text) + + +def redirect(url, code=None): + """ Aborts execution and causes a 303 or 302 redirect, depending on + the HTTP protocol version. """ + if not code: + code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 + res = response.copy(cls=HTTPResponse) + res.status = code + res.body = "" + res.set_header('Location', urljoin(request.url, url)) + raise res + + +def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024): + """ Yield chunks from a range in a file. No chunk is bigger than maxread.""" + fp.seek(offset) + while bytes > 0: + part = fp.read(min(bytes, maxread)) + if not part: break + bytes -= len(part) + yield part + + +def static_file(filename, root, + mimetype='auto', + download=False, + charset='UTF-8'): + """ Open a file in a safe way and return :exc:`HTTPResponse` with status + code 200, 305, 403 or 404. The ``Content-Type``, ``Content-Encoding``, + ``Content-Length`` and ``Last-Modified`` headers are set if possible. + Special support for ``If-Modified-Since``, ``Range`` and ``HEAD`` + requests. + + :param filename: Name or path of the file to send. + :param root: Root path for file lookups. Should be an absolute directory + path. + :param mimetype: Defines the content-type header (default: guess from + file extension) + :param download: If True, ask the browser to open a `Save as...` dialog + instead of opening the file with the associated program. You can + specify a custom filename as a string. If not specified, the + original filename is used (default: False). + :param charset: The charset to use for files with a ``text/*`` + mime-type. (default: UTF-8) + """ + + root = os.path.abspath(root) + os.sep + filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) + headers = dict() + + if not filename.startswith(root): + return HTTPError(403, "Access denied.") + if not os.path.exists(filename) or not os.path.isfile(filename): + return HTTPError(404, "File does not exist.") + if not os.access(filename, os.R_OK): + return HTTPError(403, "You do not have permission to access this file.") + + if mimetype == 'auto': + if download and download != True: + mimetype, encoding = mimetypes.guess_type(download) + else: + mimetype, encoding = mimetypes.guess_type(filename) + if encoding: headers['Content-Encoding'] = encoding + + if mimetype: + if mimetype[:5] == 'text/' and charset and 'charset' not in mimetype: + mimetype += '; charset=%s' % charset + headers['Content-Type'] = mimetype + + if download: + download = os.path.basename(filename if download == True else download) + headers['Content-Disposition'] = 'attachment; filename="%s"' % download + + stats = os.stat(filename) + headers['Content-Length'] = clen = stats.st_size + lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) + headers['Last-Modified'] = lm + + ims = request.environ.get('HTTP_IF_MODIFIED_SINCE') + if ims: + ims = parse_date(ims.split(";")[0].strip()) + if ims is not None and ims >= int(stats.st_mtime): + headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", + time.gmtime()) + return HTTPResponse(status=304, **headers) + + body = '' if request.method == 'HEAD' else open(filename, 'rb') + + headers["Accept-Ranges"] = "bytes" + ranges = request.environ.get('HTTP_RANGE') + if 'HTTP_RANGE' in request.environ: + ranges = list(parse_range_header(request.environ['HTTP_RANGE'], clen)) + if not ranges: + return HTTPError(416, "Requested Range Not Satisfiable") + offset, end = ranges[0] + headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) + headers["Content-Length"] = str(end - offset) + if body: body = _file_iter_range(body, offset, end - offset) + return HTTPResponse(body, status=206, **headers) + return HTTPResponse(body, **headers) + +############################################################################### +# HTTP Utilities and MISC (TODO) ############################################### +############################################################################### + + +def debug(mode=True): + """ Change the debug level. + There is only one debug level supported at the moment.""" + global DEBUG + if mode: warnings.simplefilter('default') + DEBUG = bool(mode) + + +def http_date(value): + if isinstance(value, (datedate, datetime)): + value = value.utctimetuple() + elif isinstance(value, (int, float)): + value = time.gmtime(value) + if not isinstance(value, basestring): + value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) + return value + + +def parse_date(ims): + """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ + try: + ts = email.utils.parsedate_tz(ims) + return time.mktime(ts[:8] + (0, )) - (ts[9] or 0) - time.timezone + except (TypeError, ValueError, IndexError, OverflowError): + return None + + +def parse_auth(header): + """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" + try: + method, data = header.split(None, 1) + if method.lower() == 'basic': + user, pwd = touni(base64.b64decode(tob(data))).split(':', 1) + return user, pwd + except (KeyError, ValueError): + return None + + +def parse_range_header(header, maxlen=0): + """ Yield (start, end) ranges parsed from a HTTP Range header. Skip + unsatisfiable ranges. The end index is non-inclusive.""" + if not header or header[:6] != 'bytes=': return + ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r] + for start, end in ranges: + try: + if not start: # bytes=-100 -> last 100 bytes + start, end = max(0, maxlen - int(end)), maxlen + elif not end: # bytes=100- -> all but the first 99 bytes + start, end = int(start), maxlen + else: # bytes=100-200 -> bytes 100-200 (inclusive) + start, end = int(start), min(int(end) + 1, maxlen) + if 0 <= start < end <= maxlen: + yield start, end + except ValueError: + pass + + +def _parse_qsl(qs): + r = [] + for pair in qs.replace(';', '&').split('&'): + if not pair: continue + nv = pair.split('=', 1) + if len(nv) != 2: nv.append('') + key = urlunquote(nv[0].replace('+', ' ')) + value = urlunquote(nv[1].replace('+', ' ')) + r.append((key, value)) + return r + + +def _lscmp(a, b): + """ Compares two strings in a cryptographically safe way: + Runtime is not affected by length of common prefix. """ + return not sum(0 if x == y else 1 + for x, y in zip(a, b)) and len(a) == len(b) + + +def cookie_encode(data, key): + """ Encode and sign a pickle-able object. Return a (byte) string """ + msg = base64.b64encode(pickle.dumps(data, -1)) + sig = base64.b64encode(hmac.new(tob(key), msg).digest()) + return tob('!') + sig + tob('?') + msg + + +def cookie_decode(data, key): + """ Verify and decode an encoded string. Return an object or None.""" + data = tob(data) + if cookie_is_encoded(data): + sig, msg = data.split(tob('?'), 1) + if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())): + return pickle.loads(base64.b64decode(msg)) + return None + + +def cookie_is_encoded(data): + """ Return True if the argument looks like a encoded cookie.""" + return bool(data.startswith(tob('!')) and tob('?') in data) + + +def html_escape(string): + """ Escape HTML special characters ``&<>`` and quotes ``'"``. """ + return string.replace('&', '&').replace('<', '<').replace('>', '>')\ + .replace('"', '"').replace("'", ''') + + +def html_quote(string): + """ Escape and quote a string to be used as an HTTP attribute.""" + return '"%s"' % html_escape(string).replace('\n', ' ')\ + .replace('\r', ' ').replace('\t', ' ') + + +def yieldroutes(func): + """ Return a generator for routes that match the signature (name, args) + of the func parameter. This may yield more than one route if the function + takes optional keyword arguments. The output is best described by example:: + + a() -> '/a' + b(x, y) -> '/b//' + c(x, y=5) -> '/c/' and '/c//' + d(x=5, y=6) -> '/d' and '/d/' and '/d//' + """ + path = '/' + func.__name__.replace('__', '/').lstrip('/') + spec = getargspec(func) + argc = len(spec[0]) - len(spec[3] or []) + path += ('/<%s>' * argc) % tuple(spec[0][:argc]) + yield path + for arg in spec[0][argc:]: + path += '/<%s>' % arg + yield path + + +def path_shift(script_name, path_info, shift=1): + """ Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. + + :return: The modified paths. + :param script_name: The SCRIPT_NAME path. + :param script_name: The PATH_INFO path. + :param shift: The number of path fragments to shift. May be negative to + change the shift direction. (default: 1) + """ + if shift == 0: return script_name, path_info + pathlist = path_info.strip('/').split('/') + scriptlist = script_name.strip('/').split('/') + if pathlist and pathlist[0] == '': pathlist = [] + if scriptlist and scriptlist[0] == '': scriptlist = [] + if 0 < shift <= len(pathlist): + moved = pathlist[:shift] + scriptlist = scriptlist + moved + pathlist = pathlist[shift:] + elif 0 > shift >= -len(scriptlist): + moved = scriptlist[shift:] + pathlist = moved + pathlist + scriptlist = scriptlist[:shift] + else: + empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO' + raise AssertionError("Cannot shift. Nothing left from %s" % empty) + new_script_name = '/' + '/'.join(scriptlist) + new_path_info = '/' + '/'.join(pathlist) + if path_info.endswith('/') and pathlist: new_path_info += '/' + return new_script_name, new_path_info + + +def auth_basic(check, realm="private", text="Access denied"): + """ Callback decorator to require HTTP auth (basic). + TODO: Add route(check_auth=...) parameter. """ + + def decorator(func): + + @functools.wraps(func) + def wrapper(*a, **ka): + user, password = request.auth or (None, None) + if user is None or not check(user, password): + err = HTTPError(401, text) + err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm) + return err + return func(*a, **ka) + + return wrapper + + return decorator + +# Shortcuts for common Bottle methods. +# They all refer to the current default application. + + +def make_default_app_wrapper(name): + """ Return a callable that relays calls to the current default app. """ + + @functools.wraps(getattr(Bottle, name)) + def wrapper(*a, **ka): + return getattr(app(), name)(*a, **ka) + + return wrapper + + +route = make_default_app_wrapper('route') +get = make_default_app_wrapper('get') +post = make_default_app_wrapper('post') +put = make_default_app_wrapper('put') +delete = make_default_app_wrapper('delete') +patch = make_default_app_wrapper('patch') +error = make_default_app_wrapper('error') +mount = make_default_app_wrapper('mount') +hook = make_default_app_wrapper('hook') +install = make_default_app_wrapper('install') +uninstall = make_default_app_wrapper('uninstall') +url = make_default_app_wrapper('get_url') + +############################################################################### +# Server Adapter ############################################################### +############################################################################### + + +class ServerAdapter(object): + quiet = False + + def __init__(self, host='127.0.0.1', port=8080, **options): + self.options = options + self.host = host + self.port = int(port) + + def run(self, handler): # pragma: no cover + pass + + def __repr__(self): + args = ', '.join(['%s=%s' % (k, repr(v)) + for k, v in self.options.items()]) + return "%s(%s)" % (self.__class__.__name__, args) + + +class CGIServer(ServerAdapter): + quiet = True + + def run(self, handler): # pragma: no cover + from wsgiref.handlers import CGIHandler + + def fixed_environ(environ, start_response): + environ.setdefault('PATH_INFO', '') + return handler(environ, start_response) + + CGIHandler().run(fixed_environ) + + +class FlupFCGIServer(ServerAdapter): + def run(self, handler): # pragma: no cover + import flup.server.fcgi + self.options.setdefault('bindAddress', (self.host, self.port)) + flup.server.fcgi.WSGIServer(handler, **self.options).run() + + +class WSGIRefServer(ServerAdapter): + def run(self, app): # pragma: no cover + from wsgiref.simple_server import make_server + from wsgiref.simple_server import WSGIRequestHandler, WSGIServer + import socket + + class FixedHandler(WSGIRequestHandler): + def address_string(self): # Prevent reverse DNS lookups please. + return self.client_address[0] + + def log_request(*args, **kw): + if not self.quiet: + return WSGIRequestHandler.log_request(*args, **kw) + + handler_cls = self.options.get('handler_class', FixedHandler) + server_cls = self.options.get('server_class', WSGIServer) + + if ':' in self.host: # Fix wsgiref for IPv6 addresses. + if getattr(server_cls, 'address_family') == socket.AF_INET: + + class server_cls(server_cls): + address_family = socket.AF_INET6 + + self.srv = make_server(self.host, self.port, app, server_cls, + handler_cls) + self.port = self.srv.server_port # update port actual port (0 means random) + try: + self.srv.serve_forever() + except KeyboardInterrupt: + self.srv.server_close() # Prevent ResourceWarning: unclosed socket + raise + + +class CherryPyServer(ServerAdapter): + def run(self, handler): # pragma: no cover + from cherrypy import wsgiserver + self.options['bind_addr'] = (self.host, self.port) + self.options['wsgi_app'] = handler + + certfile = self.options.get('certfile') + if certfile: + del self.options['certfile'] + keyfile = self.options.get('keyfile') + if keyfile: + del self.options['keyfile'] + + server = wsgiserver.CherryPyWSGIServer(**self.options) + if certfile: + server.ssl_certificate = certfile + if keyfile: + server.ssl_private_key = keyfile + + try: + server.start() + finally: + server.stop() + + +class WaitressServer(ServerAdapter): + def run(self, handler): + from waitress import serve + serve(handler, host=self.host, port=self.port, _quiet=self.quiet) + + +class PasteServer(ServerAdapter): + def run(self, handler): # pragma: no cover + from paste import httpserver + from paste.translogger import TransLogger + handler = TransLogger(handler, setup_console_handler=(not self.quiet)) + httpserver.serve(handler, + host=self.host, + port=str(self.port), **self.options) + + +class MeinheldServer(ServerAdapter): + def run(self, handler): + from meinheld import server + server.listen((self.host, self.port)) + server.run(handler) + + +class FapwsServer(ServerAdapter): + """ Extremely fast webserver using libev. See http://www.fapws.org/ """ + + def run(self, handler): # pragma: no cover + import fapws._evwsgi as evwsgi + from fapws import base, config + port = self.port + if float(config.SERVER_IDENT[-2:]) > 0.4: + # fapws3 silently changed its API in 0.5 + port = str(port) + evwsgi.start(self.host, port) + # fapws3 never releases the GIL. Complain upstream. I tried. No luck. + if 'BOTTLE_CHILD' in os.environ and not self.quiet: + _stderr("WARNING: Auto-reloading does not work with Fapws3.\n") + _stderr(" (Fapws3 breaks python thread support)\n") + evwsgi.set_base_module(base) + + def app(environ, start_response): + environ['wsgi.multiprocess'] = False + return handler(environ, start_response) + + evwsgi.wsgi_cb(('', app)) + evwsgi.run() + + +class TornadoServer(ServerAdapter): + """ The super hyped asynchronous server by facebook. Untested. """ + + def run(self, handler): # pragma: no cover + import tornado.wsgi, tornado.httpserver, tornado.ioloop + container = tornado.wsgi.WSGIContainer(handler) + server = tornado.httpserver.HTTPServer(container) + server.listen(port=self.port, address=self.host) + tornado.ioloop.IOLoop.instance().start() + + +class AppEngineServer(ServerAdapter): + """ Adapter for Google App Engine. """ + quiet = True + + def run(self, handler): + from google.appengine.ext.webapp import util + # A main() function in the handler script enables 'App Caching'. + # Lets makes sure it is there. This _really_ improves performance. + module = sys.modules.get('__main__') + if module and not hasattr(module, 'main'): + module.main = lambda: util.run_wsgi_app(handler) + util.run_wsgi_app(handler) + + +class TwistedServer(ServerAdapter): + """ Untested. """ + + def run(self, handler): + from twisted.web import server, wsgi + from twisted.python.threadpool import ThreadPool + from twisted.internet import reactor + thread_pool = ThreadPool() + thread_pool.start() + reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop) + factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler)) + reactor.listenTCP(self.port, factory, interface=self.host) + if not reactor.running: + reactor.run() + + +class DieselServer(ServerAdapter): + """ Untested. """ + + def run(self, handler): + from diesel.protocols.wsgi import WSGIApplication + app = WSGIApplication(handler, port=self.port) + app.run() + + +class GeventServer(ServerAdapter): + """ Untested. Options: + + * `fast` (default: False) uses libevent's http server, but has some + issues: No streaming, no pipelining, no SSL. + * See gevent.wsgi.WSGIServer() documentation for more options. + """ + + def run(self, handler): + from gevent import wsgi, pywsgi, local + if not isinstance(threading.local(), local.local): + msg = "Bottle requires gevent.monkey.patch_all() (before import)" + raise RuntimeError(msg) + if not self.options.pop('fast', None): wsgi = pywsgi + self.options['log'] = None if self.quiet else 'default' + address = (self.host, self.port) + server = wsgi.WSGIServer(address, handler, **self.options) + if 'BOTTLE_CHILD' in os.environ: + import signal + signal.signal(signal.SIGINT, lambda s, f: server.stop()) + server.serve_forever() + + +class GeventSocketIOServer(ServerAdapter): + def run(self, handler): + from socketio import server + address = (self.host, self.port) + server.SocketIOServer(address, handler, **self.options).serve_forever() + + +class GunicornServer(ServerAdapter): + """ Untested. See http://gunicorn.org/configure.html for options. """ + + def run(self, handler): + from gunicorn.app.base import Application + + config = {'bind': "%s:%d" % (self.host, int(self.port))} + config.update(self.options) + + class GunicornApplication(Application): + def init(self, parser, opts, args): + return config + + def load(self): + return handler + + GunicornApplication().run() + + +class EventletServer(ServerAdapter): + """ Untested. Options: + + * `backlog` adjust the eventlet backlog parameter which is the maximum + number of queued connections. Should be at least 1; the maximum + value is system-dependent. + * `family`: (default is 2) socket family, optional. See socket + documentation for available families. + """ + + def run(self, handler): + from eventlet import wsgi, listen, patcher + if not patcher.is_monkey_patched(os): + msg = "Bottle requires eventlet.monkey_patch() (before import)" + raise RuntimeError(msg) + socket_args = {} + for arg in ('backlog', 'family'): + try: + socket_args[arg] = self.options.pop(arg) + except KeyError: + pass + address = (self.host, self.port) + try: + wsgi.server(listen(address, **socket_args), handler, + log_output=(not self.quiet)) + except TypeError: + # Fallback, if we have old version of eventlet + wsgi.server(listen(address), handler) + + +class RocketServer(ServerAdapter): + """ Untested. """ + + def run(self, handler): + from rocket import Rocket + server = Rocket((self.host, self.port), 'wsgi', {'wsgi_app': handler}) + server.start() + + +class BjoernServer(ServerAdapter): + """ Fast server written in C: https://github.com/jonashaag/bjoern """ + + def run(self, handler): + from bjoern import run + run(handler, self.host, self.port) + + +class AiohttpServer(ServerAdapter): + """ Untested. + aiohttp + https://pypi.python.org/pypi/aiohttp/ + """ + + def run(self, handler): + import asyncio + from aiohttp.wsgi import WSGIServerHttpProtocol + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + protocol_factory = lambda: WSGIServerHttpProtocol( + handler, + readpayload=True, + debug=(not self.quiet)) + self.loop.run_until_complete(self.loop.create_server(protocol_factory, + self.host, + self.port)) + + if 'BOTTLE_CHILD' in os.environ: + import signal + signal.signal(signal.SIGINT, lambda s, f: self.loop.stop()) + + try: + self.loop.run_forever() + except KeyboardInterrupt: + self.loop.stop() + + +class AutoServer(ServerAdapter): + """ Untested. """ + adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, + WSGIRefServer] + + def run(self, handler): + for sa in self.adapters: + try: + return sa(self.host, self.port, **self.options).run(handler) + except ImportError: + pass + + +server_names = { + 'cgi': CGIServer, + 'flup': FlupFCGIServer, + 'wsgiref': WSGIRefServer, + 'waitress': WaitressServer, + 'cherrypy': CherryPyServer, + 'paste': PasteServer, + 'fapws3': FapwsServer, + 'tornado': TornadoServer, + 'gae': AppEngineServer, + 'twisted': TwistedServer, + 'diesel': DieselServer, + 'meinheld': MeinheldServer, + 'gunicorn': GunicornServer, + 'eventlet': EventletServer, + 'gevent': GeventServer, + 'geventSocketIO': GeventSocketIOServer, + 'rocket': RocketServer, + 'bjoern': BjoernServer, + 'aiohttp': AiohttpServer, + 'auto': AutoServer, +} + +############################################################################### +# Application Control ########################################################## +############################################################################### + + +def load(target, **namespace): + """ Import a module or fetch an object from a module. + + * ``package.module`` returns `module` as a module object. + * ``pack.mod:name`` returns the module variable `name` from `pack.mod`. + * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result. + + The last form accepts not only function calls, but any type of + expression. Keyword arguments passed to this function are available as + local variables. Example: ``import_string('re:compile(x)', x='[a-z]')`` + """ + module, target = target.split(":", 1) if ':' in target else (target, None) + if module not in sys.modules: __import__(module) + if not target: return sys.modules[module] + if target.isalnum(): return getattr(sys.modules[module], target) + package_name = module.split('.')[0] + namespace[package_name] = sys.modules[package_name] + return eval('%s.%s' % (module, target), namespace) + + +def load_app(target): + """ Load a bottle application from a module and make sure that the import + does not affect the current default application, but returns a separate + application object. See :func:`load` for the target parameter. """ + global NORUN + NORUN, nr_old = True, NORUN + tmp = default_app.push() # Create a new "default application" + try: + rv = load(target) # Import the target module + return rv if callable(rv) else tmp + finally: + default_app.remove(tmp) # Remove the temporary added default application + NORUN = nr_old + + +_debug = debug + + +def run(app=None, + server='wsgiref', + host='127.0.0.1', + port=8080, + interval=1, + reloader=False, + quiet=False, + plugins=None, + debug=None, + config=None, **kargs): + """ Start a server instance. This method blocks until the server terminates. + + :param app: WSGI application or target string supported by + :func:`load_app`. (default: :func:`default_app`) + :param server: Server adapter to use. See :data:`server_names` keys + for valid names or pass a :class:`ServerAdapter` subclass. + (default: `wsgiref`) + :param host: Server address to bind to. Pass ``0.0.0.0`` to listens on + all interfaces including the external one. (default: 127.0.0.1) + :param port: Server port to bind to. Values below 1024 require root + privileges. (default: 8080) + :param reloader: Start auto-reloading server? (default: False) + :param interval: Auto-reloader interval in seconds (default: 1) + :param quiet: Suppress output to stdout and stderr? (default: False) + :param options: Options passed to the server adapter. + """ + if NORUN: return + if reloader and not os.environ.get('BOTTLE_CHILD'): + import subprocess + lockfile = None + try: + fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') + os.close(fd) # We only need this file to exist. We never write to it + while os.path.exists(lockfile): + args = [sys.executable] + sys.argv + environ = os.environ.copy() + environ['BOTTLE_CHILD'] = 'true' + environ['BOTTLE_LOCKFILE'] = lockfile + p = subprocess.Popen(args, env=environ) + while p.poll() is None: # Busy wait... + os.utime(lockfile, None) # I am alive! + time.sleep(interval) + if p.poll() != 3: + if os.path.exists(lockfile): os.unlink(lockfile) + sys.exit(p.poll()) + except KeyboardInterrupt: + pass + finally: + if os.path.exists(lockfile): + os.unlink(lockfile) + return + + try: + if debug is not None: _debug(debug) + app = app or default_app() + if isinstance(app, basestring): + app = load_app(app) + if not callable(app): + raise ValueError("Application is not callable: %r" % app) + + for plugin in plugins or []: + if isinstance(plugin, basestring): + plugin = load(plugin) + app.install(plugin) + + if config: + app.config.update(config) + + if server in server_names: + server = server_names.get(server) + if isinstance(server, basestring): + server = load(server) + if isinstance(server, type): + server = server(host=host, port=port, **kargs) + if not isinstance(server, ServerAdapter): + raise ValueError("Unknown or unsupported server: %r" % server) + + server.quiet = server.quiet or quiet + if not server.quiet: + _stderr("Bottle v%s server starting up (using %s)...\n" % + (__version__, repr(server))) + _stderr("Listening on http://%s:%d/\n" % + (server.host, server.port)) + _stderr("Hit Ctrl-C to quit.\n\n") + + if reloader: + lockfile = os.environ.get('BOTTLE_LOCKFILE') + bgcheck = FileCheckerThread(lockfile, interval) + with bgcheck: + server.run(app) + if bgcheck.status == 'reload': + sys.exit(3) + else: + server.run(app) + except KeyboardInterrupt: + pass + except (SystemExit, MemoryError): + raise + except: + if not reloader: raise + if not getattr(server, 'quiet', quiet): + print_exc() + time.sleep(interval) + sys.exit(3) + + +class FileCheckerThread(threading.Thread): + """ Interrupt main-thread as soon as a changed module file is detected, + the lockfile gets deleted or gets to old. """ + + def __init__(self, lockfile, interval): + threading.Thread.__init__(self) + self.daemon = True + self.lockfile, self.interval = lockfile, interval + #: Is one of 'reload', 'error' or 'exit' + self.status = None + + def run(self): + exists = os.path.exists + mtime = lambda p: os.stat(p).st_mtime + files = dict() + + for module in list(sys.modules.values()): + path = getattr(module, '__file__', '') + if path[-4:] in ('.pyo', '.pyc'): path = path[:-1] + if path and exists(path): files[path] = mtime(path) + + while not self.status: + if not exists(self.lockfile)\ + or mtime(self.lockfile) < time.time() - self.interval - 5: + self.status = 'error' + thread.interrupt_main() + for path, lmtime in list(files.items()): + if not exists(path) or mtime(path) > lmtime: + self.status = 'reload' + thread.interrupt_main() + break + time.sleep(self.interval) + + def __enter__(self): + self.start() + + def __exit__(self, exc_type, *_): + if not self.status: self.status = 'exit' # silent exit + self.join() + return exc_type is not None and issubclass(exc_type, KeyboardInterrupt) + +############################################################################### +# Template Adapters ############################################################ +############################################################################### + + +class TemplateError(HTTPError): + def __init__(self, message): + HTTPError.__init__(self, 500, message) + + +class BaseTemplate(object): + """ Base class and minimal API for template adapters """ + extensions = ['tpl', 'html', 'thtml', 'stpl'] + settings = {} #used in prepare() + defaults = {} #used in render() + + def __init__(self, + source=None, + name=None, + lookup=None, + encoding='utf8', **settings): + """ Create a new template. + If the source parameter (str or buffer) is missing, the name argument + is used to guess a template filename. Subclasses can assume that + self.source and/or self.filename are set. Both are strings. + The lookup, encoding and settings parameters are stored as instance + variables. + The lookup parameter stores a list containing directory paths. + The encoding parameter should be used to decode byte strings or files. + The settings parameter contains a dict for engine-specific settings. + """ + self.name = name + self.source = source.read() if hasattr(source, 'read') else source + self.filename = source.filename if hasattr(source, 'filename') else None + self.lookup = [os.path.abspath(x) for x in lookup] if lookup else [] + self.encoding = encoding + self.settings = self.settings.copy() # Copy from class variable + self.settings.update(settings) # Apply + if not self.source and self.name: + self.filename = self.search(self.name, self.lookup) + if not self.filename: + raise TemplateError('Template %s not found.' % repr(name)) + if not self.source and not self.filename: + raise TemplateError('No template specified.') + self.prepare(**self.settings) + + @classmethod + def search(cls, name, lookup=None): + """ Search name in all directories specified in lookup. + First without, then with common extensions. Return first hit. """ + if not lookup: + depr('The template lookup path list should not be empty.', + True) #0.12 + lookup = ['.'] + + if os.path.isabs(name) and os.path.isfile(name): + depr('Absolute template path names are deprecated.', True) #0.12 + return os.path.abspath(name) + + for spath in lookup: + spath = os.path.abspath(spath) + os.sep + fname = os.path.abspath(os.path.join(spath, name)) + if not fname.startswith(spath): continue + if os.path.isfile(fname): return fname + for ext in cls.extensions: + if os.path.isfile('%s.%s' % (fname, ext)): + return '%s.%s' % (fname, ext) + + @classmethod + def global_config(cls, key, *args): + """ This reads or sets the global settings stored in class.settings. """ + if args: + cls.settings = cls.settings.copy() # Make settings local to class + cls.settings[key] = args[0] + else: + return cls.settings[key] + + def prepare(self, **options): + """ Run preparations (parsing, caching, ...). + It should be possible to call this again to refresh a template or to + update settings. + """ + raise NotImplementedError + + def render(self, *args, **kwargs): + """ Render the template with the specified local variables and return + a single byte or unicode string. If it is a byte string, the encoding + must match self.encoding. This method must be thread-safe! + Local variables may be provided in dictionaries (args) + or directly, as keywords (kwargs). + """ + raise NotImplementedError + + +class MakoTemplate(BaseTemplate): + def prepare(self, **options): + from mako.template import Template + from mako.lookup import TemplateLookup + options.update({'input_encoding': self.encoding}) + options.setdefault('format_exceptions', bool(DEBUG)) + lookup = TemplateLookup(directories=self.lookup, **options) + if self.source: + self.tpl = Template(self.source, lookup=lookup, **options) + else: + self.tpl = Template(uri=self.name, + filename=self.filename, + lookup=lookup, **options) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + _defaults = self.defaults.copy() + _defaults.update(kwargs) + return self.tpl.render(**_defaults) + + +class CheetahTemplate(BaseTemplate): + def prepare(self, **options): + from Cheetah.Template import Template + self.context = threading.local() + self.context.vars = {} + options['searchList'] = [self.context.vars] + if self.source: + self.tpl = Template(source=self.source, **options) + else: + self.tpl = Template(file=self.filename, **options) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + self.context.vars.update(self.defaults) + self.context.vars.update(kwargs) + out = str(self.tpl) + self.context.vars.clear() + return out + + +class Jinja2Template(BaseTemplate): + def prepare(self, filters=None, tests=None, globals={}, **kwargs): + from jinja2 import Environment, FunctionLoader + self.env = Environment(loader=FunctionLoader(self.loader), **kwargs) + if filters: self.env.filters.update(filters) + if tests: self.env.tests.update(tests) + if globals: self.env.globals.update(globals) + if self.source: + self.tpl = self.env.from_string(self.source) + else: + self.tpl = self.env.get_template(self.filename) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + _defaults = self.defaults.copy() + _defaults.update(kwargs) + return self.tpl.render(**_defaults) + + def loader(self, name): + fname = self.search(name, self.lookup) + if not fname: return + with open(fname, "rb") as f: + return f.read().decode(self.encoding) + + +class SimpleTemplate(BaseTemplate): + def prepare(self, + escape_func=html_escape, + noescape=False, + syntax=None, **ka): + self.cache = {} + enc = self.encoding + self._str = lambda x: touni(x, enc) + self._escape = lambda x: escape_func(touni(x, enc)) + self.syntax = syntax + if noescape: + self._str, self._escape = self._escape, self._str + + @cached_property + def co(self): + return compile(self.code, self.filename or '', 'exec') + + @cached_property + def code(self): + source = self.source + if not source: + with open(self.filename, 'rb') as f: + source = f.read() + try: + source, encoding = touni(source), 'utf8' + except UnicodeError: + depr('Template encodings other than utf8 are not supported.') #0.11 + source, encoding = touni(source, 'latin1'), 'latin1' + parser = StplParser(source, encoding=encoding, syntax=self.syntax) + code = parser.translate() + self.encoding = parser.encoding + return code + + def _rebase(self, _env, _name=None, **kwargs): + _env['_rebase'] = (_name, kwargs) + + def _include(self, _env, _name=None, **kwargs): + env = _env.copy() + env.update(kwargs) + if _name not in self.cache: + self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) + return self.cache[_name].execute(env['_stdout'], env) + + def execute(self, _stdout, kwargs): + env = self.defaults.copy() + env.update(kwargs) + env.update({ + '_stdout': _stdout, + '_printlist': _stdout.extend, + 'include': functools.partial(self._include, env), + 'rebase': functools.partial(self._rebase, env), + '_rebase': None, + '_str': self._str, + '_escape': self._escape, + 'get': env.get, + 'setdefault': env.setdefault, + 'defined': env.__contains__ + }) + eval(self.co, env) + if env.get('_rebase'): + subtpl, rargs = env.pop('_rebase') + rargs['base'] = ''.join(_stdout) #copy stdout + del _stdout[:] # clear stdout + return self._include(env, subtpl, **rargs) + return env + + def render(self, *args, **kwargs): + """ Render the template using keyword arguments as local variables. """ + env = {} + stdout = [] + for dictarg in args: + env.update(dictarg) + env.update(kwargs) + self.execute(stdout, env) + return ''.join(stdout) + + +class StplSyntaxError(TemplateError): + + pass + + +class StplParser(object): + """ Parser for stpl templates. """ + _re_cache = {} #: Cache for compiled re patterns + + # This huge pile of voodoo magic splits python code into 8 different tokens. + # We use the verbose (?x) regex mode to make this more manageable + + _re_tok = _re_inl = r'''((?mx) # verbose and dot-matches-newline mode + [urbURB]* + (?: ''(?!') + |""(?!") + |'{6} + |"{6} + |'(?:[^\\']|\\.)+?' + |"(?:[^\\"]|\\.)+?" + |'{3}(?:[^\\]|\\.|\n)+?'{3} + |"{3}(?:[^\\]|\\.|\n)+?"{3} + ) + )''' + + _re_inl = _re_tok.replace(r'|\n', '') # We re-use this string pattern later + + _re_tok += r''' + # 2: Comments (until end of line, but not the newline itself) + |(\#.*) + + # 3: Open and close (4) grouping tokens + |([\[\{\(]) + |([\]\}\)]) + + # 5,6: Keywords that start or continue a python block (only start of line) + |^([\ \t]*(?:if|for|while|with|try|def|class)\b) + |^([\ \t]*(?:elif|else|except|finally)\b) + + # 7: Our special 'end' keyword (but only if it stands alone) + |((?:^|;)[\ \t]*end[\ \t]*(?=(?:%(block_close)s[\ \t]*)?\r?$|;|\#)) + + # 8: A customizable end-of-code-block template token (only end of line) + |(%(block_close)s[\ \t]*(?=\r?$)) + + # 9: And finally, a single newline. The 10th token is 'everything else' + |(\r?\n) + ''' + + # Match the start tokens of code areas in a template + _re_split = r'''(?m)^[ \t]*(\\?)((%(line_start)s)|(%(block_start)s))''' + # Match inline statements (may contain python strings) + _re_inl = r'''%%(inline_start)s((?:%s|[^'"\n]+?)*?)%%(inline_end)s''' % _re_inl + + default_syntax = '<% %> % {{ }}' + + def __init__(self, source, syntax=None, encoding='utf8'): + self.source, self.encoding = touni(source, encoding), encoding + self.set_syntax(syntax or self.default_syntax) + self.code_buffer, self.text_buffer = [], [] + self.lineno, self.offset = 1, 0 + self.indent, self.indent_mod = 0, 0 + self.paren_depth = 0 + + def get_syntax(self): + """ Tokens as a space separated string (default: <% %> % {{ }}) """ + return self._syntax + + def set_syntax(self, syntax): + self._syntax = syntax + self._tokens = syntax.split() + if not syntax in self._re_cache: + names = 'block_start block_close line_start inline_start inline_end' + etokens = map(re.escape, self._tokens) + pattern_vars = dict(zip(names.split(), etokens)) + patterns = (self._re_split, self._re_tok, self._re_inl) + patterns = [re.compile(p % pattern_vars) for p in patterns] + self._re_cache[syntax] = patterns + self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax] + + syntax = property(get_syntax, set_syntax) + + def translate(self): + if self.offset: raise RuntimeError('Parser is a one time instance.') + while True: + m = self.re_split.search(self.source, pos=self.offset) + if m: + text = self.source[self.offset:m.start()] + self.text_buffer.append(text) + self.offset = m.end() + if m.group(1): # Escape syntax + line, sep, _ = self.source[self.offset:].partition('\n') + self.text_buffer.append(self.source[m.start():m.start(1)] + + m.group(2) + line + sep) + self.offset += len(line + sep) + continue + self.flush_text() + self.offset += self.read_code(self.source[self.offset:], + multiline=bool(m.group(4))) + else: + break + self.text_buffer.append(self.source[self.offset:]) + self.flush_text() + return ''.join(self.code_buffer) + + def read_code(self, pysource, multiline): + code_line, comment = '', '' + offset = 0 + while True: + m = self.re_tok.search(pysource, pos=offset) + if not m: + code_line += pysource[offset:] + offset = len(pysource) + self.write_code(code_line.strip(), comment) + break + code_line += pysource[offset:m.start()] + offset = m.end() + _str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups() + if self.paren_depth > 0 and (_blk1 or _blk2): # a if b else c + code_line += _blk1 or _blk2 + continue + if _str: # Python string + code_line += _str + elif _com: # Python comment (up to EOL) + comment = _com + if multiline and _com.strip().endswith(self._tokens[1]): + multiline = False # Allow end-of-block in comments + elif _po: # open parenthesis + self.paren_depth += 1 + code_line += _po + elif _pc: # close parenthesis + if self.paren_depth > 0: + # we could check for matching parentheses here, but it's + # easier to leave that to python - just check counts + self.paren_depth -= 1 + code_line += _pc + elif _blk1: # Start-block keyword (if/for/while/def/try/...) + code_line, self.indent_mod = _blk1, -1 + self.indent += 1 + elif _blk2: # Continue-block keyword (else/elif/except/...) + code_line, self.indent_mod = _blk2, -1 + elif _end: # The non-standard 'end'-keyword (ends a block) + self.indent -= 1 + elif _cend: # The end-code-block template token (usually '%>') + if multiline: multiline = False + else: code_line += _cend + else: # \n + self.write_code(code_line.strip(), comment) + self.lineno += 1 + code_line, comment, self.indent_mod = '', '', 0 + if not multiline: + break + + return offset + + def flush_text(self): + text = ''.join(self.text_buffer) + del self.text_buffer[:] + if not text: return + parts, pos, nl = [], 0, '\\\n' + ' ' * self.indent + for m in self.re_inl.finditer(text): + prefix, pos = text[pos:m.start()], m.end() + if prefix: + parts.append(nl.join(map(repr, prefix.splitlines(True)))) + if prefix.endswith('\n'): parts[-1] += nl + parts.append(self.process_inline(m.group(1).strip())) + if pos < len(text): + prefix = text[pos:] + lines = prefix.splitlines(True) + if lines[-1].endswith('\\\\\n'): lines[-1] = lines[-1][:-3] + elif lines[-1].endswith('\\\\\r\n'): lines[-1] = lines[-1][:-4] + parts.append(nl.join(map(repr, lines))) + code = '_printlist((%s,))' % ', '.join(parts) + self.lineno += code.count('\n') + 1 + self.write_code(code) + + @staticmethod + def process_inline(chunk): + if chunk[0] == '!': return '_str(%s)' % chunk[1:] + return '_escape(%s)' % chunk + + def write_code(self, line, comment=''): + code = ' ' * (self.indent + self.indent_mod) + code += line.lstrip() + comment + '\n' + self.code_buffer.append(code) + + +def template(*args, **kwargs): + """ + Get a rendered template as a string iterator. + You can use a name, a filename or a template string as first parameter. + Template rendering arguments can be passed as dictionaries + or directly (as keyword arguments). + """ + tpl = args[0] if args else None + adapter = kwargs.pop('template_adapter', SimpleTemplate) + lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) + tplid = (id(lookup), tpl) + if tplid not in TEMPLATES or DEBUG: + settings = kwargs.pop('template_settings', {}) + if isinstance(tpl, adapter): + TEMPLATES[tplid] = tpl + if settings: TEMPLATES[tplid].prepare(**settings) + elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: + TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings) + else: + TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) + if not TEMPLATES[tplid]: + abort(500, 'Template (%s) not found' % tpl) + for dictarg in args[1:]: + kwargs.update(dictarg) + return TEMPLATES[tplid].render(kwargs) + + +mako_template = functools.partial(template, template_adapter=MakoTemplate) +cheetah_template = functools.partial(template, + template_adapter=CheetahTemplate) +jinja2_template = functools.partial(template, template_adapter=Jinja2Template) + + +def view(tpl_name, **defaults): + """ Decorator: renders a template for a handler. + The handler can control its behavior like that: + + - return a dict of template vars to fill out the template + - return something other than a dict and the view decorator will not + process the template, but return the handler result as is. + This includes returning a HTTPResponse(dict) to get, + for instance, JSON with autojson or other castfilters. + """ + + def decorator(func): + + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if isinstance(result, (dict, DictMixin)): + tplvars = defaults.copy() + tplvars.update(result) + return template(tpl_name, **tplvars) + elif result is None: + return template(tpl_name, defaults) + return result + + return wrapper + + return decorator + + +mako_view = functools.partial(view, template_adapter=MakoTemplate) +cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) +jinja2_view = functools.partial(view, template_adapter=Jinja2Template) + +############################################################################### +# Constants and Globals ######################################################## +############################################################################### + +TEMPLATE_PATH = ['./', './views/'] +TEMPLATES = {} +DEBUG = False +NORUN = False # If set, run() does nothing. Used by load_app() + +#: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') +HTTP_CODES = httplib.responses.copy() +HTTP_CODES[418] = "I'm a teapot" # RFC 2324 +HTTP_CODES[428] = "Precondition Required" +HTTP_CODES[429] = "Too Many Requests" +HTTP_CODES[431] = "Request Header Fields Too Large" +HTTP_CODES[511] = "Network Authentication Required" +_HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v)) + for (k, v) in HTTP_CODES.items()) + +#: The default template used for error pages. Override with @error() +ERROR_PAGE_TEMPLATE = """ +%%try: + %%from %s import DEBUG, request + + + + Error: {{e.status}} + + + +

Error: {{e.status}}

+

Sorry, the requested URL {{repr(request.url)}} + caused an error:

+
{{e.body}}
+ %%if DEBUG and e.exception: +

Exception:

+
{{repr(e.exception)}}
+ %%end + %%if DEBUG and e.traceback: +

Traceback:

+
{{e.traceback}}
+ %%end + + +%%except ImportError: + ImportError: Could not generate the error page. Please add bottle to + the import path. +%%end +""" % __name__ + +#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a +#: request callback, this instance always refers to the *current* request +#: (even on a multi-threaded server). +request = LocalRequest() + +#: A thread-safe instance of :class:`LocalResponse`. It is used to change the +#: HTTP response for the *current* request. +response = LocalResponse() + +#: A thread-safe namespace. Not used by Bottle. +local = threading.local() + +# Initialize app stack (create first empty Bottle app) +# BC: 0.6.4 and needed for run() +app = default_app = AppStack() +app.push() + +#: A virtual package that redirects import statements. +#: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. +ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else + __name__ + ".ext", 'bottle_%s').module + + + +if __name__ == '__main__': + opt, args, parser = _cli_parse(sys.argv) + + def _cli_error(msg): + parser.print_help() + _stderr('\nError: %s\n' % msg) + sys.exit(1) + + if opt.version: + _stdout('Bottle %s\n' % __version__) + sys.exit(0) + if not args: + _cli_error("No application entry point specified.") + + sys.path.insert(0, '.') + sys.modules.setdefault('bottle', sys.modules['__main__']) + + host, port = (opt.bind or 'localhost'), 8080 + if ':' in host and host.rfind(']') < host.rfind(':'): + host, port = host.rsplit(':', 1) + host = host.strip('[]') + + config = ConfigDict() + + for cfile in opt.conf or []: + try: + if cfile.endswith('.json'): + with open(cfile, 'rb') as fp: + config.load_dict(json_loads(fp.read())) + else: + config.load_config(cfile) + except ConfigParserError: + _cli_error(str(_e())) + except IOError: + _cli_error("Unable to read config file %r" % cfile) + except (UnicodeError, TypeError, ValueError): + _cli_error("Unable to parse config file %r: %s" % (cfile, _e())) + + for cval in opt.param or []: + if '=' in cval: + config.update((cval.split('=', 1),)) + else: + config[cval] = True + + run(args[0], + host=host, + port=int(port), + server=opt.server, + reloader=opt.reload, + plugins=opt.plugin, + debug=opt.debug, + config=config) + +# THE END \ No newline at end of file diff --git a/_src/om2py4w/4wex0/examples/basicfirst.py b/_src/om2py4w/4wex0/examples/basicfirst.py new file mode 100644 index 000000000..6eacd4532 --- /dev/null +++ b/_src/om2py4w/4wex0/examples/basicfirst.py @@ -0,0 +1,24 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +import sys +import pycurl + +class Test: + def __init__(self): + self.contents = '' + + def body_callback(self, buf): + self.contents = self.contents + buf + +sys.stderr.write("Testing %s\n" % pycurl.version) + +t = Test() +c = pycurl.Curl() +c.setopt(c.URL, 'http://curl.haxx.se/dev/') +c.setopt(c.WRITEFUNCTION, t.body_callback) +c.perform() +c.close() + +print(t.contents) diff --git a/_src/om2py4w/4wex0/examples/file_upload.py b/_src/om2py4w/4wex0/examples/file_upload.py new file mode 100644 index 000000000..a3b769a44 --- /dev/null +++ b/_src/om2py4w/4wex0/examples/file_upload.py @@ -0,0 +1,45 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +import os, sys +import pycurl + +# Class which holds a file reference and the read callback +class FileReader: + def __init__(self, fp): + self.fp = fp + def read_callback(self, size): + return self.fp.read(size) + +# Check commandline arguments +if len(sys.argv) < 3: + print("Usage: %s " % sys.argv[0]) + raise SystemExit +url = sys.argv[1] +filename = sys.argv[2] + +if not os.path.exists(filename): + print("Error: the file '%s' does not exist" % filename) + raise SystemExit + +# Initialize pycurl +c = pycurl.Curl() +c.setopt(pycurl.URL, url) +c.setopt(pycurl.UPLOAD, 1) + +# Two versions with the same semantics here, but the filereader version +# is useful when you have to process the data which is read before returning +if 1: + c.setopt(pycurl.READFUNCTION, FileReader(open(filename, 'rb')).read_callback) +else: + c.setopt(pycurl.READFUNCTION, open(filename, 'rb').read) + +# Set size of file to be uploaded. +filesize = os.path.getsize(filename) +c.setopt(pycurl.INFILESIZE, filesize) + +# Start transfer +print('Uploading file %s to url %s' % (filename, url)) +c.perform() +c.close() diff --git a/_src/om2py4w/4wex0/examples/linksys.py b/_src/om2py4w/4wex0/examples/linksys.py new file mode 100644 index 000000000..3c6f837db --- /dev/null +++ b/_src/om2py4w/4wex0/examples/linksys.py @@ -0,0 +1,568 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et +# +# linksys.py -- program settings on a Linkys router +# +# This tool is designed to help you recover from the occasional episodes +# of catatonia that afflict Linksys boxes. It allows you to batch-program +# them rather than manually entering values to the Web interface. Commands +# are taken from the command line first, then standard input. +# +# The somewhat spotty coverage of status queries is because I only did the +# ones that were either (a) easy, or (b) necessary. If you want to know the +# status of the box, look at the web interface. +# +# This code has been tested against the following hardware: +# +# Hardware Firmware +# ---------- --------------------- +# BEFW11S4v2 1.44.2.1, Dec 20 2002 +# +# The code is, of course, sensitive to changes in the names of CGI pages +# and field names. +# +# Note: to make the no-arguments form work, you'll need to have the following +# entry in your ~/.netrc file. If you have changed the router IP address or +# name/password, modify accordingly. +# +# machine 192.168.1.1 +# login "" +# password admin +# +# By Eric S. Raymond, August April 2003. All rites reversed. + +import sys, re, copy, curl, exceptions + +def print_stderr(arg): + sys.stderr.write(arg) + sys.stderr.write("\n") + +class LinksysError(exceptions.Exception): + def __init__(self, *args): + self.args = args + +class LinksysSession: + months = 'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec' + + WAN_CONNECT_AUTO = '1' + WAN_CONNECT_STATIC = '2' + WAN_CONNECT_PPOE = '3' + WAN_CONNECT_RAS = '4' + WAN_CONNECT_PPTP = '5' + WAN_CONNECT_HEARTBEAT = '6' + + # Substrings to check for on each page load. + # This may enable us to detect when a firmware change has hosed us. + check_strings = { + "": "basic setup functions", + "Passwd.htm": "For security reasons,", + "DHCP.html": "You can configure the router to act as a DHCP", + "Log.html": "There are some log settings and lists in this page.", + "Forward.htm":"Port forwarding can be used to set up public services", + } + + def __init__(self): + self.actions = [] + self.host = "http://192.168.1.1" + self.verbosity = False + self.pagecache = {} + + def set_verbosity(self, flag): + self.verbosity = flag + + # This is not a performance hack -- we need the page cache to do + # sanity checks at configure time. + def cache_load(self, page): + if page not in self.pagecache: + fetch = curl.Curl(self.host) + fetch.set_verbosity(self.verbosity) + fetch.get(page) + self.pagecache[page] = fetch.body() + if fetch.answered("401"): + raise LinksysError("authorization failure.", True) + elif not fetch.answered(LinksysSession.check_strings[page]): + del self.pagecache[page] + raise LinksysError("check string for page %s missing!" % os.path.join(self.host, page), False) + fetch.close() + def cache_flush(self): + self.pagecache = {} + + # Primitives + def screen_scrape(self, page, template): + self.cache_load(page) + match = re.compile(template).search(self.pagecache[page]) + if match: + result = match.group(1) + else: + result = None + return result + def get_MAC_address(self, page, prefix): + return self.screen_scrape("", prefix+r":[^M]*\(MAC Address: *([^)]*)") + def set_flag(page, flag, value): + if value: + self.actions.append(page, flag, "1") + else: + self.actions.append(page, flag, "0") + def set_IP_address(self, page, cgi, role, ip): + ind = 0 + for octet in ip.split("."): + self.actions.append(("", "F1", role + `ind+1`, octet)) + ind += 1 + + # Scrape configuration data off the main page + def get_firmware_version(self): + # This is fragile. There is no distinguishing tag before the firmware + # version, so we have to key off the pattern of the version number. + # Our model is ">1.44.2.1, Dec 20 2002<" + return self.screen_scrape("", ">([0-9.v]*, (" + \ + LinksysSession.months + ")[^<]*)<", ) + def get_LAN_MAC(self): + return self.get_MAC_address("", r"LAN IP Address") + def get_Wireless_MAC(self): + return self.get_MAC_address("", r"Wireless") + def get_WAN_MAC(self): + return self.get_MAC_address("", r"WAN Connection Type") + + # Set configuration data on the main page + def set_host_name(self, name): + self.actions.append(("", "hostName", name)) + def set_domain_name(self, name): + self.actions.append(("", "DomainName", name)) + def set_LAN_IP(self, ip): + self.set_IP_address("", "ipAddr", ip) + def set_LAN_netmask(self, ip): + if not ip.startswith("255.255.255."): + raise ValueError + lastquad = ip.split(".")[-1] + if lastquad not in ("0", "128", "192", "240", "252"): + raise ValueError + self.actions.append("", "netMask", lastquad) + def set_wireless(self, flag): + self.set_flag("", "wirelessStatus") + def set_SSID(self, ssid): + self.actions.append(("", "wirelessESSID", ssid)) + def set_SSID_broadcast(self, flag): + self.set_flag("", "broadcastSSID") + def set_channel(self, channel): + self.actions.append(("", "wirelessChannel", channel)) + def set_WEP(self, flag): + self.set_flag("", "WepType") + # FIXME: Add support for setting WEP keys + def set_connection_type(self, type): + self.actions.append(("", "WANConnectionType", type)) + def set_WAN_IP(self, ip): + self.set_IP_address("", "aliasIP", ip) + def set_WAN_netmask(self, ip): + self.set_IP_address("", "aliasMaskIP", ip) + def set_WAN_gateway_address(self, ip): + self.set_IP_address("", "routerIP", ip) + def set_DNS_server(self, index, ip): + self.set_IP_address("", "dns" + "ABC"[index], ip) + + # Set configuration data on the password page + def set_password(self, str): + self.actions.append("Passwd.htm","sysPasswd", str) + self.actions.append("Passwd.htm","sysPasswdConfirm", str) + def set_UPnP(self, flag): + self.set_flag("Passwd.htm", "UPnP_Work") + def reset(self): + self.actions.append("Passwd.htm", "FactoryDefaults") + + # DHCP features + def set_DHCP(self, flag): + if flag: + self.actions.append("DHCP.htm","dhcpStatus","Enable") + else: + self.actions.append("DHCP.htm","dhcpStatus","Disable") + def set_DHCP_starting_IP(self, val): + self.actions.append("DHCP.htm","dhcpS4", str(val)) + def set_DHCP_users(self, val): + self.actions.append("DHCP.htm","dhcpLen", str(val)) + def set_DHCP_lease_time(self, val): + self.actions.append("DHCP.htm","leaseTime", str(val)) + def set_DHCP_DNS_server(self, index, ip): + self.set_IP_address("DHCP.htm", "dns" + "ABC"[index], ip) + # FIXME: add support for setting WINS key + + # Logging features + def set_logging(self, flag): + if flag: + self.actions.append("Log.htm", "rLog", "Enable") + else: + self.actions.append("Log.htm", "rLog", "Disable") + def set_log_address(self, val): + self.actions.append("DHCP.htm","trapAddr3", str(val)) + + # The AOL parental control flag is not supported by design. + + # FIXME: add Filters and other advanced features + + def configure(self): + "Write configuration changes to the Linksys." + if self.actions: + fields = [] + self.cache_flush() + for (page, field, value) in self.actions: + self.cache_load(page) + if self.pagecache[page].find(field) == -1: + print_stderr("linksys: field %s not found where expected in page %s!" % (field, os.path.join(self.host, page))) + continue + else: + fields.append((field, value)) + # Clearing the action list before fieldsping is deliberate. + # Otherwise we could get permanently wedged by a 401. + self.actions = [] + transaction = curl.Curl(self.host) + transaction.set_verbosity(self.verbosity) + transaction.get("Gozila.cgi", tuple(fields)) + transaction.close() + +if __name__ == "__main__": + import os, cmd + + class LinksysInterpreter(cmd.Cmd): + """Interpret commands to perform LinkSys programming actions.""" + def __init__(self): + cmd.Cmd.__init__(self) + self.session = LinksysSession() + if os.isatty(0): + import readline + print("Type ? or `help' for help.") + self.prompt = self.session.host + ": " + else: + self.prompt = "" + print("Bar1") + + def flag_command(self, func, line): + if line.strip() in ("on", "enable", "yes"): + func(True) + elif line.strip() in ("off", "disable", "no"): + func(False) + else: + print_stderr("linksys: unknown switch value") + return 0 + + def do_connect(self, line): + newhost = line.strip() + if newhost: + self.session.host = newhost + self.session.cache_flush() + self.prompt = self.session.host + ": " + else: + print(self.session.host) + return 0 + def help_connect(self): + print("Usage: connect []") + print("Connect to a Linksys by name or IP address.") + print("If no argument is given, print the current host.") + + def do_status(self, line): + self.session.cache_load("") + if "" in self.session.pagecache: + print("Firmware:", self.session.get_firmware_version()) + print("LAN MAC:", self.session.get_LAN_MAC()) + print("Wireless MAC:", self.session.get_Wireless_MAC()) + print("WAN MAC:", self.session.get_WAN_MAC()) + print(".") + return 0 + def help_status(self): + print("Usage: status") + print("The status command shows the status of the Linksys.") + print("It is mainly useful as a sanity check to make sure") + print("the box is responding correctly.") + + def do_verbose(self, line): + self.flag_command(self.session.set_verbosity, line) + def help_verbose(self): + print("Usage: verbose {on|off|enable|disable|yes|no}") + print("Enables display of HTTP requests.") + + def do_host(self, line): + self.session.set_host_name(line) + return 0 + def help_host(self): + print("Usage: host ") + print("Sets the Host field to be queried by the ISP.") + + def do_domain(self, line): + print("Usage: host ") + self.session.set_domain_name(line) + return 0 + def help_domain(self): + print("Sets the Domain field to be queried by the ISP.") + + def do_lan_address(self, line): + self.session.set_LAN_IP(line) + return 0 + def help_lan_address(self): + print("Usage: lan_address ") + print("Sets the LAN IP address.") + + def do_lan_netmask(self, line): + self.session.set_LAN_netmask(line) + return 0 + def help_lan_netmask(self): + print("Usage: lan_netmask ") + print("Sets the LAN subnetwork mask.") + + def do_wireless(self, line): + self.flag_command(self.session.set_wireless, line) + return 0 + def help_wireless(self): + print("Usage: wireless {on|off|enable|disable|yes|no}") + print("Switch to enable or disable wireless features.") + + def do_ssid(self, line): + self.session.set_SSID(line) + return 0 + def help_ssid(self): + print("Usage: ssid ") + print("Sets the SSID used to control wireless access.") + + def do_ssid_broadcast(self, line): + self.flag_command(self.session.set_SSID_broadcast, line) + return 0 + def help_ssid_broadcast(self): + print("Usage: ssid_broadcast {on|off|enable|disable|yes|no}") + print("Switch to enable or disable SSID broadcast.") + + def do_channel(self, line): + self.session.set_channel(line) + return 0 + def help_channel(self): + print("Usage: channel ") + print("Sets the wireless channel.") + + def do_wep(self, line): + self.flag_command(self.session.set_WEP, line) + return 0 + def help_wep(self): + print("Usage: wep {on|off|enable|disable|yes|no}") + print("Switch to enable or disable WEP security.") + + def do_wan_type(self, line): + try: + type=eval("LinksysSession.WAN_CONNECT_"+line.strip().upper()) + self.session.set_connection_type(type) + except ValueError: + print_stderr("linksys: unknown connection type.") + return 0 + def help_wan_type(self): + print("Usage: wan_type {auto|static|ppoe|ras|pptp|heartbeat}") + print("Set the WAN connection type.") + + def do_wan_address(self, line): + self.session.set_WAN_IP(line) + return 0 + def help_wan_address(self): + print("Usage: wan_address ") + print("Sets the WAN IP address.") + + def do_wan_netmask(self, line): + self.session.set_WAN_netmask(line) + return 0 + def help_wan_netmask(self): + print("Usage: wan_netmask ") + print("Sets the WAN subnetwork mask.") + + def do_wan_gateway(self, line): + self.session.set_WAN_gateway(line) + return 0 + def help_wan_gateway(self): + print("Usage: wan_gateway ") + print("Sets the LAN subnetwork mask.") + + def do_dns(self, line): + (index, address) = line.split() + if index in ("1", "2", "3"): + self.session.set_DNS_server(eval(index), address) + else: + print_stderr("linksys: server index out of bounds.") + return 0 + def help_dns(self): + print("Usage: dns {1|2|3} ") + print("Sets a primary, secondary, or tertiary DNS server address.") + + def do_password(self, line): + self.session.set_password(line) + return 0 + def help_password(self): + print("Usage: password ") + print("Sets the router password.") + + def do_upnp(self, line): + self.flag_command(self.session.set_UPnP, line) + return 0 + def help_upnp(self): + print("Usage: upnp {on|off|enable|disable|yes|no}") + print("Switch to enable or disable Universal Plug and Play.") + + def do_reset(self, line): + self.session.reset() + def help_reset(self): + print("Usage: reset") + print("Reset Linksys settings to factory defaults.") + + def do_dhcp(self, line): + self.flag_command(self.session.set_DHCP, line) + def help_dhcp(self): + print("Usage: dhcp {on|off|enable|disable|yes|no}") + print("Switch to enable or disable DHCP features.") + + def do_dhcp_start(self, line): + self.session.set_DHCP_starting_IP(line) + def help_dhcp_start(self): + print("Usage: dhcp_start ") + print("Set the start address of the DHCP pool.") + + def do_dhcp_users(self, line): + self.session.set_DHCP_users(line) + def help_dhcp_users(self): + print("Usage: dhcp_users ") + print("Set number of address slots to allocate in the DHCP pool.") + + def do_dhcp_lease(self, line): + self.session.set_DHCP_lease(line) + def help_dhcp_lease(self): + print("Usage: dhcp_lease ") + print("Set number of address slots to allocate in the DHCP pool.") + + def do_dhcp_dns(self, line): + (index, address) = line.split() + if index in ("1", "2", "3"): + self.session.set_DHCP_DNS_server(eval(index), address) + else: + print_stderr("linksys: server index out of bounds.") + return 0 + def help_dhcp_dns(self): + print("Usage: dhcp_dns {1|2|3} ") + print("Sets primary, secondary, or tertiary DNS server address.") + + def do_logging(self, line): + self.flag_command(self.session.set_logging, line) + def help_logging(self): + print("Usage: logging {on|off|enable|disable|yes|no}") + print("Switch to enable or disable session logging.") + + def do_log_address(self, line): + self.session.set_Log_address(line) + def help_log_address(self): + print("Usage: log_address ") + print("Set the last quad of the address to which to log.") + + def do_configure(self, line): + self.session.configure() + return 0 + def help_configure(self): + print("Usage: configure") + print("Writes the configuration to the Linksys.") + + def do_cache(self, line): + print(self.session.pagecache) + def help_cache(self): + print("Usage: cache") + print("Display the page cache.") + + def do_quit(self, line): + return 1 + def help_quit(self, line): + print("The quit command ends your linksys session without") + print("writing configuration changes to the Linksys.") + def do_EOF(self, line): + print("") + self.session.configure() + return 1 + def help_EOF(self): + print("The EOF command writes the configuration to the linksys") + print("and ends your session.") + + def default(self, line): + """Pass the command through to be executed by the shell.""" + os.system(line) + return 0 + + def help_help(self): + print("On-line help is available through this command.") + print("? is a convenience alias for help.") + + def help_introduction(self): + print("""\ + +This program supports changing the settings on Linksys blue-box routers. This +capability may come in handy when they freeze up and have to be reset. Though +it can be used interactively (and will command-prompt when standard input is a +terminal) it is really designed to be used in batch mode. Commands are taken +from the command line first, then standard input. + +By default, it is assumed that the Linksys is at http://192.168.1.1, the +default LAN address. You can connect to a different address or IP with the +'connect' command. Note that your .netrc must contain correct user/password +credentials for the router. The entry corresponding to the defaults is: + +machine 192.168.1.1 + login "" + password admin + +Most commands queue up changes but don't actually send them to the Linksys. +You can force pending changes to be written with 'configure'. Otherwise, they +will be shipped to the Linksys at the end of session (e.g. when the program +running in batch mode encounters end-of-file or you type a control-D). If you +end the session with `quit', pending changes will be discarded. + +For more help, read the topics 'wan', 'lan', and 'wireless'.""") + + def help_lan(self): + print("""\ +The `lan_address' and `lan_netmask' commands let you set the IP location of +the Linksys on your LAN, or inside. Normally you'll want to leave these +untouched.""") + + def help_wan(self): + print("""\ +The WAN commands become significant if you are using the BEFSR41 or any of +the other Linksys boxes designed as DSL or cable-modem gateways. You will +need to use `wan_type' to declare how you expect to get your address. + +If your ISP has issued you a static address, you'll need to use the +`wan_address', `wan_netmask', and `wan_gateway' commands to set the address +of the router as seen from the WAN, the outside. In this case you will also +need to use the `dns' command to declare which remote servers your DNS +requests should be forwarded to. + +Some ISPs may require you to set host and domain for use with dynamic-address +allocation.""") + + def help_wireless(self): + print("""\ +The channel, ssid, ssid_broadcast, wep, and wireless commands control +wireless routing.""") + + def help_switches(self): + print("Switches may be turned on with 'on', 'enable', or 'yes'.") + print("Switches may be turned off with 'off', 'disable', or 'no'.") + print("Switch commands include: wireless, ssid_broadcast.") + + def help_addresses(self): + print("An address argument must be a valid IP address;") + print("four decimal numbers separated by dots, each ") + print("between 0 and 255.") + + def emptyline(self): + pass + + interpreter = LinksysInterpreter() + for arg in sys.argv[1:]: + interpreter.onecmd(arg) + fatal = False + while not fatal: + try: + interpreter.cmdloop() + fatal = True + except LinksysError, (message, fatal): + print "linksys:", message + +# The following sets edit modes for GNU EMACS +# Local Variables: +# mode:python +# End: diff --git a/_src/om2py4w/4wex0/examples/quickstart/follow_redirect.py b/_src/om2py4w/4wex0/examples/quickstart/follow_redirect.py new file mode 100644 index 000000000..9e3e3e423 --- /dev/null +++ b/_src/om2py4w/4wex0/examples/quickstart/follow_redirect.py @@ -0,0 +1,13 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +import pycurl + +c = pycurl.Curl() +# Redirects to https://www.python.org/. +c.setopt(c.URL, 'http://www.python.org/') +# Follow redirect. +c.setopt(c.FOLLOWLOCATION, True) +c.perform() +c.close() diff --git a/_src/om2py4w/4wex0/examples/quickstart/form_post.py b/_src/om2py4w/4wex0/examples/quickstart/form_post.py new file mode 100644 index 000000000..4eaf1daca --- /dev/null +++ b/_src/om2py4w/4wex0/examples/quickstart/form_post.py @@ -0,0 +1,25 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +import pycurl +try: + # python 3 + from urllib.parse import urlencode +except ImportError: + # python 2 + from urllib import urlencode + +c = pycurl.Curl() +c.setopt(c.URL, 'http://pycurl.sourceforge.net/tests/testpostvars.php') + +post_data = {'field': 'message'} +# Form data must be provided already urlencoded. +postfields = urlencode(post_data) +# Sets request method to POST, +# Content-Type header to application/x-www-form-urlencoded +# and data to send in request body. +c.setopt(c.POSTFIELDS, postfields) + +c.perform() +c.close() diff --git a/_src/om2py4w/4wex0/examples/quickstart/get.py b/_src/om2py4w/4wex0/examples/quickstart/get.py new file mode 100644 index 000000000..7322bb474 --- /dev/null +++ b/_src/om2py4w/4wex0/examples/quickstart/get.py @@ -0,0 +1,24 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +import pycurl +try: + from io import BytesIO +except ImportError: + from StringIO import StringIO as BytesIO + +buffer = BytesIO() +c = pycurl.Curl() +c.setopt(c.URL, 'http://pycurl.sourceforge.net/') +c.setopt(c.WRITEDATA, buffer) +# For older PycURL versions: +#c.setopt(c.WRITEFUNCTION, buffer.write) +c.perform() +c.close() + +body = buffer.getvalue() +# Body is a string on Python 2 and a byte string on Python 3. +# If we know the encoding, we can always decode the body and +# end up with a Unicode string. +print(body.decode('iso-8859-1')) diff --git a/_src/om2py4w/4wex0/examples/quickstart/get_python2.py b/_src/om2py4w/4wex0/examples/quickstart/get_python2.py new file mode 100644 index 000000000..5abd892cf --- /dev/null +++ b/_src/om2py4w/4wex0/examples/quickstart/get_python2.py @@ -0,0 +1,20 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +import pycurl +from StringIO import StringIO + +buffer = StringIO() +c = pycurl.Curl() +c.setopt(c.URL, 'http://pycurl.sourceforge.net/') +c.setopt(c.WRITEDATA, buffer) +# For older PycURL versions: +#c.setopt(c.WRITEFUNCTION, buffer.write) +c.perform() +c.close() + +body = buffer.getvalue() +# Body is a string in some encoding. +# In Python 2, we can print it without knowing what the encoding is. +print(body) \ No newline at end of file diff --git a/_src/om2py4w/4wex0/examples/quickstart/get_python3.py b/_src/om2py4w/4wex0/examples/quickstart/get_python3.py new file mode 100644 index 000000000..8d30245b1 --- /dev/null +++ b/_src/om2py4w/4wex0/examples/quickstart/get_python3.py @@ -0,0 +1,19 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +import pycurl +from io import BytesIO + +buffer = BytesIO() +c = pycurl.Curl() +c.setopt(c.URL, 'http://pycurl.sourceforge.net/') +c.setopt(c.WRITEDATA, buffer) +c.perform() +c.close() + +body = buffer.getvalue() +# Body is a byte string. +# We have to know the encoding in order to print it to a text file +# such as standard output. +print(body.decode('iso-8859-1')) diff --git a/_src/om2py4w/4wex0/examples/quickstart/out.html b/_src/om2py4w/4wex0/examples/quickstart/out.html new file mode 100644 index 000000000..113a8fa3b --- /dev/null +++ b/_src/om2py4w/4wex0/examples/quickstart/out.html @@ -0,0 +1,229 @@ + + + + + + + + PycURL Home Page + + + + + + + + + + + +
+ libcurl +
+ +
+
+ PYCURL 7.19.5.3
+ Nov 3 2015 - + Release Notes +
+
+ +

Quick Links

+ + + + +

Overview

+ +

+PycURL is a +Python interface to +libcurl. +PycURL can be used to fetch objects identified by a URL +from a Python program, similar to the +urllib Python module. +PycURL is mature, very fast, and supports a lot of features. +

+ + +
    +
  • + libcurl is a free and easy-to-use client-side URL transfer library, supporting FTP, FTPS, HTTP, HTTPS, SCP, SFTP, TFTP, TELNET, DICT, LDAP, LDAPS, FILE, IMAP, SMTP, POP3 and RTSP. libcurl supports SSL certificates, HTTP POST, HTTP PUT, FTP uploading, HTTP form based upload, proxies, cookies, user+password authentication (Basic, Digest, NTLM, Negotiate, Kerberos4), file transfer resume, http proxy tunneling and more! +
    +
    +
  • +
  • + libcurl is highly portable, it builds and works identically on numerous platforms, including Solaris, NetBSD, FreeBSD, OpenBSD, Darwin, HPUX, IRIX, AIX, Tru64, Linux, UnixWare, HURD, Windows, Amiga, OS/2, BeOs, Mac OS X, Ultrix, QNX, OpenVMS, RISC OS, Novell NetWare, DOS and more... +
    +
    +
  • +
  • + libcurl is + free, + thread-safe, + IPv6 compatible, + feature rich, + well supported, + fast, + thoroughly documented + and is already used by many known, big and successful companies + and numerous applications. +
  • + +
+ + +

Intended Audience

+ +

+PycURL is targeted at an advanced developer - if you need dozens of +concurrent, fast and reliable connections or any of the sophisticated +features listed above then PycURL is for you. +

+ +

+The main drawback of PycURL is that it is a relatively thin layer over +libcurl without any of those nice Pythonic class hierarchies. +This means it has a somewhat steep learning curve unless you +are already familiar with libcurl's C API. +

+ +

+To sum up, PycURL is very fast (especially for multiple concurrent operations) +and very feature rich, but has a somewhat complex interface. +If you need something simpler or prefer a pure Python +module you might want to check out +urllib2 +and +urlgrabber. +

+ + +

Documentation

+ +

+PycURL includes API documentation in the doc directory of the distribution, +as well as a number of test and example scripts in the tests +and examples +directories of the distribution. +

+ +

+The real info, though, is located in the +libcurl documentation, +most important being +curl_easy_setopt. +The +libcurl tutorial +also provides a lot of useful information. +

+ +

+For a quick start have a look at the high-performance URL downloader +retriever-multi.py. +

+ +

+For a list of changes consult the PycURL ChangeLog. +

+ + +

Download

+ +

+Download +PycURL sources version 7.19.5.3 (Nov 3 2015) or try +the code from +the Git repository. +

+ +

+You can get prebuilt Win32 modules as well as older versions from the +download area. +Please note that the prebuilt versions are provided for your +convenience only and are completely unsupported - use them +at your own risk. +

+ +

+Also, official PycURL packages are available for Ubuntu, +Debian GNU/Linux, FreeBSD, +Gentoo Linux, NetBSD, +and OpenBSD. +

+ + +

Community

+ +

+If you want to ask questions or discuss PycURL related issues, our +mailing list +is the place to be. +Mailing list +archives are available for your perusal. +

+ +

+Bugs and +patches are tracked +on GitHub. +If your patch or proposal is non-trivial, please discuss it on +the mailing list before submitting code. +Older bugs and patches can be found on the +issues and +pull requests pages +for the temporary Git import repository on Github, and on the +PycURL SourceForge +project page. +

+ +

+The libcurl library also has its own +mailing lists. +

+ + +

License

+ +Copyright (C) 2001-2008 Kjetil Jacobsen
+Copyright (C) 2001-2008 Markus F.X.J. Oberhumer
+Copyright (C) 2013-2015 Oleg Pudeyev
+
+PycURL is dual licensed under the LGPL and an MIT/X derivative license +based on the cURL license. You can redistribute and/or modify PycURL +according to the terms of either license. + +
+
+ + + + + + + + +
+ + Valid XHTML 1.0! +
+
+ + + Last modified Tue Nov 03 13:30:29 UTC 2015. + + + diff --git a/_src/om2py4w/4wex0/examples/quickstart/response_headers.py b/_src/om2py4w/4wex0/examples/quickstart/response_headers.py new file mode 100644 index 000000000..825c1f5f8 --- /dev/null +++ b/_src/om2py4w/4wex0/examples/quickstart/response_headers.py @@ -0,0 +1,68 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +import pycurl +import re +try: + from io import BytesIO +except ImportError: + from StringIO import StringIO as BytesIO + +headers = {} +def header_function(header_line): + # HTTP standard specifies that headers are encoded in iso-8859-1. + # On Python 2, decoding step can be skipped. + # On Python 3, decoding step is required. + header_line = header_line.decode('iso-8859-1') + + # Header lines include the first status line (HTTP/1.x ...). + # We are going to ignore all lines that don't have a colon in them. + # This will botch headers that are split on multiple lines... + if ':' not in header_line: + return + + # Break the header line into header name and value. + name, value = header_line.split(':', 1) + + # Remove whitespace that may be present. + # Header lines include the trailing newline, and there may be whitespace + # around the colon. + name = name.strip() + value = value.strip() + + # Header names are case insensitive. + # Lowercase name here. + name = name.lower() + + # Now we can actually record the header name and value. + headers[name] = value + +buffer = BytesIO() +c = pycurl.Curl() +c.setopt(c.URL, 'http://pycurl.sourceforge.net') +c.setopt(c.WRITEFUNCTION, buffer.write) +# Set our header function. +c.setopt(c.HEADERFUNCTION, header_function) +c.perform() +c.close() + +# Figure out what encoding was sent with the response, if any. +# Check against lowercased header name. +encoding = None +if 'content-type' in headers: + content_type = headers['content-type'].lower() + match = re.search('charset=(\S+)', content_type) + if match: + encoding = match.group(1) + print('Decoding using %s' % encoding) +if encoding is None: + # Default encoding for HTML is iso-8859-1. + # Other content types may have different default encoding, + # or in case of binary data, may have no encoding at all. + encoding = 'iso-8859-1' + print('Assuming encoding is %s' % encoding) + +body = buffer.getvalue() +# Decode using the encoding we figured out. +print(body.decode(encoding)) diff --git a/_src/om2py4w/4wex0/examples/quickstart/response_info.py b/_src/om2py4w/4wex0/examples/quickstart/response_info.py new file mode 100644 index 000000000..2417c1cf3 --- /dev/null +++ b/_src/om2py4w/4wex0/examples/quickstart/response_info.py @@ -0,0 +1,23 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +import pycurl +try: + from io import BytesIO +except ImportError: + from StringIO import StringIO as BytesIO + +buffer = BytesIO() +c = pycurl.Curl() +c.setopt(c.URL, 'http://pycurl.sourceforge.net/') +c.setopt(c.WRITEDATA, buffer) +c.perform() + +# HTTP response code, e.g. 200. +print('Status: %d' % c.getinfo(c.RESPONSE_CODE)) +# Elapsed time for the transfer. +print('Status: %f' % c.getinfo(c.TOTAL_TIME)) + +# getinfo must be called before close. +c.close() diff --git a/_src/om2py4w/4wex0/examples/quickstart/write_file.py b/_src/om2py4w/4wex0/examples/quickstart/write_file.py new file mode 100644 index 000000000..f4f9ee991 --- /dev/null +++ b/_src/om2py4w/4wex0/examples/quickstart/write_file.py @@ -0,0 +1,14 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +import pycurl + +# As long as the file is opened in binary mode, both Python 2 and Python 3 +# can write response body to it without decoding. +with open('out.html', 'wb') as f: + c = pycurl.Curl() + c.setopt(c.URL, 'http://pycurl.sourceforge.net/') + c.setopt(c.WRITEDATA, f) + c.perform() + c.close() diff --git a/_src/om2py4w/4wex0/examples/retriever-multi.py b/_src/om2py4w/4wex0/examples/retriever-multi.py new file mode 100644 index 000000000..2daff11ec --- /dev/null +++ b/_src/om2py4w/4wex0/examples/retriever-multi.py @@ -0,0 +1,121 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +# +# Usage: python retriever-multi.py [<# of +# concurrent connections>] +# + +import sys +import pycurl + +# We should ignore SIGPIPE when using pycurl.NOSIGNAL - see +# the libcurl tutorial for more info. +try: + import signal + from signal import SIGPIPE, SIG_IGN + signal.signal(signal.SIGPIPE, signal.SIG_IGN) +except ImportError: + pass + + +# Get args +num_conn = 10 +try: + if sys.argv[1] == "-": + urls = sys.stdin.readlines() + else: + urls = open(sys.argv[1]).readlines() + if len(sys.argv) >= 3: + num_conn = int(sys.argv[2]) +except: + print("Usage: %s [<# of concurrent connections>]" % sys.argv[0]) + raise SystemExit + + +# Make a queue with (url, filename) tuples +queue = [] +for url in urls: + url = url.strip() + if not url or url[0] == "#": + continue + filename = "doc_%03d.dat" % (len(queue) + 1) + queue.append((url, filename)) + + +# Check args +assert queue, "no URLs given" +num_urls = len(queue) +num_conn = min(num_conn, num_urls) +assert 1 <= num_conn <= 10000, "invalid number of concurrent connections" +print("PycURL %s (compiled against 0x%x)" % (pycurl.version, pycurl.COMPILE_LIBCURL_VERSION_NUM)) +print("----- Getting", num_urls, "URLs using", num_conn, "connections -----") + + +# Pre-allocate a list of curl objects +m = pycurl.CurlMulti() +m.handles = [] +for i in range(num_conn): + c = pycurl.Curl() + c.fp = None + c.setopt(pycurl.FOLLOWLOCATION, 1) + c.setopt(pycurl.MAXREDIRS, 5) + c.setopt(pycurl.CONNECTTIMEOUT, 30) + c.setopt(pycurl.TIMEOUT, 300) + c.setopt(pycurl.NOSIGNAL, 1) + m.handles.append(c) + + +# Main loop +freelist = m.handles[:] +num_processed = 0 +while num_processed < num_urls: + # If there is an url to process and a free curl object, add to multi stack + while queue and freelist: + url, filename = queue.pop(0) + c = freelist.pop() + c.fp = open(filename, "wb") + c.setopt(pycurl.URL, url) + c.setopt(pycurl.WRITEDATA, c.fp) + m.add_handle(c) + # store some info + c.filename = filename + c.url = url + # Run the internal curl state machine for the multi stack + while 1: + ret, num_handles = m.perform() + if ret != pycurl.E_CALL_MULTI_PERFORM: + break + # Check for curl objects which have terminated, and add them to the freelist + while 1: + num_q, ok_list, err_list = m.info_read() + for c in ok_list: + c.fp.close() + c.fp = None + m.remove_handle(c) + print("Success:", c.filename, c.url, c.getinfo(pycurl.EFFECTIVE_URL)) + freelist.append(c) + for c, errno, errmsg in err_list: + c.fp.close() + c.fp = None + m.remove_handle(c) + print("Failed: ", c.filename, c.url, errno, errmsg) + freelist.append(c) + num_processed = num_processed + len(ok_list) + len(err_list) + if num_q == 0: + break + # Currently no more I/O is pending, could do something in the meantime + # (display a progress bar, etc.). + # We just call select() to sleep until some more data is available. + m.select(1.0) + + +# Cleanup +for c in m.handles: + if c.fp is not None: + c.fp.close() + c.fp = None + c.close() +m.close() + diff --git a/_src/om2py4w/4wex0/examples/retriever.py b/_src/om2py4w/4wex0/examples/retriever.py new file mode 100644 index 000000000..7cabea3e5 --- /dev/null +++ b/_src/om2py4w/4wex0/examples/retriever.py @@ -0,0 +1,98 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +# +# Usage: python retriever.py [<# of +# concurrent connections>] +# + +import sys, threading, Queue +import pycurl + +# We should ignore SIGPIPE when using pycurl.NOSIGNAL - see +# the libcurl tutorial for more info. +try: + import signal + from signal import SIGPIPE, SIG_IGN + signal.signal(signal.SIGPIPE, signal.SIG_IGN) +except ImportError: + pass + + +# Get args +num_conn = 10 +try: + if sys.argv[1] == "-": + urls = sys.stdin.readlines() + else: + urls = open(sys.argv[1]).readlines() + if len(sys.argv) >= 3: + num_conn = int(sys.argv[2]) +except: + print("Usage: %s [<# of concurrent connections>]" % sys.argv[0]) + raise SystemExit + + +# Make a queue with (url, filename) tuples +queue = Queue.Queue() +for url in urls: + url = url.strip() + if not url or url[0] == "#": + continue + filename = "doc_%03d.dat" % (len(queue.queue) + 1) + queue.put((url, filename)) + + +# Check args +assert queue.queue, "no URLs given" +num_urls = len(queue.queue) +num_conn = min(num_conn, num_urls) +assert 1 <= num_conn <= 10000, "invalid number of concurrent connections" +print("PycURL %s (compiled against 0x%x)" % (pycurl.version, pycurl.COMPILE_LIBCURL_VERSION_NUM)) +print("----- Getting", num_urls, "URLs using", num_conn, "connections -----") + + +class WorkerThread(threading.Thread): + def __init__(self, queue): + threading.Thread.__init__(self) + self.queue = queue + + def run(self): + while 1: + try: + url, filename = self.queue.get_nowait() + except Queue.Empty: + raise SystemExit + fp = open(filename, "wb") + curl = pycurl.Curl() + curl.setopt(pycurl.URL, url) + curl.setopt(pycurl.FOLLOWLOCATION, 1) + curl.setopt(pycurl.MAXREDIRS, 5) + curl.setopt(pycurl.CONNECTTIMEOUT, 30) + curl.setopt(pycurl.TIMEOUT, 300) + curl.setopt(pycurl.NOSIGNAL, 1) + curl.setopt(pycurl.WRITEDATA, fp) + try: + curl.perform() + except: + import traceback + traceback.print_exc(file=sys.stderr) + sys.stderr.flush() + curl.close() + fp.close() + sys.stdout.write(".") + sys.stdout.flush() + + +# Start a bunch of threads +threads = [] +for dummy in range(num_conn): + t = WorkerThread(queue) + t.start() + threads.append(t) + + +# Wait for all threads to finish +for thread in threads: + thread.join() diff --git a/_src/om2py4w/4wex0/examples/sfquery.py b/_src/om2py4w/4wex0/examples/sfquery.py new file mode 100644 index 000000000..b0836d019 --- /dev/null +++ b/_src/om2py4w/4wex0/examples/sfquery.py @@ -0,0 +1,65 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et +# +# sfquery -- Source Forge query script using the ClientCGI high-level interface +# +# Retrieves a SourceForge XML export object for a given project. +# Specify the *numeric* project ID. the user name, and the password, +# as arguments. If you have a valid ~/.netrc entry for sourceforge.net, +# you can just give the project ID. +# +# By Eric S. Raymond, August 2002. All rites reversed. + +import os, sys, netrc +import curl + +class SourceForgeUserSession(curl.Curl): + # SourceForge-specific methods. Sensitive to changes in site design. + def login(self, name, password): + "Establish a login session." + self.post("account/login.php", (("form_loginname", name), + ("form_pw", password), + ("return_to", ""), + ("stay_in_ssl", "1"), + ("login", "Login With SSL"))) + def logout(self): + "Log out of SourceForge." + self.get("account/logout.php") + def fetch_xml(self, numid): + self.get("export/xml_export.php?group_id=%s" % numid) + +if __name__ == "__main__": + if len(sys.argv) == 1: + project_id = '28236' # PyCurl project ID + else: + project_id = sys.argv[1] + # Try to grab authenticators out of your .netrc + try: + auth = netrc.netrc().authenticators("sourceforge.net") + name, account, password = auth + except: + if len(sys.argv) < 4: + print("Usage: %s " % sys.argv[0]) + raise SystemExit + name = sys.argv[2] + password = sys.argv[3] + session = SourceForgeUserSession("https://sourceforge.net/") + session.set_verbosity(0) + session.login(name, password) + # Login could fail. + if session.answered("Invalid Password or User Name"): + sys.stderr.write("Login/password not accepted (%d bytes)\n" % len(session.body())) + sys.exit(1) + # We'll see this if we get the right thing. + elif session.answered("Personal Page For: " + name): + session.fetch_xml(project_id) + sys.stdout.write(session.body()) + session.logout() + sys.exit(0) + # Or maybe SourceForge has changed its site design so our check strings + # are no longer valid. + else: + sys.stderr.write("Unexpected page (%d bytes)\n"%len(session.body())) + sys.exit(1) + diff --git a/_src/om2py4w/4wex0/examples/smtp.py b/_src/om2py4w/4wex0/examples/smtp.py new file mode 100644 index 000000000..2b7ce7745 --- /dev/null +++ b/_src/om2py4w/4wex0/examples/smtp.py @@ -0,0 +1,39 @@ +# Based on the simple libcurl SMTP example: +# https://github.com/bagder/curl/blob/master/docs/examples/smtp-mail.c +# There are other SMTP examples in that directory that you may find helpful. + +import pycurl +try: + from io import BytesIO +except ImportError: + from StringIO import StringIO as BytesIO + +mail_server = 'smtp://localhost' +mail_from = 'sender@example.org' +mail_to = 'addressee@example.net' + +c = pycurl.Curl() +c.setopt(c.URL, mail_server) +c.setopt(c.MAIL_FROM, mail_from) +c.setopt(c.MAIL_RCPT, [mail_to]) + +message = '''\ +From: %s +To: %s +Subject: PycURL SMTP example + +SMTP example via PycURL +''' % (mail_from, mail_to) + +# libcurl does not perform buffering, therefore +# we need to wrap the message string into a BytesIO or StringIO. +io = BytesIO(message) +c.setopt(c.READDATA, io) + +# If UPLOAD is not set, libcurl performs SMTP VRFY. +# Setting UPLOAD to True sends a message. +c.setopt(c.UPLOAD, True) + +# Observe SMTP conversation. +c.setopt(c.VERBOSE, True) +c.perform() diff --git a/_src/om2py4w/4wex0/examples/tests/test_build_config.py b/_src/om2py4w/4wex0/examples/tests/test_build_config.py new file mode 100644 index 000000000..1b9399109 --- /dev/null +++ b/_src/om2py4w/4wex0/examples/tests/test_build_config.py @@ -0,0 +1,65 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +import pycurl +import zlib +try: + from io import BytesIO +except ImportError: + try: + from cStringIO import StringIO as BytesIO + except ImportError: + from StringIO import StringIO as BytesIO + +c = pycurl.Curl() +c.setopt(c.URL, 'http://pycurl.sourceforge.net') +#c.setopt(c.ENCODING, 'deflate') +c.setopt(c.HTTPHEADER, ['Accept-Encoding: deflate']) +body = BytesIO() +c.setopt(c.WRITEFUNCTION, body.write) +encoding_found = False +def header_function(header): + global encoding_found + if header.decode('iso-8859-1').lower().startswith('content-encoding: deflate'): + encoding_found = True +c.setopt(c.HEADERFUNCTION, header_function) +c.perform() +assert encoding_found +print('Server supports deflate encoding') +encoded = body.getvalue() +# should not raise exceptions +zlib.decompress(encoded, -zlib.MAX_WBITS) +print('Server served deflated body') + +c.reset() +c.setopt(c.URL, 'http://pycurl.sourceforge.net') +c.setopt(c.ENCODING, 'deflate') +body = BytesIO() +c.setopt(c.WRITEFUNCTION, body.write) +encoding_found = False +def header_function(header): + global encoding_found + if header.decode('iso-8859-1').lower().startswith('content-encoding: deflate'): + encoding_found = True +c.setopt(c.HEADERFUNCTION, header_function) +c.perform() +assert encoding_found +print('Server claimed deflate encoding as expected') +# body should be decoded +encoded = body.getvalue() +if '= 1.0: self.round = 0.0 + else: + self.round = float(download_d) / float(download_t) + gtk.threads_enter() + self.pbar.set_fraction(self.round) + gtk.threads_leave() + + def mainloop(self): + gtk.threads_enter() + gtk.main() + gtk.threads_leave() + + def close_app(self, *args): + args[0].destroy() + gtk.main_quit() + + +class Test(threading.Thread): + def __init__(self, url, target_file, progress): + threading.Thread.__init__(self) + self.target_file = target_file + self.progress = progress + self.curl = pycurl.Curl() + self.curl.setopt(pycurl.URL, url) + self.curl.setopt(pycurl.WRITEDATA, self.target_file) + self.curl.setopt(pycurl.FOLLOWLOCATION, 1) + self.curl.setopt(pycurl.NOPROGRESS, 0) + self.curl.setopt(pycurl.PROGRESSFUNCTION, self.progress) + self.curl.setopt(pycurl.MAXREDIRS, 5) + self.curl.setopt(pycurl.NOSIGNAL, 1) + + def run(self): + self.curl.perform() + self.curl.close() + self.target_file.close() + self.progress(1.0, 1.0, 0, 0) + + +# Check command line args +if len(sys.argv) < 3: + print("Usage: %s " % sys.argv[0]) + raise SystemExit + +# Make a progress bar window +p = ProgressBar(sys.argv[1]) +# Start thread for fetching url +Test(sys.argv[1], open(sys.argv[2], 'wb'), p.progress).start() +# Enter the GTK mainloop +gtk.threads_init() +try: + p.mainloop() +except KeyboardInterrupt: + pass diff --git a/_src/om2py4w/4wex0/examples/tests/test_xmlrpc.py b/_src/om2py4w/4wex0/examples/tests/test_xmlrpc.py new file mode 100644 index 000000000..9fb0f94a6 --- /dev/null +++ b/_src/om2py4w/4wex0/examples/tests/test_xmlrpc.py @@ -0,0 +1,31 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +## XML-RPC lib included in python2.2 +try: + import xmlrpclib +except ImportError: + import xmlrpc.client as xmlrpclib +import pycurl + +# Header fields passed in request +xmlrpc_header = [ + "User-Agent: PycURL XML-RPC Test", "Content-Type: text/xml" + ] + +# XML-RPC request template +xmlrpc_template = """ +%s%s +""" + +# Engage +c = pycurl.Curl() +c.setopt(c.URL, 'http://betty.userland.com/RPC2') +c.setopt(c.POST, 1) +c.setopt(c.HTTPHEADER, xmlrpc_header) +c.setopt(c.POSTFIELDS, xmlrpc_template % ("examples.getStateName", xmlrpclib.dumps((5,)))) + +print('Response from http://betty.userland.com/') +c.perform() +c.close() diff --git a/_src/om2py4w/4wex0/examples/xmlrpc_curl.py b/_src/om2py4w/4wex0/examples/xmlrpc_curl.py new file mode 100644 index 000000000..3cdb2d3da --- /dev/null +++ b/_src/om2py4w/4wex0/examples/xmlrpc_curl.py @@ -0,0 +1,68 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +# We should ignore SIGPIPE when using pycurl.NOSIGNAL - see +# the libcurl tutorial for more info. +try: + import signal + from signal import SIGPIPE, SIG_IGN + signal.signal(signal.SIGPIPE, signal.SIG_IGN) +except ImportError: + pass +try: + from cStringIO import StringIO +except ImportError: + try: + from StringIO import StringIO + except ImportError: + from io import StringIO +try: + import xmlrpclib +except ImportError: + import xmlrpc.client as xmlrpclib +import pycurl + + +class CURLTransport(xmlrpclib.Transport): + """Handles a cURL HTTP transaction to an XML-RPC server.""" + + xmlrpc_h = [ "Content-Type: text/xml" ] + + def __init__(self, username=None, password=None): + self.c = pycurl.Curl() + self.c.setopt(pycurl.POST, 1) + self.c.setopt(pycurl.NOSIGNAL, 1) + self.c.setopt(pycurl.CONNECTTIMEOUT, 30) + self.c.setopt(pycurl.HTTPHEADER, self.xmlrpc_h) + if username != None and password != None: + self.c.setopt(pycurl.USERPWD, '%s:%s' % (username, password)) + self._use_datetime = False + + def request(self, host, handler, request_body, verbose=0): + b = StringIO() + self.c.setopt(pycurl.URL, 'http://%s%s' % (host, handler)) + self.c.setopt(pycurl.POSTFIELDS, request_body) + self.c.setopt(pycurl.WRITEFUNCTION, b.write) + self.c.setopt(pycurl.VERBOSE, verbose) + self.verbose = verbose + try: + self.c.perform() + except pycurl.error, v: + raise xmlrpclib.ProtocolError( + host + handler, + v[0], v[1], None + ) + b.seek(0) + return self.parse_response(b) + + +if __name__ == "__main__": + ## Test + server = xmlrpclib.ServerProxy("http://betty.userland.com", + transport=CURLTransport()) + print(server) + try: + print(server.examples.getStateName(41)) + except xmlrpclib.Error, v: + print("ERROR", v) diff --git a/_src/om2py4w/4wex0/webserver.py b/_src/om2py4w/4wex0/webserver.py new file mode 100644 index 000000000..5c5221d03 --- /dev/null +++ b/_src/om2py4w/4wex0/webserver.py @@ -0,0 +1,30 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Jeremiah Zhang +from bottle import * +import sys + +@route('/') +@route('/write', method="GET") +def input_diary(): + + if request.GET.get('save','').strip(): + diary_words = request.GET.get('words','').strip() + diary_name = 'Diary.log' # if not exist then creat + diary_file = open(diary_name, 'a+') + diary_file.write(diary_words + '\n') # write words your + diary_file.close() + diary_file = open(diary_name, 'r') + diary_content = diary_file.read() + diary_file.close() + return template('write_words.tpl', content=diary_content) + + else: + diary_name = 'Diary.log' + diary_file = open(diary_name, 'r') + diary_content = diary_file.read() + diary_file.close() + return template('write_words.tpl', content=diary_content) + +if __name__ == '__main__': + run(host='localhost', port=8010, debug=True, reloader=1) # switched debug off for publich applocations \ No newline at end of file diff --git a/_src/om2py4w/4wex0/write_words.tpl b/_src/om2py4w/4wex0/write_words.tpl new file mode 100644 index 000000000..3799ffa90 --- /dev/null +++ b/_src/om2py4w/4wex0/write_words.tpl @@ -0,0 +1,12 @@ + + + +

Write new words into your diary.log:

+ + + + +

Darling!Here is your diary content!

+ + + \ No newline at end of file diff --git a/_src/om2py4w/4wex2/squars_table.py b/_src/om2py4w/4wex2/squars_table.py new file mode 100644 index 000000000..946221b0f --- /dev/null +++ b/_src/om2py4w/4wex2/squars_table.py @@ -0,0 +1,3 @@ +# conding:utf-8 +for x in range(1, 11): + print repr(x).rjust(2), repr(x*x).rjust(3), repr(x*x*x).rjust(4) \ No newline at end of file diff --git a/_src/om2py4w/README.md b/_src/om2py4w/README.md index a2a1c659a..8e9518e6e 100644 --- a/_src/om2py4w/README.md +++ b/_src/om2py4w/README.md @@ -1,8 +1,34 @@ -# OMOOC.py 周任务代码试作 +# 4w 日志交互web版 -## 4w +- Ubuntu14.04 LTS + 其自带终端 +- SublimeText2 (ST2) +- Python 2.7.10 +- Firefox 41.0.2 for Ubuntu -- 私人笔记: - + SAE 发布服务 - + web 页面端口 - + 用户认证 \ No newline at end of file +## web 端 网页访问 +- python 运行:webserver.py + - 请将 write_words.tpl 保存在 与 webserver.py 同一个文件目录下 +- 浏览器 访问:http://localhost:8010/write + - text 中书写日志 + - text area 返回上一次输入内容 +- 效果 +![04webserverresult](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/04webserverresult.jpg) + +## CLI 交互 + +- python 运行 webserver.py +- python 运行 CLI.py +- 效果 + +![CLI07result.jpg](https://raw.githubusercontent.com/JeremiahZhang/OMOOC2py/master/_image/CLI07result.jpg) + + +## 教程 + +- [教程](https://jeremiahzhang.gitbooks.io/omooc2py/content/2nDev/week04_web_way.html) + +## 代码 + +- [webserver.py](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py4w/4wex0/webserver.py) +- [CLI.py](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py4w/4wex0/CLI.py) +- [write_words.tpl](https://github.com/JeremiahZhang/OMOOC2py/blob/master/_src/om2py4w/4wex0/write_words.tpl) \ No newline at end of file diff --git a/_src/om2py4w/st3test.py b/_src/om2py4w/st3test.py new file mode 100644 index 000000000..60f781002 --- /dev/null +++ b/_src/om2py4w/st3test.py @@ -0,0 +1,2 @@ +print ('I love ' + 'python') \ No newline at end of file diff --git a/_src/om2py4w/todo/DB.py b/_src/om2py4w/todo/DB.py new file mode 100644 index 000000000..2dbd22dd1 --- /dev/null +++ b/_src/om2py4w/todo/DB.py @@ -0,0 +1,8 @@ +import sqlite3 +con = sqlite3.connect('todo.db') # Warning: This file is created in the current directory +con.execute("CREATE TABLE todo (id INTEGER PRIMARY KEY, task char(100) NOT NULL, status bool NOT NULL)") +con.execute("INSERT INTO todo (task,status) VALUES ('Read A-byte-of-python to get a good introduction into Python',0)") +con.execute("INSERT INTO todo (task,status) VALUES ('Visit the Python website',1)") +con.execute("INSERT INTO todo (task,status) VALUES ('Test various editors for and check the syntax highlighting',1)") +con.execute("INSERT INTO todo (task,status) VALUES ('Choose your favorite WSGI-Framework',0)") +con.commit() \ No newline at end of file diff --git a/_src/om2py4w/todo/demoBottle.py b/_src/om2py4w/todo/demoBottle.py new file mode 100644 index 000000000..e391837e8 --- /dev/null +++ b/_src/om2py4w/todo/demoBottle.py @@ -0,0 +1,47 @@ +# coding=utf-8 +from bottle import route, run, template + +@route('/hello/') +def index(name): + return template('Hello {{name}}', name=name) + +run(host='localhost', port=8080) + + + + +@get('/login') # or @route('/login') +def login(): + return """ +
+ Username: + Password: + + """ + +@post('/login') # or @route('/login', method='POST') +def do_login(): + username = request.forms.get('username') + password = request.forms.get('password') + if check_login(username, password): + return "

Your login information was correct.

" + else: + return "

Login failed.

" + + +"""@route('/hello/') +def greet(name='Stranger'): + return template('Hello {{name}}, how are you?', name=name) + +@route('/wiki/') #matches /wiki/Leraning Python +def show_wiki_page(pagename): + pass + +@route('//') # matches /follow/defnull +def user_api(action, user): + pass""" + +""" +def hello(): + return "Jeremiah Hello!" +""" \ No newline at end of file diff --git a/_src/om2py4w/todo/diary_beta_2.py b/_src/om2py4w/todo/diary_beta_2.py new file mode 100644 index 000000000..f17e960bd --- /dev/null +++ b/_src/om2py4w/todo/diary_beta_2.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +import sys, os, glob +from time import localtime, strftime + +reload(sys) # 必须 reload +sys.setdefaultencoding('utf-8') + +def main(): + keywords = sys.argv[1:] # 调用外部数据 + for words in keywords: + print words, + print "\n" + + read_diary() + + print welcome_statement.__doc__ + + write_diary() + +def welcome_statement(): + """ # Dear, welcome abord in Diary Ship + + 1. Please input your words as following prompt + 2. Wanna Leave? Please input the word "end" alone + 3. Thanks, Hope you have fun in writing. > < + + Let's Start. GO + """ + +def read_diary(): + current_dir = os.getcwd() # 打印之前日志 + os.chdir(current_dir) + + for file in glob.glob("*.txt"): + print(file) + file_content = open(file, "r") + print file_content.read() + "\n" + +def write_diary(): + done = False + textInput = "" + + diary_name = raw_input("请输入日志名: ".encode(sys.stdout.encoding)) + ".txt" + diary_writer = open(diary_name, "w") + + while (done==False): + nextInput = raw_input("Please input your diary words: ") + if nextInput == "end": + inputDate = ask_date("Wanna add diary date time? yes or no! ") + diary_writer.write("\n" + inputDate) + break + else: + textInput += nextInput + "\n" + diary_writer.write(nextInput + "\n") + + diary_writer.close() + print ("Here is your " + inputDate +" diary: " + textInput) + +def ask_date(prompt): # 是否添加时间 + yes_list = ['yes', 'yep', 'ye', 'y', 'YES','YEP', 'YE', 'Y'] + no_list = ['no', 'n', 'NO', 'N'] + + ok = raw_input(prompt) + if ok in yes_list: + your_datetime = strftime("%Y-%m-%d %H:%M:%S", localtime()) + if ok in no_list: + your_datetime = "\n" + + return your_datetime + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/_src/om2py4w/todo/edit_task.tpl b/_src/om2py4w/todo/edit_task.tpl new file mode 100644 index 000000000..f66991d59 --- /dev/null +++ b/_src/om2py4w/todo/edit_task.tpl @@ -0,0 +1,12 @@ +%#template for editing a task +%#The template expects to receive a value for 'no' as well a 'old', the text of t +

Edit the task with ID = {{no}}

+ + + +
+ + \ No newline at end of file diff --git a/_src/om2py4w/todo/make_table.tpl b/_src/om2py4w/todo/make_table.tpl new file mode 100644 index 000000000..87811a264 --- /dev/null +++ b/_src/om2py4w/todo/make_table.tpl @@ -0,0 +1,11 @@ +%#template to generate a HTML table from a list of tuples (or list of lists, or tuple of tuples or ...) +

The open items are as follows:

+ +%for row in rows: + + %for col in row: + + %end + +%end +
{{col}}
\ No newline at end of file diff --git a/_src/om2py4w/todo/new_task.tpl b/_src/om2py4w/todo/new_task.tpl new file mode 100644 index 000000000..060280243 --- /dev/null +++ b/_src/om2py4w/todo/new_task.tpl @@ -0,0 +1,6 @@ +%#template for the form for a new task +

Add a new task to the ToDo list:

+
+ + +
\ No newline at end of file diff --git a/_src/om2py4w/todo/todo.db b/_src/om2py4w/todo/todo.db new file mode 100644 index 000000000..158909bcd Binary files /dev/null and b/_src/om2py4w/todo/todo.db differ diff --git a/_src/om2py4w/todo/todo.py b/_src/om2py4w/todo/todo.py new file mode 100644 index 000000000..63cd70269 --- /dev/null +++ b/_src/om2py4w/todo/todo.py @@ -0,0 +1,101 @@ +import sqlite3 +from bottle import route, run, template, request, static_file, error + +@route('/todo') +@route('/my_todo_list') +def todo_list(): + conn = sqlite3.connect("todo.db") + c = conn.cursor() + c.execute("SELECT id, task FROM todo WHERE status LIKE '1'") + result = c.fetchall() + c.close() + output = template('make_table', rows=result) + return output + +@route('/new', method='GET') +def new_item(): + + if request.GET.get('save','').strip(): + + save = request.GET.get('save','').strip() + new = request.GET.get('task', '').strip() + conn = sqlite3.connect('todo.db') + c = conn.cursor() + + c.execute("INSERT INTO todo (task,status) VALUES (?,?)", (new,1)) + new_id = c.lastrowid + + conn.commit() + c.close() + + return '

The new task was inserted into the database, the ID is %s, save value is %s

' % (new_id, save) + else: + return template('new_task.tpl') + +@route('/edit/', method='GET') +def edit_item(no): + + if request.GET.get('save','').strip(): + edit = request.GET.get('task','').strip() + status = request.GET.get('status','').strip() + + if status == 'open': + status = 1 + else: + status = 0 + + conn = sqlite3.connect('todo.db') + c = conn.cursor() + c.execute("UPDATE todo SET task = ?, status = ? WHERE id LIKE ?", (edit, status, no)) + conn.commit() + + return '

The item number %s was successfully updated

' % str(no) + + else: + conn = sqlite3.connect('todo.db') + c = conn.cursor() + c.execute("SELECT task FROM todo WHERE id LIKE ?", (str(no))) + cur_data = c.fetchone() + + return template('edit_task.tpl', old=cur_data, no=no) + +@route('/item') +def show_item(item): + conn = sqlite3.connect('todo.db') + c = conn.cursor() + c.execute("SELECT task FROM todo WHERE id LIKE ?", (item)) + result = c.fetchall() + c.close() + + if not result: + return "this item number does not exist!" + else: + return 'task: %s' % result[0] + +@route('/help') +def help(): + return static_file('help.html', root='/path/to/file') # help.html file in the root path + +@route('/json') +def show_json(json): + conn = sqlite3.connect('todo.db') + c = conn.cursor() + c.execute("SELECT task FROM todo WHERE id LIKE ?", (json)) + result = c.fetchall() + c.close() + + if not result: + return {'task': 'this item number doesnot exist!'} + else: + return {'task': result[0]} + +@error(403) +def mistake403(code): + return 'This is something wrong!' + +@error(404) +def mistake404(code): + return 'Sorry! Page does not exist!' + + +run(host='localhost', port=8090, debug=True, reloader=True) \ No newline at end of file diff --git a/_src/om2py5w/5wex0/Diary.log~ b/_src/om2py5w/5wex0/Diary.log~ new file mode 100644 index 000000000..f330f77a8 --- /dev/null +++ b/_src/om2py5w/5wex0/Diary.log~ @@ -0,0 +1,13 @@ +Bottle 和 SAE 结合好了 可是时间还没有显示 +刚开始没有配置 这下写了 并显示了 恩 现在添加时间 +这里下一步要实现 时间倒序 +还要添加时间 +还是先 公网访问 恩 +公网访问 出现一点问题 +如何改进呢? +问题是 无法用公网访问 虽然 push 到 sae的git 库了 +再接再厉 +再接再厉 +ok +公网访问成功 但是 出现问题是 在这里写入的时候 就不行了 +这就说明 本地测试 和 sae deploy 是有区别的 恩 diff --git a/_src/om2py5w/5wex0/README.md b/_src/om2py5w/5wex0/README.md new file mode 100644 index 000000000..0d05d5f47 --- /dev/null +++ b/_src/om2py5w/5wex0/README.md @@ -0,0 +1,22 @@ +# 极简日志 公网版 + +APP部署于Sina SAE, 域名: http://jeremiahzhang.sinaapp.com/ + +- 网页端 + - ==tag here== 输入框中 键入 tag 回车 + - 网页显示过去日志 + - ==write== 输入框中 键入 你的日志 + - 问题: + - tag 标签 暂时没有想出办法处理 + - 数据备份 +- CLI + - 在 runCLI.py 所在文件目录下 使用 python + + python runCLI.py + + - 待改进 + - 读取 History logs 的 url 得修改(需在服务端修改 index.wsgi + - tag 分类管理尚未解决 + +- [代码](https://github.com/JeremiahZhang/OMOOC2py/tree/master/_src/om2py5w/5wex0) +- [私人笔记](https://jeremiahzhang.gitbooks.io/omooc2py/content/2nDev/week05_paas.html) \ No newline at end of file diff --git a/_src/om2py5w/5wex0/config.yaml b/_src/om2py5w/5wex0/config.yaml new file mode 100644 index 000000000..c80b1409f --- /dev/null +++ b/_src/om2py5w/5wex0/config.yaml @@ -0,0 +1,2 @@ +name: jeremiahzhang +version: 1 \ No newline at end of file diff --git a/_src/om2py5w/5wex0/index.wsgi b/_src/om2py5w/5wex0/index.wsgi new file mode 100644 index 000000000..ac5f5cc11 --- /dev/null +++ b/_src/om2py5w/5wex0/index.wsgi @@ -0,0 +1,52 @@ +# coding:utf-8 +import sys +reload(sys) +sys.setdefaultencoding('utf-8') + +from bottle import * +import sae +import time +import sae.kvdb +import os +import jinja2 + +count = 0 + +kv = sae.kvdb.Client() + +def _save_to_kvdb(post): + + global count + count += 1 + logtime = time.ctime() + key = 'key' + str(count) + value = {'time':logtime, 'content':post} + kv.set(key, value) # 设置key的值为value + +def _get_datainkvdb(): + results = [] + for item in kv.get_by_prefix('key'): + results.append(item[1]) + sort_results = sorted(results, key=lambda dict_value: dict_value['time'], reverse=True) + return sort_results + +app = Bottle() + +@app.route('/') +def tag(): + return jinja2_template('tag.html') + +@app.route('/', method='POST') +def get_tag(): + get_tag = request.forms.get('addtag') + histlogs = _get_datainkvdb() + return jinja2_template('write.html', log=histlogs) + +@app.route('/', method='POST') +def write(): + log_data = request.forms.get('addlogs') + _save_to_kvdb(log_data) + histlogs = _get_datainkvdb() + return jinja2_template('write.html', log=histlogs) + +application = sae.create_wsgi_app(app) \ No newline at end of file diff --git a/_src/om2py5w/5wex0/runCLI.py b/_src/om2py5w/5wex0/runCLI.py new file mode 100644 index 000000000..4e53b0d36 --- /dev/null +++ b/_src/om2py5w/5wex0/runCLI.py @@ -0,0 +1,50 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +import requests +from bs4 import BeautifulSoup +import sys + +def help(): + """ # Welcome This is help doc: + + 1. Quit Please Type : exit /q/quit/Q + 2. See Help Document Type: help/H/h/? + 3. See Diary Histroy Type: hist + + Let's Start. GO""" + +def hist_logs(): + html_doc = requests.get('http://jeremiahzhang.sinaapp.com') # html_doc.text is the content of html + soup = BeautifulSoup(html_doc.text) # html + for item in soup.find_all('p'): + print item.get_text() + +def enter_tag(your_tag): + tag_data = {'addtag': your_tag, 'save': 'save'} # two input in write_words.tpl so you must add name='save' item + requests.post('http://jeremiahzhang.sinaapp.com', data=tag_data) + +def input_logs(your_log): + log_data={'addtags': your_log, 'save': 'save'} + requests.post('http://jeremiahzhang.sinaapp.com', data=log_data) + +def main(): + + print help.__doc__ + + tag = raw_input('Type your tag:-->$') + enter_tag(tag) + + while True: + yourwords = raw_input('Type your words--->$') + + if yourwords in {'exit', 'q', 'quit','Q'}: + sys.exit() + elif yourwords in {'help', 'H','h','?'}: + print help.__doc__ + elif yourwords == 'hist': + hist_logs() + else: + input_logs(yourwords) # write new diarys or logs + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/_src/om2py5w/5wex0/template/layout.html b/_src/om2py5w/5wex0/template/layout.html new file mode 100644 index 000000000..b76a5d602 --- /dev/null +++ b/_src/om2py5w/5wex0/template/layout.html @@ -0,0 +1,55 @@ + + + + 雷雨 + + + + + + + + +
+ {% block welcome %}{% endblock %} + {% block showlogs %}{% endblock %} +
+ + + + + \ No newline at end of file diff --git a/_src/om2py5w/5wex0/template/tag.html b/_src/om2py5w/5wex0/template/tag.html new file mode 100644 index 000000000..9c91449da --- /dev/null +++ b/_src/om2py5w/5wex0/template/tag.html @@ -0,0 +1,14 @@ +{% extends "layout.html" %} +{% block tag %} + +{% endblock %} + +{% block welcome %} +

Welcome to your diary!

+

Tag first

+{% endblock %} \ No newline at end of file diff --git a/_src/om2py5w/5wex0/template/write.html b/_src/om2py5w/5wex0/template/write.html new file mode 100644 index 000000000..358aa5863 --- /dev/null +++ b/_src/om2py5w/5wex0/template/write.html @@ -0,0 +1,17 @@ +{% extends "layout.html" %} +{% block writelogs %} + +{% endblock %} + +{% block showlogs %} +

Log History

+ {% for mylog in log %} + {{mylog['content']}}
+

{{mylog['time']}}


+ {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/_src/om2py5w/README.md b/_src/om2py5w/README.md deleted file mode 100644 index 1826bfe60..000000000 --- a/_src/om2py5w/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# OMOOC.py 周任务代码试作 - -## 5w - -- 私人笔记: - + wx 后台服务 \ No newline at end of file diff --git a/_src/om2py5w/jinja2/layout.html b/_src/om2py5w/jinja2/layout.html new file mode 100644 index 000000000..b84d872bf --- /dev/null +++ b/_src/om2py5w/jinja2/layout.html @@ -0,0 +1,79 @@ + + + + Jeremiah Bottel Template Example + + + + + + + +
+ + {% block heading %} +

{% block page %}{% endblock %} - Bottle Super Example

+ {% endblock %} +

This is part of my base template

+
+ {% block content %}{% endblock %} +
+ {% block title %}{% endblock %} +
+

This is part of my base template

+
+ +
+ + + + \ No newline at end of file diff --git a/_src/om2py5w/jinja2/macro.html b/_src/om2py5w/jinja2/macro.html new file mode 100644 index 000000000..db52da57b --- /dev/null +++ b/_src/om2py5w/jinja2/macro.html @@ -0,0 +1,8 @@ +{% import bottle %} +{% macro nav_link(endpoint, name) %} +{% if request.endpoint.endswith(endpoint) %} +
  • {{name}}
  • +{% else %} +
  • {{name}}
  • +{% endif %} +{% endmacro %} \ No newline at end of file diff --git a/_src/om2py5w/jinja2/run.py b/_src/om2py5w/jinja2/run.py new file mode 100644 index 000000000..3087fc19b --- /dev/null +++ b/_src/om2py5w/jinja2/run.py @@ -0,0 +1,18 @@ +#! /usr/bin/env python +# -*- coding:utf-8 -*- +from bottle import Bottle, run, route, jinja2_template, request, jinja2_env +import jinja2 +import datetime + +app = Bottle() + +@app.template_filter() +def datetimefilter(value, format='%Y/%m/%d %H:%M'): + """convert a datetime to a different format.""" + return value.strftime(format) + +app.jinja2_env.filters['datetimefilter'] = datetimefilter + +@app.route("/") +def template_test(): + return jinja2_template('template.html', my_string='Wheeee!', my_list=[0, 1, 2, 3, 4], current_time=datetime.datetime.now()) \ No newline at end of file diff --git a/_src/om2py5w/jinja2/template.html b/_src/om2py5w/jinja2/template.html new file mode 100644 index 000000000..38ef7979a --- /dev/null +++ b/_src/om2py5w/jinja2/template.html @@ -0,0 +1,24 @@ +{% extends "layout.html" %} +{% block content %} +

    +
    +

    My string: {{my_string}}

    +

    Value from the list: {{my_list[3]}}

    +

    Loop through the list:

    +
      + {% for n in my_list %} +
    • {{n}}
    • + {% endfor %} +
    +

    Same list with a filter: {{ my_list|join(', ') }}

    +

    This is the end of my child template

    +

    Current date/time: {{ current_time | datetimefilter }}

    + + {% block footer %} + {{ super() }} + {% endblock %} + + {% block heading %} + {{ super() }} + {% endblock %} +{% endblock %} \ No newline at end of file diff --git a/_src/om2py6w/6wex0/.gitignore b/_src/om2py6w/6wex0/.gitignore new file mode 100644 index 000000000..26433dbf1 --- /dev/null +++ b/_src/om2py6w/6wex0/.gitignore @@ -0,0 +1,8 @@ +# 隐藏文件 不被push + +*.pyc +*.db +*.log +*.wsgic + +mykv.db diff --git a/_src/om2py6w/6wex0/README.md b/_src/om2py6w/6wex0/README.md new file mode 100644 index 000000000..f7f5d5995 --- /dev/null +++ b/_src/om2py6w/6wex0/README.md @@ -0,0 +1,13 @@ +# OMOOC.py 周任务代码试作 + +## 6w 极简日志交互微信版 + +- [代码](https://github.com/JeremiahZhang/OMOOC2py/tree/master/_src/om2py6w/6wex0/sae) + - 将下载sae文件 部署到 SinaSAE + - 可按照 笔记 完成对接 +- [笔记](https://jeremiahzhang.gitbooks.io/omooc2py/content/2nDev/week06_wechat.html) + +## 微信公众号 +- 微信号:BeiyaStudy +![二维码](http://dn-jeremiahzhang.qbox.me/wechatbeiya.jpg) +- 输入 h 可以查看帮助 是也乎 diff --git a/_src/om2py6w/6wex0/sae/bottle.py b/_src/om2py6w/6wex0/sae/bottle.py new file mode 100644 index 000000000..dcd20136a --- /dev/null +++ b/_src/om2py6w/6wex0/sae/bottle.py @@ -0,0 +1,4011 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Bottle is a fast and simple micro-framework for small web applications. It +offers request dispatching (Routes) with URL parameter support, templates, +a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and +template engines - all in a single file and with no dependencies other than the +Python Standard Library. + +Homepage and documentation: http://bottlepy.org/ + +Copyright (c) 2014, Marcel Hellkamp. +License: MIT (see LICENSE for details) +""" + +from __future__ import with_statement +import sys + +__author__ = 'Marcel Hellkamp' +__version__ = '0.13-dev' +__license__ = 'MIT' + +############################################################################### +# Command-line interface ######################################################## +############################################################################### +# INFO: Some server adapters need to monkey-patch std-lib modules before they +# are imported. This is why some of the command-line handling is done here, but +# the actual call to main() is at the end of the file. + + +def _cli_parse(args): + from optparse import OptionParser + parser = OptionParser( + usage="usage: %prog [options] package.module:app") + opt = parser.add_option + opt("--version", action="store_true", help="show version number.") + opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") + opt("-s", "--server", default='wsgiref', help="use SERVER as backend.") + opt("-p", "--plugin", action="append", help="install additional plugin/s.") + opt("-c", "--conf", action="append", metavar="FILE", + help="load config values from FILE.") + opt("-C", "--param", action="append", metavar="NAME=VALUE", + help="override config values.") + opt("--debug", action="store_true", help="start server in debug mode.") + opt("--reload", action="store_true", help="auto-reload on file changes.") + opts, args = parser.parse_args(args[1:]) + + return opts, args, parser + + +def _cli_patch(args): + opts, _, _ = _cli_parse(args) + if opts.server: + if opts.server.startswith('gevent'): + import gevent.monkey + gevent.monkey.patch_all() + elif opts.server.startswith('eventlet'): + import eventlet + eventlet.monkey_patch() + + +if __name__ == '__main__': + _cli_patch(sys.argv) + +############################################################################### +# Imports and Python 2/3 unification ########################################### +############################################################################### + + +import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\ + os, re, tempfile, threading, time, warnings + +from types import FunctionType +from datetime import date as datedate, datetime, timedelta +from tempfile import TemporaryFile +from traceback import format_exc, print_exc +from unicodedata import normalize + +# inspect.getargspec was removed in Python 3.6, use +# Signature-based version where we can (Python 3.3+) +try: + from inspect import signature + def getargspec(func): + params = signature(func).parameters + args, varargs, keywords, defaults = [], None, None, [] + for name, param in params.items(): + if param.kind == param.VAR_POSITIONAL: + varargs = name + elif param.kind == param.VAR_KEYWORD: + keywords = name + else: + args.append(name) + if param.default is not param.empty: + defaults.append(param.default) + return (args, varargs, keywords, tuple(defaults) or None) +except ImportError: + from inspect import getargspec + +try: + from simplejson import dumps as json_dumps, loads as json_lds +except ImportError: # pragma: no cover + try: + from json import dumps as json_dumps, loads as json_lds + except ImportError: + try: + from django.utils.simplejson import dumps as json_dumps, loads as json_lds + except ImportError: + + def json_dumps(data): + raise ImportError( + "JSON support requires Python 2.6 or simplejson.") + + json_lds = json_dumps + +# We now try to fix 2.5/2.6/3.1/3.2 incompatibilities. +# It ain't pretty but it works... Sorry for the mess. + +py = sys.version_info +py3k = py >= (3, 0, 0) +py25 = py < (2, 6, 0) +py31 = (3, 1, 0) <= py < (3, 2, 0) + +# Workaround for the missing "as" keyword in py3k. +def _e(): + return sys.exc_info()[1] + +# Workaround for the "print is a keyword/function" Python 2/3 dilemma +# and a fallback for mod_wsgi (resticts stdout/err attribute access) +try: + _stdout, _stderr = sys.stdout.write, sys.stderr.write +except IOError: + _stdout = lambda x: sys.stdout.write(x) + _stderr = lambda x: sys.stderr.write(x) + +# Lots of stdlib and builtin differences. +if py3k: + import http.client as httplib + import _thread as thread + from urllib.parse import urljoin, SplitResult as UrlSplitResult + from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote + urlunquote = functools.partial(urlunquote, encoding='latin1') + from http.cookies import SimpleCookie + from collections import MutableMapping as DictMixin + import pickle + from io import BytesIO + from configparser import ConfigParser, Error as ConfigParserError + basestring = str + unicode = str + json_loads = lambda s: json_lds(touni(s)) + callable = lambda x: hasattr(x, '__call__') + imap = map + + def _raise(*a): + raise a[0](a[1]).with_traceback(a[2]) +else: # 2.x + import httplib + import thread + from urlparse import urljoin, SplitResult as UrlSplitResult + from urllib import urlencode, quote as urlquote, unquote as urlunquote + from Cookie import SimpleCookie + from itertools import imap + import cPickle as pickle + from StringIO import StringIO as BytesIO + from ConfigParser import SafeConfigParser as ConfigParser, \ + Error as ConfigParserError + if py25: + msg = "Python 2.5 support may be dropped in future versions of Bottle." + warnings.warn(msg, DeprecationWarning) + from UserDict import DictMixin + + def next(it): + return it.next() + + bytes = str + else: # 2.6, 2.7 + from collections import MutableMapping as DictMixin + unicode = unicode + json_loads = json_lds + eval(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec')) + + +# Some helpers for string/byte handling +def tob(s, enc='utf8'): + return s.encode(enc) if isinstance(s, unicode) else bytes(s) + + +def touni(s, enc='utf8', err='strict'): + if isinstance(s, bytes): + return s.decode(enc, err) + else: + return unicode(s or ("" if s is None else s)) + + +tonat = touni if py3k else tob + +# 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense). +# 3.1 needs a workaround. +if py31: + from io import TextIOWrapper + + class NCTextIOWrapper(TextIOWrapper): + def close(self): + pass # Keep wrapped buffer open. + + +# A bug in functools causes it to break if the wrapper is an instance method +def update_wrapper(wrapper, wrapped, *a, **ka): + try: + functools.update_wrapper(wrapper, wrapped, *a, **ka) + except AttributeError: + pass + +# These helpers are used at module level and need to be defined first. +# And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. + + +def depr(message, strict=False): + warnings.warn(message, DeprecationWarning, stacklevel=3) + + +def makelist(data): # This is just too handy + if isinstance(data, (tuple, list, set, dict)): + return list(data) + elif data: + return [data] + else: + return [] + + +class DictProperty(object): + """ Property that maps to a key in a local dict-like attribute. """ + + def __init__(self, attr, key=None, read_only=False): + self.attr, self.key, self.read_only = attr, key, read_only + + def __call__(self, func): + functools.update_wrapper(self, func, updated=[]) + self.getter, self.key = func, self.key or func.__name__ + return self + + def __get__(self, obj, cls): + if obj is None: return self + key, storage = self.key, getattr(obj, self.attr) + if key not in storage: storage[key] = self.getter(obj) + return storage[key] + + def __set__(self, obj, value): + if self.read_only: raise AttributeError("Read-Only property.") + getattr(obj, self.attr)[self.key] = value + + def __delete__(self, obj): + if self.read_only: raise AttributeError("Read-Only property.") + del getattr(obj, self.attr)[self.key] + + +class cached_property(object): + """ A property that is only computed once per instance and then replaces + itself with an ordinary attribute. Deleting the attribute resets the + property. """ + + def __init__(self, func): + self.__doc__ = getattr(func, '__doc__') + self.func = func + + def __get__(self, obj, cls): + if obj is None: return self + value = obj.__dict__[self.func.__name__] = self.func(obj) + return value + + +class lazy_attribute(object): + """ A property that caches itself to the class object. """ + + def __init__(self, func): + functools.update_wrapper(self, func, updated=[]) + self.getter = func + + def __get__(self, obj, cls): + value = self.getter(cls) + setattr(cls, self.__name__, value) + return value + +############################################################################### +# Exceptions and Events ######################################################## +############################################################################### + + +class BottleException(Exception): + """ A base class for exceptions used by bottle. """ + pass + +############################################################################### +# Routing ###################################################################### +############################################################################### + + +class RouteError(BottleException): + """ This is a base class for all routing related exceptions """ + + +class RouteReset(BottleException): + """ If raised by a plugin or request handler, the route is reset and all + plugins are re-applied. """ + + +class RouterUnknownModeError(RouteError): + + pass + + +class RouteSyntaxError(RouteError): + """ The route parser found something not supported by this router. """ + + +class RouteBuildError(RouteError): + """ The route could not be built. """ + + +def _re_flatten(p): + """ Turn all capturing groups in a regular expression pattern into + non-capturing groups. """ + if '(' not in p: + return p + return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', lambda m: m.group(0) if + len(m.group(1)) % 2 else m.group(1) + '(?:', p) + + +class Router(object): + """ A Router is an ordered collection of route->target pairs. It is used to + efficiently match WSGI requests against a number of routes and return + the first target that satisfies the request. The target may be anything, + usually a string, ID or callable object. A route consists of a path-rule + and a HTTP method. + + The path-rule is either a static path (e.g. `/contact`) or a dynamic + path that contains wildcards (e.g. `/wiki/`). The wildcard syntax + and details on the matching order are described in docs:`routing`. + """ + + default_pattern = '[^/]+' + default_filter = 're' + + #: The current CPython regexp implementation does not allow more + #: than 99 matching groups per regular expression. + _MAX_GROUPS_PER_PATTERN = 99 + + def __init__(self, strict=False): + self.rules = [] # All rules in order + self._groups = {} # index of regexes to find them in dyna_routes + self.builder = {} # Data structure for the url builder + self.static = {} # Search structure for static routes + self.dyna_routes = {} + self.dyna_regexes = {} # Search structure for dynamic routes + #: If true, static routes are no longer checked first. + self.strict_order = strict + self.filters = { + 're': lambda conf: (_re_flatten(conf or self.default_pattern), + None, None), + 'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))), + 'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))), + 'path': lambda conf: (r'.+?', None, None) + } + + def add_filter(self, name, func): + """ Add a filter. The provided function is called with the configuration + string as parameter and must return a (regexp, to_python, to_url) tuple. + The first element is a string, the last two are callables or None. """ + self.filters[name] = func + + rule_syntax = re.compile('(\\\\*)' + '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)' + '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)' + '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') + + def _itertokens(self, rule): + offset, prefix = 0, '' + for match in self.rule_syntax.finditer(rule): + prefix += rule[offset:match.start()] + g = match.groups() + if len(g[0]) % 2: # Escaped wildcard + prefix += match.group(0)[len(g[0]):] + offset = match.end() + continue + if prefix: + yield prefix, None, None + name, filtr, conf = g[4:7] if g[2] is None else g[1:4] + yield name, filtr or 'default', conf or None + offset, prefix = match.end(), '' + if offset <= len(rule) or prefix: + yield prefix + rule[offset:], None, None + + def add(self, rule, method, target, name=None): + """ Add a new rule or replace the target for an existing rule. """ + anons = 0 # Number of anonymous wildcards found + keys = [] # Names of keys + pattern = '' # Regular expression pattern with named groups + filters = [] # Lists of wildcard input filters + builder = [] # Data structure for the URL builder + is_static = True + + for key, mode, conf in self._itertokens(rule): + if mode: + is_static = False + if mode == 'default': mode = self.default_filter + mask, in_filter, out_filter = self.filters[mode](conf) + if not key: + pattern += '(?:%s)' % mask + key = 'anon%d' % anons + anons += 1 + else: + pattern += '(?P<%s>%s)' % (key, mask) + keys.append(key) + if in_filter: filters.append((key, in_filter)) + builder.append((key, out_filter or str)) + elif key: + pattern += re.escape(key) + builder.append((None, key)) + + self.builder[rule] = builder + if name: self.builder[name] = builder + + if is_static and not self.strict_order: + self.static.setdefault(method, {}) + self.static[method][self.build(rule)] = (target, None) + return + + try: + re_pattern = re.compile('^(%s)$' % pattern) + re_match = re_pattern.match + except re.error: + raise RouteSyntaxError("Could not add Route: %s (%s)" % + (rule, _e())) + + if filters: + + def getargs(path): + url_args = re_match(path).groupdict() + for name, wildcard_filter in filters: + try: + url_args[name] = wildcard_filter(url_args[name]) + except ValueError: + raise HTTPError(400, 'Path has wrong format.') + return url_args + elif re_pattern.groupindex: + + def getargs(path): + return re_match(path).groupdict() + else: + getargs = None + + flatpat = _re_flatten(pattern) + whole_rule = (rule, flatpat, target, getargs) + + if (flatpat, method) in self._groups: + if DEBUG: + msg = 'Route <%s %s> overwrites a previously defined route' + warnings.warn(msg % (method, rule), RuntimeWarning) + self.dyna_routes[method][ + self._groups[flatpat, method]] = whole_rule + else: + self.dyna_routes.setdefault(method, []).append(whole_rule) + self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1 + + self._compile(method) + + def _compile(self, method): + all_rules = self.dyna_routes[method] + comborules = self.dyna_regexes[method] = [] + maxgroups = self._MAX_GROUPS_PER_PATTERN + for x in range(0, len(all_rules), maxgroups): + some = all_rules[x:x + maxgroups] + combined = (flatpat for (_, flatpat, _, _) in some) + combined = '|'.join('(^%s$)' % flatpat for flatpat in combined) + combined = re.compile(combined).match + rules = [(target, getargs) for (_, _, target, getargs) in some] + comborules.append((combined, rules)) + + def build(self, _name, *anons, **query): + """ Build an URL by filling the wildcards in a rule. """ + builder = self.builder.get(_name) + if not builder: + raise RouteBuildError("No route with that name.", _name) + try: + for i, value in enumerate(anons): + query['anon%d' % i] = value + url = ''.join([f(query.pop(n)) if n else f for (n, f) in builder]) + return url if not query else url + '?' + urlencode(query) + except KeyError: + raise RouteBuildError('Missing URL argument: %r' % _e().args[0]) + + def match(self, environ): + """ Return a (target, url_args) tuple or raise HTTPError(400/404/405). """ + verb = environ['REQUEST_METHOD'].upper() + path = environ['PATH_INFO'] or '/' + + if verb == 'HEAD': + methods = ['PROXY', verb, 'GET', 'ANY'] + else: + methods = ['PROXY', verb, 'ANY'] + + for method in methods: + if method in self.static and path in self.static[method]: + target, getargs = self.static[method][path] + return target, getargs(path) if getargs else {} + elif method in self.dyna_regexes: + for combined, rules in self.dyna_regexes[method]: + match = combined(path) + if match: + target, getargs = rules[match.lastindex - 1] + return target, getargs(path) if getargs else {} + + # No matching route found. Collect alternative methods for 405 response + allowed = set([]) + nocheck = set(methods) + for method in set(self.static) - nocheck: + if path in self.static[method]: + allowed.add(verb) + for method in set(self.dyna_regexes) - allowed - nocheck: + for combined, rules in self.dyna_regexes[method]: + match = combined(path) + if match: + allowed.add(method) + if allowed: + allow_header = ",".join(sorted(allowed)) + raise HTTPError(405, "Method not allowed.", Allow=allow_header) + + # No matching route and no alternative method found. We give up + raise HTTPError(404, "Not found: " + repr(path)) + + +class Route(object): + """ This class wraps a route callback along with route specific metadata and + configuration and applies Plugins on demand. It is also responsible for + turing an URL path rule into a regular expression usable by the Router. + """ + + def __init__(self, app, rule, method, callback, + name=None, + plugins=None, + skiplist=None, **config): + #: The application this route is installed to. + self.app = app + #: The path-rule string (e.g. ``/wiki/``). + self.rule = rule + #: The HTTP method as a string (e.g. ``GET``). + self.method = method + #: The original callback with no plugins applied. Useful for introspection. + self.callback = callback + #: The name of the route (if specified) or ``None``. + self.name = name or None + #: A list of route-specific plugins (see :meth:`Bottle.route`). + self.plugins = plugins or [] + #: A list of plugins to not apply to this route (see :meth:`Bottle.route`). + self.skiplist = skiplist or [] + #: Additional keyword arguments passed to the :meth:`Bottle.route` + #: decorator are stored in this dictionary. Used for route-specific + #: plugin configuration and meta-data. + self.config = ConfigDict().load_dict(config) + + @cached_property + def call(self): + """ The route callback with all plugins applied. This property is + created on demand and then cached to speed up subsequent requests.""" + return self._make_callback() + + def reset(self): + """ Forget any cached values. The next time :attr:`call` is accessed, + all plugins are re-applied. """ + self.__dict__.pop('call', None) + + def prepare(self): + """ Do all on-demand work immediately (useful for debugging).""" + self.call + + def all_plugins(self): + """ Yield all Plugins affecting this route. """ + unique = set() + for p in reversed(self.app.plugins + self.plugins): + if True in self.skiplist: break + name = getattr(p, 'name', False) + if name and (name in self.skiplist or name in unique): continue + if p in self.skiplist or type(p) in self.skiplist: continue + if name: unique.add(name) + yield p + + def _make_callback(self): + callback = self.callback + for plugin in self.all_plugins(): + try: + if hasattr(plugin, 'apply'): + callback = plugin.apply(callback, self) + else: + callback = plugin(callback) + except RouteReset: # Try again with changed configuration. + return self._make_callback() + if not callback is self.callback: + update_wrapper(callback, self.callback) + return callback + + def get_undecorated_callback(self): + """ Return the callback. If the callback is a decorated function, try to + recover the original function. """ + func = self.callback + func = getattr(func, '__func__' if py3k else 'im_func', func) + closure_attr = '__closure__' if py3k else 'func_closure' + while hasattr(func, closure_attr) and getattr(func, closure_attr): + attributes = getattr(func, closure_attr) + func = attributes[0].cell_contents + + # in case of decorators with multiple arguments + if not isinstance(func, FunctionType): + # pick first FunctionType instance from multiple arguments + func = filter(lambda x: isinstance(x, FunctionType), + map(lambda x: x.cell_contents, attributes)) + func = list(func)[0] # py3 support + return func + + def get_callback_args(self): + """ Return a list of argument names the callback (most likely) accepts + as keyword arguments. If the callback is a decorated function, try + to recover the original function before inspection. """ + return getargspec(self.get_undecorated_callback())[0] + + def get_config(self, key, default=None): + """ Lookup a config field and return its value, first checking the + route.config, then route.app.config.""" + for conf in (self.config, self.app.config): + if key in conf: return conf[key] + return default + + def __repr__(self): + cb = self.get_undecorated_callback() + return '<%s %r %r>' % (self.method, self.rule, cb) + +############################################################################### +# Application Object ########################################################### +############################################################################### + + +class Bottle(object): + """ Each Bottle object represents a single, distinct web application and + consists of routes, callbacks, plugins, resources and configuration. + Instances are callable WSGI applications. + + :param catchall: If true (default), handle all exceptions. Turn off to + let debugging middleware handle exceptions. + """ + + def __init__(self, catchall=True, autojson=True): + #: A :class:`ConfigDict` for app specific configuration. + self.config = ConfigDict() + self.config._on_change = functools.partial(self.trigger_hook, 'config') + self.config.meta_set('autojson', 'validate', bool) + self.config.meta_set('catchall', 'validate', bool) + self.config['catchall'] = catchall + self.config['autojson'] = autojson + + #: A :class:`ResourceManager` for application files + self.resources = ResourceManager() + + self.routes = [] # List of installed :class:`Route` instances. + self.router = Router() # Maps requests to :class:`Route` instances. + self.error_handler = {} + + # Core plugins + self.plugins = [] # List of installed plugins. + if self.config['autojson']: + self.install(JSONPlugin()) + self.install(TemplatePlugin()) + + #: If true, most exceptions are caught and returned as :exc:`HTTPError` + catchall = DictProperty('config', 'catchall') + + __hook_names = 'before_request', 'after_request', 'app_reset', 'config' + __hook_reversed = 'after_request' + + @cached_property + def _hooks(self): + return dict((name, []) for name in self.__hook_names) + + def add_hook(self, name, func): + """ Attach a callback to a hook. Three hooks are currently implemented: + + before_request + Executed once before each request. The request context is + available, but no routing has happened yet. + after_request + Executed once after each request regardless of its outcome. + app_reset + Called whenever :meth:`Bottle.reset` is called. + """ + if name in self.__hook_reversed: + self._hooks[name].insert(0, func) + else: + self._hooks[name].append(func) + + def remove_hook(self, name, func): + """ Remove a callback from a hook. """ + if name in self._hooks and func in self._hooks[name]: + self._hooks[name].remove(func) + return True + + def trigger_hook(self, __name, *args, **kwargs): + """ Trigger a hook and return a list of results. """ + return [hook(*args, **kwargs) for hook in self._hooks[__name][:]] + + def hook(self, name): + """ Return a decorator that attaches a callback to a hook. See + :meth:`add_hook` for details.""" + + def decorator(func): + self.add_hook(name, func) + return func + + return decorator + + def mount(self, prefix, app, **options): + """ Mount an application (:class:`Bottle` or plain WSGI) to a specific + URL prefix. Example:: + + root_app.mount('/admin/', admin_app) + + :param prefix: path prefix or `mount-point`. If it ends in a slash, + that slash is mandatory. + :param app: an instance of :class:`Bottle` or a WSGI application. + + All other parameters are passed to the underlying :meth:`route` call. + """ + + segments = [p for p in prefix.split('/') if p] + if not segments: raise ValueError('Empty path prefix.') + path_depth = len(segments) + + def mountpoint_wrapper(): + try: + request.path_shift(path_depth) + rs = HTTPResponse([]) + + def start_response(status, headerlist, exc_info=None): + if exc_info: + _raise(*exc_info) + rs.status = status + for name, value in headerlist: + rs.add_header(name, value) + return rs.body.append + + body = app(request.environ, start_response) + rs.body = itertools.chain(rs.body, body) if rs.body else body + return rs + finally: + request.path_shift(-path_depth) + + options.setdefault('skip', True) + options.setdefault('method', 'PROXY') + options.setdefault('mountpoint', {'prefix': prefix, 'target': app}) + options['callback'] = mountpoint_wrapper + + self.route('/%s/<:re:.*>' % '/'.join(segments), **options) + if not prefix.endswith('/'): + self.route('/' + '/'.join(segments), **options) + + def merge(self, routes): + """ Merge the routes of another :class:`Bottle` application or a list of + :class:`Route` objects into this application. The routes keep their + 'owner', meaning that the :data:`Route.app` attribute is not + changed. """ + if isinstance(routes, Bottle): + routes = routes.routes + for route in routes: + self.add_route(route) + + def install(self, plugin): + """ Add a plugin to the list of plugins and prepare it for being + applied to all routes of this application. A plugin may be a simple + decorator or an object that implements the :class:`Plugin` API. + """ + if hasattr(plugin, 'setup'): plugin.setup(self) + if not callable(plugin) and not hasattr(plugin, 'apply'): + raise TypeError("Plugins must be callable or implement .apply()") + self.plugins.append(plugin) + self.reset() + return plugin + + def uninstall(self, plugin): + """ Uninstall plugins. Pass an instance to remove a specific plugin, a type + object to remove all plugins that match that type, a string to remove + all plugins with a matching ``name`` attribute or ``True`` to remove all + plugins. Return the list of removed plugins. """ + removed, remove = [], plugin + for i, plugin in list(enumerate(self.plugins))[::-1]: + if remove is True or remove is plugin or remove is type(plugin) \ + or getattr(plugin, 'name', True) == remove: + removed.append(plugin) + del self.plugins[i] + if hasattr(plugin, 'close'): plugin.close() + if removed: self.reset() + return removed + + def reset(self, route=None): + """ Reset all routes (force plugins to be re-applied) and clear all + caches. If an ID or route object is given, only that specific route + is affected. """ + if route is None: routes = self.routes + elif isinstance(route, Route): routes = [route] + else: routes = [self.routes[route]] + for route in routes: + route.reset() + if DEBUG: + for route in routes: + route.prepare() + self.trigger_hook('app_reset') + + def close(self): + """ Close the application and all installed plugins. """ + for plugin in self.plugins: + if hasattr(plugin, 'close'): plugin.close() + + def run(self, **kwargs): + """ Calls :func:`run` with the same parameters. """ + run(self, **kwargs) + + def match(self, environ): + """ Search for a matching route and return a (:class:`Route` , urlargs) + tuple. The second value is a dictionary with parameters extracted + from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" + return self.router.match(environ) + + def get_url(self, routename, **kargs): + """ Return a string that matches a named route """ + scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/' + location = self.router.build(routename, **kargs).lstrip('/') + return urljoin(urljoin('/', scriptname), location) + + def add_route(self, route): + """ Add a route object, but do not change the :data:`Route.app` + attribute.""" + self.routes.append(route) + self.router.add(route.rule, route.method, route, name=route.name) + if DEBUG: route.prepare() + + def route(self, + path=None, + method='GET', + callback=None, + name=None, + apply=None, + skip=None, **config): + """ A decorator to bind a function to a request URL. Example:: + + @app.route('/hello/') + def hello(name): + return 'Hello %s' % name + + The ```` part is a wildcard. See :class:`Router` for syntax + details. + + :param path: Request path or a list of paths to listen to. If no + path is specified, it is automatically generated from the + signature of the function. + :param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of + methods to listen to. (default: `GET`) + :param callback: An optional shortcut to avoid the decorator + syntax. ``route(..., callback=func)`` equals ``route(...)(func)`` + :param name: The name for this route. (default: None) + :param apply: A decorator or plugin or a list of plugins. These are + applied to the route callback in addition to installed plugins. + :param skip: A list of plugins, plugin classes or names. Matching + plugins are not installed to this route. ``True`` skips all. + + Any additional keyword arguments are stored as route-specific + configuration and passed to plugins (see :meth:`Plugin.apply`). + """ + if callable(path): path, callback = None, path + plugins = makelist(apply) + skiplist = makelist(skip) + + def decorator(callback): + if isinstance(callback, basestring): callback = load(callback) + for rule in makelist(path) or yieldroutes(callback): + for verb in makelist(method): + verb = verb.upper() + route = Route(self, rule, verb, callback, + name=name, + plugins=plugins, + skiplist=skiplist, **config) + self.add_route(route) + return callback + + return decorator(callback) if callback else decorator + + def get(self, path=None, method='GET', **options): + """ Equals :meth:`route`. """ + return self.route(path, method, **options) + + def post(self, path=None, method='POST', **options): + """ Equals :meth:`route` with a ``POST`` method parameter. """ + return self.route(path, method, **options) + + def put(self, path=None, method='PUT', **options): + """ Equals :meth:`route` with a ``PUT`` method parameter. """ + return self.route(path, method, **options) + + def delete(self, path=None, method='DELETE', **options): + """ Equals :meth:`route` with a ``DELETE`` method parameter. """ + return self.route(path, method, **options) + + def patch(self, path=None, method='PATCH', **options): + """ Equals :meth:`route` with a ``PATCH`` method parameter. """ + return self.route(path, method, **options) + + def error(self, code=500): + """ Decorator: Register an output handler for a HTTP error code""" + + def wrapper(handler): + self.error_handler[int(code)] = handler + return handler + + return wrapper + + def default_error_handler(self, res): + return tob(template(ERROR_PAGE_TEMPLATE, e=res)) + + def _handle(self, environ): + path = environ['bottle.raw_path'] = environ['PATH_INFO'] + if py3k: + environ['PATH_INFO'] = path.encode('latin1').decode('utf8', 'ignore') + + def _inner_handle(): + # Maybe pass variables as locals for better performance? + try: + route, args = self.router.match(environ) + environ['route.handle'] = route + environ['bottle.route'] = route + environ['route.url_args'] = args + return route.call(**args) + except HTTPResponse: + return _e() + except RouteReset: + route.reset() + return _inner_handle() + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception: + if not self.catchall: raise + stacktrace = format_exc() + environ['wsgi.errors'].write(stacktrace) + return HTTPError(500, "Internal Server Error", _e(), stacktrace) + + try: + out = None + environ['bottle.app'] = self + request.bind(environ) + response.bind() + self.trigger_hook('before_request') + out = _inner_handle() + return out; + finally: + if isinstance(out, HTTPResponse): + out.apply(response) + self.trigger_hook('after_request') + + def _cast(self, out, peek=None): + """ Try to convert the parameter into something WSGI compatible and set + correct HTTP headers when possible. + Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like, + iterable of strings and iterable of unicodes + """ + + # Empty output is done here + if not out: + if 'Content-Length' not in response: + response['Content-Length'] = 0 + return [] + # Join lists of byte or unicode strings. Mixed lists are NOT supported + if isinstance(out, (tuple, list))\ + and isinstance(out[0], (bytes, unicode)): + out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' + # Encode unicode strings + if isinstance(out, unicode): + out = out.encode(response.charset) + # Byte Strings are just returned + if isinstance(out, bytes): + if 'Content-Length' not in response: + response['Content-Length'] = len(out) + return [out] + # HTTPError or HTTPException (recursive, because they may wrap anything) + # TODO: Handle these explicitly in handle() or make them iterable. + if isinstance(out, HTTPError): + out.apply(response) + out = self.error_handler.get(out.status_code, + self.default_error_handler)(out) + return self._cast(out) + if isinstance(out, HTTPResponse): + out.apply(response) + return self._cast(out.body) + + # File-like objects. + if hasattr(out, 'read'): + if 'wsgi.file_wrapper' in request.environ: + return request.environ['wsgi.file_wrapper'](out) + elif hasattr(out, 'close') or not hasattr(out, '__iter__'): + return WSGIFileWrapper(out) + + # Handle Iterables. We peek into them to detect their inner type. + try: + iout = iter(out) + first = next(iout) + while not first: + first = next(iout) + except StopIteration: + return self._cast('') + except HTTPResponse: + first = _e() + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except: + if not self.catchall: raise + first = HTTPError(500, 'Unhandled exception', _e(), format_exc()) + + # These are the inner types allowed in iterator or generator objects. + if isinstance(first, HTTPResponse): + return self._cast(first) + elif isinstance(first, bytes): + new_iter = itertools.chain([first], iout) + elif isinstance(first, unicode): + encoder = lambda x: x.encode(response.charset) + new_iter = imap(encoder, itertools.chain([first], iout)) + else: + msg = 'Unsupported response type: %s' % type(first) + return self._cast(HTTPError(500, msg)) + if hasattr(out, 'close'): + new_iter = _closeiter(new_iter, out.close) + return new_iter + + def wsgi(self, environ, start_response): + """ The bottle WSGI-interface. """ + try: + out = self._cast(self._handle(environ)) + # rfc2616 section 4.3 + if response._status_code in (100, 101, 204, 304)\ + or environ['REQUEST_METHOD'] == 'HEAD': + if hasattr(out, 'close'): out.close() + out = [] + start_response(response._status_line, response.headerlist) + return out + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except: + if not self.catchall: raise + err = '

    Critical error while processing request: %s

    ' \ + % html_escape(environ.get('PATH_INFO', '/')) + if DEBUG: + err += '

    Error:

    \n
    \n%s\n
    \n' \ + '

    Traceback:

    \n
    \n%s\n
    \n' \ + % (html_escape(repr(_e())), html_escape(format_exc())) + environ['wsgi.errors'].write(err) + headers = [('Content-Type', 'text/html; charset=UTF-8')] + start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info()) + return [tob(err)] + + def __call__(self, environ, start_response): + """ Each instance of :class:'Bottle' is a WSGI application. """ + return self.wsgi(environ, start_response) + + def __enter__(self): + """ Use this application as default for all module-level shortcuts. """ + default_app.push(self) + return self + + def __exit__(self, exc_type, exc_value, traceback): + default_app.pop() + + def __setattr__(self, name, value): + if name in self.__dict__: + raise AttributeError("Attribute %s already defined. Plugin conflict?" % name) + self.__dict__[name] = value + + +############################################################################### +# HTTP and WSGI Tools ########################################################## +############################################################################### + + +class BaseRequest(object): + """ A wrapper for WSGI environment dictionaries that adds a lot of + convenient access methods and properties. Most of them are read-only. + + Adding new attributes to a request actually adds them to the environ + dictionary (as 'bottle.request.ext.'). This is the recommended + way to store and access request-specific data. + """ + + __slots__ = ('environ', ) + + #: Maximum size of memory buffer for :attr:`body` in bytes. + MEMFILE_MAX = 102400 + + def __init__(self, environ=None): + """ Wrap a WSGI environ dictionary. """ + #: The wrapped WSGI environ dictionary. This is the only real attribute. + #: All other attributes actually are read-only properties. + self.environ = {} if environ is None else environ + self.environ['bottle.request'] = self + + @DictProperty('environ', 'bottle.app', read_only=True) + def app(self): + """ Bottle application handling this request. """ + raise RuntimeError('This request is not connected to an application.') + + @DictProperty('environ', 'bottle.route', read_only=True) + def route(self): + """ The bottle :class:`Route` object that matches this request. """ + raise RuntimeError('This request is not connected to a route.') + + @DictProperty('environ', 'route.url_args', read_only=True) + def url_args(self): + """ The arguments extracted from the URL. """ + raise RuntimeError('This request is not connected to a route.') + + @property + def path(self): + """ The value of ``PATH_INFO`` with exactly one prefixed slash (to fix + broken clients and avoid the "empty path" edge case). """ + return '/' + self.environ.get('PATH_INFO', '').lstrip('/') + + @property + def method(self): + """ The ``REQUEST_METHOD`` value as an uppercase string. """ + return self.environ.get('REQUEST_METHOD', 'GET').upper() + + @DictProperty('environ', 'bottle.request.headers', read_only=True) + def headers(self): + """ A :class:`WSGIHeaderDict` that provides case-insensitive access to + HTTP request headers. """ + return WSGIHeaderDict(self.environ) + + def get_header(self, name, default=None): + """ Return the value of a request header, or a given default value. """ + return self.headers.get(name, default) + + @DictProperty('environ', 'bottle.request.cookies', read_only=True) + def cookies(self): + """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT + decoded. Use :meth:`get_cookie` if you expect signed cookies. """ + cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values() + return FormsDict((c.key, c.value) for c in cookies) + + def get_cookie(self, key, default=None, secret=None): + """ Return the content of a cookie. To read a `Signed Cookie`, the + `secret` must match the one used to create the cookie (see + :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing + cookie or wrong signature), return a default value. """ + value = self.cookies.get(key) + if secret and value: + dec = cookie_decode(value, secret) # (key, value) tuple or None + return dec[1] if dec and dec[0] == key else default + return value or default + + @DictProperty('environ', 'bottle.request.query', read_only=True) + def query(self): + """ The :attr:`query_string` parsed into a :class:`FormsDict`. These + values are sometimes called "URL arguments" or "GET parameters", but + not to be confused with "URL wildcards" as they are provided by the + :class:`Router`. """ + get = self.environ['bottle.get'] = FormsDict() + pairs = _parse_qsl(self.environ.get('QUERY_STRING', '')) + for key, value in pairs: + get[key] = value + return get + + @DictProperty('environ', 'bottle.request.forms', read_only=True) + def forms(self): + """ Form values parsed from an `url-encoded` or `multipart/form-data` + encoded POST or PUT request body. The result is returned as a + :class:`FormsDict`. All keys and values are strings. File uploads + are stored separately in :attr:`files`. """ + forms = FormsDict() + for name, item in self.POST.allitems(): + if not isinstance(item, FileUpload): + forms[name] = item + return forms + + @DictProperty('environ', 'bottle.request.params', read_only=True) + def params(self): + """ A :class:`FormsDict` with the combined values of :attr:`query` and + :attr:`forms`. File uploads are stored in :attr:`files`. """ + params = FormsDict() + for key, value in self.query.allitems(): + params[key] = value + for key, value in self.forms.allitems(): + params[key] = value + return params + + @DictProperty('environ', 'bottle.request.files', read_only=True) + def files(self): + """ File uploads parsed from `multipart/form-data` encoded POST or PUT + request body. The values are instances of :class:`FileUpload`. + + """ + files = FormsDict() + for name, item in self.POST.allitems(): + if isinstance(item, FileUpload): + files[name] = item + return files + + @DictProperty('environ', 'bottle.request.json', read_only=True) + def json(self): + """ If the ``Content-Type`` header is ``application/json``, this + property holds the parsed content of the request body. Only requests + smaller than :attr:`MEMFILE_MAX` are processed to avoid memory + exhaustion. Invalid JSON raises a 400 error response. """ + ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0] + if ctype == 'application/json': + b = self._get_body_string() + if not b: + return None + try: + return json_loads(b) + except (ValueError, TypeError): + raise HTTPError(400, 'Invalid JSON') + return None + + def _iter_body(self, read, bufsize): + maxread = max(0, self.content_length) + while maxread: + part = read(min(maxread, bufsize)) + if not part: break + yield part + maxread -= len(part) + + @staticmethod + def _iter_chunked(read, bufsize): + err = HTTPError(400, 'Error while parsing chunked transfer body.') + rn, sem, bs = tob('\r\n'), tob(';'), tob('') + while True: + header = read(1) + while header[-2:] != rn: + c = read(1) + header += c + if not c: raise err + if len(header) > bufsize: raise err + size, _, _ = header.partition(sem) + try: + maxread = int(tonat(size.strip()), 16) + except ValueError: + raise err + if maxread == 0: break + buff = bs + while maxread > 0: + if not buff: + buff = read(min(maxread, bufsize)) + part, buff = buff[:maxread], buff[maxread:] + if not part: raise err + yield part + maxread -= len(part) + if read(2) != rn: + raise err + + @DictProperty('environ', 'bottle.request.body', read_only=True) + def _body(self): + try: + read_func = self.environ['wsgi.input'].read + except KeyError: + self.environ['wsgi.input'] = BytesIO() + return self.environ['wsgi.input'] + body_iter = self._iter_chunked if self.chunked else self._iter_body + body, body_size, is_temp_file = BytesIO(), 0, False + for part in body_iter(read_func, self.MEMFILE_MAX): + body.write(part) + body_size += len(part) + if not is_temp_file and body_size > self.MEMFILE_MAX: + body, tmp = TemporaryFile(mode='w+b'), body + body.write(tmp.getvalue()) + del tmp + is_temp_file = True + self.environ['wsgi.input'] = body + body.seek(0) + return body + + def _get_body_string(self): + """ read body until content-length or MEMFILE_MAX into a string. Raise + HTTPError(413) on requests that are to large. """ + clen = self.content_length + if clen > self.MEMFILE_MAX: + raise HTTPError(413, 'Request entity too large') + if clen < 0: clen = self.MEMFILE_MAX + 1 + data = self.body.read(clen) + if len(data) > self.MEMFILE_MAX: # Fail fast + raise HTTPError(413, 'Request entity too large') + return data + + @property + def body(self): + """ The HTTP request body as a seek-able file-like object. Depending on + :attr:`MEMFILE_MAX`, this is either a temporary file or a + :class:`io.BytesIO` instance. Accessing this property for the first + time reads and replaces the ``wsgi.input`` environ variable. + Subsequent accesses just do a `seek(0)` on the file object. """ + self._body.seek(0) + return self._body + + @property + def chunked(self): + """ True if Chunked transfer encoding was. """ + return 'chunked' in self.environ.get( + 'HTTP_TRANSFER_ENCODING', '').lower() + + #: An alias for :attr:`query`. + GET = query + + @DictProperty('environ', 'bottle.request.post', read_only=True) + def POST(self): + """ The values of :attr:`forms` and :attr:`files` combined into a single + :class:`FormsDict`. Values are either strings (form values) or + instances of :class:`cgi.FieldStorage` (file uploads). + """ + post = FormsDict() + # We default to application/x-www-form-urlencoded for everything that + # is not multipart and take the fast path (also: 3.1 workaround) + if not self.content_type.startswith('multipart/'): + pairs = _parse_qsl(tonat(self._get_body_string(), 'latin1')) + for key, value in pairs: + post[key] = value + return post + + safe_env = {'QUERY_STRING': ''} # Build a safe environment for cgi + for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): + if key in self.environ: safe_env[key] = self.environ[key] + args = dict(fp=self.body, environ=safe_env, keep_blank_values=True) + if py31: + args['fp'] = NCTextIOWrapper(args['fp'], + encoding='utf8', + newline='\n') + elif py3k: + args['encoding'] = 'utf8' + data = cgi.FieldStorage(**args) + self['_cgi.FieldStorage'] = data #http://bugs.python.org/issue18394 + data = data.list or [] + for item in data: + if item.filename: + post[item.name] = FileUpload(item.file, item.name, + item.filename, item.headers) + else: + post[item.name] = item.value + return post + + @property + def url(self): + """ The full request URI including hostname and scheme. If your app + lives behind a reverse proxy or load balancer and you get confusing + results, make sure that the ``X-Forwarded-Host`` header is set + correctly. """ + return self.urlparts.geturl() + + @DictProperty('environ', 'bottle.request.urlparts', read_only=True) + def urlparts(self): + """ The :attr:`url` string as an :class:`urlparse.SplitResult` tuple. + The tuple contains (scheme, host, path, query_string and fragment), + but the fragment is always empty because it is not visible to the + server. """ + env = self.environ + http = env.get('HTTP_X_FORWARDED_PROTO') \ + or env.get('wsgi.url_scheme', 'http') + host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') + if not host: + # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. + host = env.get('SERVER_NAME', '127.0.0.1') + port = env.get('SERVER_PORT') + if port and port != ('80' if http == 'http' else '443'): + host += ':' + port + path = urlquote(self.fullpath) + return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '') + + @property + def fullpath(self): + """ Request path including :attr:`script_name` (if present). """ + return urljoin(self.script_name, self.path.lstrip('/')) + + @property + def query_string(self): + """ The raw :attr:`query` part of the URL (everything in between ``?`` + and ``#``) as a string. """ + return self.environ.get('QUERY_STRING', '') + + @property + def script_name(self): + """ The initial portion of the URL's `path` that was removed by a higher + level (server or routing middleware) before the application was + called. This script path is returned with leading and tailing + slashes. """ + script_name = self.environ.get('SCRIPT_NAME', '').strip('/') + return '/' + script_name + '/' if script_name else '/' + + def path_shift(self, shift=1): + """ Shift path segments from :attr:`path` to :attr:`script_name` and + vice versa. + + :param shift: The number of path segments to shift. May be negative + to change the shift direction. (default: 1) + """ + script, path = path_shift(self.environ.get('SCRIPT_NAME', '/'), self.path, shift) + self['SCRIPT_NAME'], self['PATH_INFO'] = script, path + + @property + def content_length(self): + """ The request body length as an integer. The client is responsible to + set this header. Otherwise, the real length of the body is unknown + and -1 is returned. In this case, :attr:`body` will be empty. """ + return int(self.environ.get('CONTENT_LENGTH') or -1) + + @property + def content_type(self): + """ The Content-Type header as a lowercase-string (default: empty). """ + return self.environ.get('CONTENT_TYPE', '').lower() + + @property + def is_xhr(self): + """ True if the request was triggered by a XMLHttpRequest. This only + works with JavaScript libraries that support the `X-Requested-With` + header (most of the popular libraries do). """ + requested_with = self.environ.get('HTTP_X_REQUESTED_WITH', '') + return requested_with.lower() == 'xmlhttprequest' + + @property + def is_ajax(self): + """ Alias for :attr:`is_xhr`. "Ajax" is not the right term. """ + return self.is_xhr + + @property + def auth(self): + """ HTTP authentication data as a (user, password) tuple. This + implementation currently supports basic (not digest) authentication + only. If the authentication happened at a higher level (e.g. in the + front web-server or a middleware), the password field is None, but + the user field is looked up from the ``REMOTE_USER`` environ + variable. On any errors, None is returned. """ + basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION', '')) + if basic: return basic + ruser = self.environ.get('REMOTE_USER') + if ruser: return (ruser, None) + return None + + @property + def remote_route(self): + """ A list of all IPs that were involved in this request, starting with + the client IP and followed by zero or more proxies. This does only + work if all proxies support the ```X-Forwarded-For`` header. Note + that this information can be forged by malicious clients. """ + proxy = self.environ.get('HTTP_X_FORWARDED_FOR') + if proxy: return [ip.strip() for ip in proxy.split(',')] + remote = self.environ.get('REMOTE_ADDR') + return [remote] if remote else [] + + @property + def remote_addr(self): + """ The client IP as a string. Note that this information can be forged + by malicious clients. """ + route = self.remote_route + return route[0] if route else None + + def copy(self): + """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """ + return Request(self.environ.copy()) + + def get(self, value, default=None): + return self.environ.get(value, default) + + def __getitem__(self, key): + return self.environ[key] + + def __delitem__(self, key): + self[key] = "" + del (self.environ[key]) + + def __iter__(self): + return iter(self.environ) + + def __len__(self): + return len(self.environ) + + def keys(self): + return self.environ.keys() + + def __setitem__(self, key, value): + """ Change an environ value and clear all caches that depend on it. """ + + if self.environ.get('bottle.request.readonly'): + raise KeyError('The environ dictionary is read-only.') + + self.environ[key] = value + todelete = () + + if key == 'wsgi.input': + todelete = ('body', 'forms', 'files', 'params', 'post', 'json') + elif key == 'QUERY_STRING': + todelete = ('query', 'params') + elif key.startswith('HTTP_'): + todelete = ('headers', 'cookies') + + for key in todelete: + self.environ.pop('bottle.request.' + key, None) + + def __repr__(self): + return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url) + + def __getattr__(self, name): + """ Search in self.environ for additional user defined attributes. """ + try: + var = self.environ['bottle.request.ext.%s' % name] + return var.__get__(self) if hasattr(var, '__get__') else var + except KeyError: + raise AttributeError('Attribute %r not defined.' % name) + + def __setattr__(self, name, value): + if name == 'environ': return object.__setattr__(self, name, value) + key = 'bottle.request.ext.%s' % name + if key in self.environ: + raise AttributeError("Attribute already defined: %s" % name) + self.environ[key] = value + + def __delattr__(self, name, value): + try: + del self.environ['bottle.request.ext.%s' % name] + except KeyError: + raise AttributeError("Attribute not defined: %s" % name) + +def _hkey(s): + return s.title().replace('_', '-') + + +class HeaderProperty(object): + def __init__(self, name, reader=None, writer=str, default=''): + self.name, self.default = name, default + self.reader, self.writer = reader, writer + self.__doc__ = 'Current value of the %r header.' % name.title() + + def __get__(self, obj, _): + if obj is None: return self + value = obj.headers.get(self.name, self.default) + return self.reader(value) if self.reader else value + + def __set__(self, obj, value): + obj.headers[self.name] = self.writer(value) + + def __delete__(self, obj): + del obj.headers[self.name] + + +class BaseResponse(object): + """ Storage class for a response body as well as headers and cookies. + + This class does support dict-like case-insensitive item-access to + headers, but is NOT a dict. Most notably, iterating over a response + yields parts of the body and not the headers. + + :param body: The response body as one of the supported types. + :param status: Either an HTTP status code (e.g. 200) or a status line + including the reason phrase (e.g. '200 OK'). + :param headers: A dictionary or a list of name-value pairs. + + Additional keyword arguments are added to the list of headers. + Underscores in the header name are replaced with dashes. + """ + + default_status = 200 + default_content_type = 'text/html; charset=UTF-8' + + # Header blacklist for specific response codes + # (rfc2616 section 10.2.3 and 10.3.5) + bad_headers = { + 204: set(('Content-Type', )), + 304: set(('Allow', 'Content-Encoding', 'Content-Language', + 'Content-Length', 'Content-Range', 'Content-Type', + 'Content-Md5', 'Last-Modified')) + } + + def __init__(self, body='', status=None, headers=None, **more_headers): + self._cookies = None + self._headers = {} + self.body = body + self.status = status or self.default_status + if headers: + if isinstance(headers, dict): + headers = headers.items() + for name, value in headers: + self.add_header(name, value) + if more_headers: + for name, value in more_headers.items(): + self.add_header(name, value) + + def copy(self, cls=None): + """ Returns a copy of self. """ + cls = cls or BaseResponse + assert issubclass(cls, BaseResponse) + copy = cls() + copy.status = self.status + copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) + if self._cookies: + copy._cookies = SimpleCookie() + copy._cookies.load(self._cookies.output(header='')) + return copy + + def __iter__(self): + return iter(self.body) + + def close(self): + if hasattr(self.body, 'close'): + self.body.close() + + @property + def status_line(self): + """ The HTTP status line as a string (e.g. ``404 Not Found``).""" + return self._status_line + + @property + def status_code(self): + """ The HTTP status code as an integer (e.g. 404).""" + return self._status_code + + def _set_status(self, status): + if isinstance(status, int): + code, status = status, _HTTP_STATUS_LINES.get(status) + elif ' ' in status: + status = status.strip() + code = int(status.split()[0]) + else: + raise ValueError('String status line without a reason phrase.') + if not 100 <= code <= 999: + raise ValueError('Status code out of range.') + self._status_code = code + self._status_line = str(status or ('%d Unknown' % code)) + + def _get_status(self): + return self._status_line + + status = property( + _get_status, _set_status, None, + ''' A writeable property to change the HTTP response status. It accepts + either a numeric code (100-999) or a string with a custom reason + phrase (e.g. "404 Brain not found"). Both :data:`status_line` and + :data:`status_code` are updated accordingly. The return value is + always a status string. ''') + del _get_status, _set_status + + @property + def headers(self): + """ An instance of :class:`HeaderDict`, a case-insensitive dict-like + view on the response headers. """ + hdict = HeaderDict() + hdict.dict = self._headers + return hdict + + def __contains__(self, name): + return _hkey(name) in self._headers + + def __delitem__(self, name): + del self._headers[_hkey(name)] + + def __getitem__(self, name): + return self._headers[_hkey(name)][-1] + + def __setitem__(self, name, value): + self._headers[_hkey(name)] = [value if isinstance(value, unicode) else + str(value)] + + def get_header(self, name, default=None): + """ Return the value of a previously defined header. If there is no + header with that name, return a default value. """ + return self._headers.get(_hkey(name), [default])[-1] + + def set_header(self, name, value): + """ Create a new response header, replacing any previously defined + headers with the same name. """ + self._headers[_hkey(name)] = [value if isinstance(value, unicode) + else str(value)] + + def add_header(self, name, value): + """ Add an additional response header, not removing duplicates. """ + self._headers.setdefault(_hkey(name), []).append( + value if isinstance(value, unicode) else str(value)) + + def iter_headers(self): + """ Yield (header, value) tuples, skipping headers that are not + allowed with the current response status code. """ + return self.headerlist + + @property + def headerlist(self): + """ WSGI conform list of (header, value) tuples. """ + out = [] + headers = list(self._headers.items()) + if 'Content-Type' not in self._headers: + headers.append(('Content-Type', [self.default_content_type])) + if self._status_code in self.bad_headers: + bad_headers = self.bad_headers[self._status_code] + headers = [h for h in headers if h[0] not in bad_headers] + out += [(name, val) for (name, vals) in headers for val in vals] + if self._cookies: + for c in self._cookies.values(): + out.append(('Set-Cookie', c.OutputString())) + if py3k: + return [(k, v.encode('utf8').decode('latin1')) for (k, v) in out] + else: + return [(k, v.encode('utf8') if isinstance(v, unicode) else v) + for (k, v) in out] + + content_type = HeaderProperty('Content-Type') + content_length = HeaderProperty('Content-Length', reader=int) + expires = HeaderProperty( + 'Expires', + reader=lambda x: datetime.utcfromtimestamp(parse_date(x)), + writer=lambda x: http_date(x)) + + @property + def charset(self, default='UTF-8'): + """ Return the charset specified in the content-type header (default: utf8). """ + if 'charset=' in self.content_type: + return self.content_type.split('charset=')[-1].split(';')[0].strip() + return default + + def set_cookie(self, name, value, secret=None, **options): + """ Create a new cookie or replace an old one. If the `secret` parameter is + set, create a `Signed Cookie` (described below). + + :param name: the name of the cookie. + :param value: the value of the cookie. + :param secret: a signature key required for signed cookies. + + Additionally, this method accepts all RFC 2109 attributes that are + supported by :class:`cookie.Morsel`, including: + + :param max_age: maximum age in seconds. (default: None) + :param expires: a datetime object or UNIX timestamp. (default: None) + :param domain: the domain that is allowed to read the cookie. + (default: current domain) + :param path: limits the cookie to a given path (default: current path) + :param secure: limit the cookie to HTTPS connections (default: off). + :param httponly: prevents client-side javascript to read this cookie + (default: off, requires Python 2.6 or newer). + + If neither `expires` nor `max_age` is set (default), the cookie will + expire at the end of the browser session (as soon as the browser + window is closed). + + Signed cookies may store any pickle-able object and are + cryptographically signed to prevent manipulation. Keep in mind that + cookies are limited to 4kb in most browsers. + + Warning: Signed cookies are not encrypted (the client can still see + the content) and not copy-protected (the client can restore an old + cookie). The main intention is to make pickling and unpickling + save, not to store secret information at client side. + """ + if not self._cookies: + self._cookies = SimpleCookie() + + if secret: + value = touni(cookie_encode((name, value), secret)) + elif not isinstance(value, basestring): + raise TypeError('Secret key missing for non-string Cookie.') + + # Cookie size plus options must not exceed 4kb. + if len(name) + len(value) > 3800: + raise ValueError('Content does not fit into a cookie.') + + self._cookies[name] = value + + for key, value in options.items(): + if key == 'max_age': + if isinstance(value, timedelta): + value = value.seconds + value.days * 24 * 3600 + if key == 'expires': + if isinstance(value, (datedate, datetime)): + value = value.timetuple() + elif isinstance(value, (int, float)): + value = time.gmtime(value) + value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) + if key in ('secure', 'httponly') and not value: + continue + self._cookies[name][key.replace('_', '-')] = value + + def delete_cookie(self, key, **kwargs): + """ Delete a cookie. Be sure to use the same `domain` and `path` + settings as used to create the cookie. """ + kwargs['max_age'] = -1 + kwargs['expires'] = 0 + self.set_cookie(key, '', **kwargs) + + def __repr__(self): + out = '' + for name, value in self.headerlist: + out += '%s: %s\n' % (name.title(), value.strip()) + return out + + +def _local_property(): + ls = threading.local() + + def fget(_): + try: + return ls.var + except AttributeError: + raise RuntimeError("Request context not initialized.") + + def fset(_, value): + ls.var = value + + def fdel(_): + del ls.var + + return property(fget, fset, fdel, 'Thread-local property') + + +class LocalRequest(BaseRequest): + """ A thread-local subclass of :class:`BaseRequest` with a different + set of attributes for each thread. There is usually only one global + instance of this class (:data:`request`). If accessed during a + request/response cycle, this instance always refers to the *current* + request (even on a multithreaded server). """ + bind = BaseRequest.__init__ + environ = _local_property() + + +class LocalResponse(BaseResponse): + """ A thread-local subclass of :class:`BaseResponse` with a different + set of attributes for each thread. There is usually only one global + instance of this class (:data:`response`). Its attributes are used + to build the HTTP response at the end of the request/response cycle. + """ + bind = BaseResponse.__init__ + _status_line = _local_property() + _status_code = _local_property() + _cookies = _local_property() + _headers = _local_property() + body = _local_property() + + +Request = BaseRequest +Response = BaseResponse + + +class HTTPResponse(Response, BottleException): + def __init__(self, body='', status=None, headers=None, **more_headers): + super(HTTPResponse, self).__init__(body, status, headers, **more_headers) + + def apply(self, other): + other._status_code = self._status_code + other._status_line = self._status_line + other._headers = self._headers + other._cookies = self._cookies + other.body = self.body + + +class HTTPError(HTTPResponse): + default_status = 500 + + def __init__(self, + status=None, + body=None, + exception=None, + traceback=None, **more_headers): + self.exception = exception + self.traceback = traceback + super(HTTPError, self).__init__(body, status, **more_headers) + +############################################################################### +# Plugins ###################################################################### +############################################################################### + + +class PluginError(BottleException): + pass + + +class JSONPlugin(object): + name = 'json' + api = 2 + + def __init__(self, json_dumps=json_dumps): + self.json_dumps = json_dumps + + def apply(self, callback, _): + dumps = self.json_dumps + if not dumps: return callback + + def wrapper(*a, **ka): + try: + rv = callback(*a, **ka) + except HTTPError: + rv = _e() + + if isinstance(rv, dict): + #Attempt to serialize, raises exception on failure + json_response = dumps(rv) + #Set content type only if serialization successful + response.content_type = 'application/json' + return json_response + elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict): + rv.body = dumps(rv.body) + rv.content_type = 'application/json' + return rv + + return wrapper + + +class TemplatePlugin(object): + """ This plugin applies the :func:`view` decorator to all routes with a + `template` config parameter. If the parameter is a tuple, the second + element must be a dict with additional options (e.g. `template_engine`) + or default variables for the template. """ + name = 'template' + api = 2 + + def setup(self, app): + app.tpl = self + + def apply(self, callback, route): + conf = route.config.get('template') + if isinstance(conf, (tuple, list)) and len(conf) == 2: + return view(conf[0], **conf[1])(callback) + elif isinstance(conf, str): + return view(conf)(callback) + else: + return callback + + +#: Not a plugin, but part of the plugin API. TODO: Find a better place. +class _ImportRedirect(object): + def __init__(self, name, impmask): + """ Create a virtual package that redirects imports (see PEP 302). """ + self.name = name + self.impmask = impmask + self.module = sys.modules.setdefault(name, imp.new_module(name)) + self.module.__dict__.update({ + '__file__': __file__, + '__path__': [], + '__all__': [], + '__loader__': self + }) + sys.meta_path.append(self) + + def find_module(self, fullname, path=None): + if '.' not in fullname: return + packname = fullname.rsplit('.', 1)[0] + if packname != self.name: return + return self + + def load_module(self, fullname): + if fullname in sys.modules: return sys.modules[fullname] + modname = fullname.rsplit('.', 1)[1] + realname = self.impmask % modname + __import__(realname) + module = sys.modules[fullname] = sys.modules[realname] + setattr(self.module, modname, module) + module.__loader__ = self + return module + +############################################################################### +# Common Utilities ############################################################# +############################################################################### + + +class MultiDict(DictMixin): + """ This dict stores multiple values per key, but behaves exactly like a + normal dict in that it returns only the newest value for any given key. + There are special methods available to access the full list of values. + """ + + def __init__(self, *a, **k): + self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items()) + + def __len__(self): + return len(self.dict) + + def __iter__(self): + return iter(self.dict) + + def __contains__(self, key): + return key in self.dict + + def __delitem__(self, key): + del self.dict[key] + + def __getitem__(self, key): + return self.dict[key][-1] + + def __setitem__(self, key, value): + self.append(key, value) + + def keys(self): + return self.dict.keys() + + if py3k: + + def values(self): + return (v[-1] for v in self.dict.values()) + + def items(self): + return ((k, v[-1]) for k, v in self.dict.items()) + + def allitems(self): + return ((k, v) for k, vl in self.dict.items() for v in vl) + + iterkeys = keys + itervalues = values + iteritems = items + iterallitems = allitems + + else: + + def values(self): + return [v[-1] for v in self.dict.values()] + + def items(self): + return [(k, v[-1]) for k, v in self.dict.items()] + + def iterkeys(self): + return self.dict.iterkeys() + + def itervalues(self): + return (v[-1] for v in self.dict.itervalues()) + + def iteritems(self): + return ((k, v[-1]) for k, v in self.dict.iteritems()) + + def iterallitems(self): + return ((k, v) for k, vl in self.dict.iteritems() for v in vl) + + def allitems(self): + return [(k, v) for k, vl in self.dict.iteritems() for v in vl] + + def get(self, key, default=None, index=-1, type=None): + """ Return the most recent value for a key. + + :param default: The default value to be returned if the key is not + present or the type conversion fails. + :param index: An index for the list of available values. + :param type: If defined, this callable is used to cast the value + into a specific type. Exception are suppressed and result in + the default value to be returned. + """ + try: + val = self.dict[key][index] + return type(val) if type else val + except Exception: + pass + return default + + def append(self, key, value): + """ Add a new value to the list of values for this key. """ + self.dict.setdefault(key, []).append(value) + + def replace(self, key, value): + """ Replace the list of values with a single value. """ + self.dict[key] = [value] + + def getall(self, key): + """ Return a (possibly empty) list of values for a key. """ + return self.dict.get(key) or [] + + #: Aliases for WTForms to mimic other multi-dict APIs (Django) + getone = get + getlist = getall + + +class FormsDict(MultiDict): + """ This :class:`MultiDict` subclass is used to store request form data. + Additionally to the normal dict-like item access methods (which return + unmodified data as native strings), this container also supports + attribute-like access to its values. Attributes are automatically de- + or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing + attributes default to an empty string. """ + + #: Encoding used for attribute values. + input_encoding = 'utf8' + #: If true (default), unicode strings are first encoded with `latin1` + #: and then decoded to match :attr:`input_encoding`. + recode_unicode = True + + def _fix(self, s, encoding=None): + if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI + return s.encode('latin1').decode(encoding or self.input_encoding) + elif isinstance(s, bytes): # Python 2 WSGI + return s.decode(encoding or self.input_encoding) + else: + return s + + def decode(self, encoding=None): + """ Returns a copy with all keys and values de- or recoded to match + :attr:`input_encoding`. Some libraries (e.g. WTForms) want a + unicode dictionary. """ + copy = FormsDict() + enc = copy.input_encoding = encoding or self.input_encoding + copy.recode_unicode = False + for key, value in self.allitems(): + copy.append(self._fix(key, enc), self._fix(value, enc)) + return copy + + def getunicode(self, name, default=None, encoding=None): + """ Return the value as a unicode string, or the default. """ + try: + return self._fix(self[name], encoding) + except (UnicodeError, KeyError): + return default + + def __getattr__(self, name, default=unicode()): + # Without this guard, pickle generates a cryptic TypeError: + if name.startswith('__') and name.endswith('__'): + return super(FormsDict, self).__getattr__(name) + return self.getunicode(name, default=default) + + +class HeaderDict(MultiDict): + """ A case-insensitive version of :class:`MultiDict` that defaults to + replace the old value instead of appending it. """ + + def __init__(self, *a, **ka): + self.dict = {} + if a or ka: self.update(*a, **ka) + + def __contains__(self, key): + return _hkey(key) in self.dict + + def __delitem__(self, key): + del self.dict[_hkey(key)] + + def __getitem__(self, key): + return self.dict[_hkey(key)][-1] + + def __setitem__(self, key, value): + self.dict[_hkey(key)] = [value if isinstance(value, unicode) else + str(value)] + + def append(self, key, value): + self.dict.setdefault(_hkey(key), []).append( + value if isinstance(value, unicode) else str(value)) + + def replace(self, key, value): + self.dict[_hkey(key)] = [value if isinstance(value, unicode) else + str(value)] + + def getall(self, key): + return self.dict.get(_hkey(key)) or [] + + def get(self, key, default=None, index=-1): + return MultiDict.get(self, _hkey(key), default, index) + + def filter(self, names): + for name in [_hkey(n) for n in names]: + if name in self.dict: + del self.dict[name] + + +class WSGIHeaderDict(DictMixin): + """ This dict-like class wraps a WSGI environ dict and provides convenient + access to HTTP_* fields. Keys and values are native strings + (2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI + environment contains non-native string values, these are de- or encoded + using a lossless 'latin1' character set. + + The API will remain stable even on changes to the relevant PEPs. + Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one + that uses non-native strings.) + """ + #: List of keys that do not have a ``HTTP_`` prefix. + cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH') + + def __init__(self, environ): + self.environ = environ + + def _ekey(self, key): + """ Translate header field name to CGI/WSGI environ key. """ + key = key.replace('-', '_').upper() + if key in self.cgikeys: + return key + return 'HTTP_' + key + + def raw(self, key, default=None): + """ Return the header value as is (may be bytes or unicode). """ + return self.environ.get(self._ekey(key), default) + + def __getitem__(self, key): + val = self.environ[self._ekey(key)] + if py3k: + if isinstance(val, unicode): + val = val.encode('latin1').decode('utf8') + else: + val = val.decode('utf8') + return val + + def __setitem__(self, key, value): + raise TypeError("%s is read-only." % self.__class__) + + def __delitem__(self, key): + raise TypeError("%s is read-only." % self.__class__) + + def __iter__(self): + for key in self.environ: + if key[:5] == 'HTTP_': + yield _hkey(key[5:]) + elif key in self.cgikeys: + yield _hkey(key) + + def keys(self): + return [x for x in self] + + def __len__(self): + return len(self.keys()) + + def __contains__(self, key): + return self._ekey(key) in self.environ + + +class ConfigDict(dict): + """ A dict-like configuration storage with additional support for + namespaces, validators, meta-data, on_change listeners and more. + """ + + __slots__ = ('_meta', '_on_change') + + def __init__(self): + self._meta = {} + self._on_change = lambda name, value: None + + def load_module(self, path, squash): + """ Load values from a Python module. + :param squash: Squash nested dicts into namespaces by using + load_dict(), otherwise use update() + Example: load_config('my.app.settings', True) + Example: load_config('my.app.settings', False) + """ + config_obj = __import__(path) + obj = dict([(key, getattr(config_obj, key)) + for key in dir(config_obj) if key.isupper()]) + if squash: + self.load_dict(obj) + else: + self.update(obj) + return self + + def load_config(self, filename): + """ Load values from an ``*.ini`` style config file. + + If the config file contains sections, their names are used as + namespaces for the values within. The two special sections + ``DEFAULT`` and ``bottle`` refer to the root namespace (no prefix). + """ + conf = ConfigParser() + conf.read(filename) + for section in conf.sections(): + for key, value in conf.items(section): + if section not in ('DEFAULT', 'bottle'): + key = section + '.' + key + self[key] = value + return self + + def load_dict(self, source, namespace=''): + """ Load values from a dictionary structure. Nesting can be used to + represent namespaces. + + >>> c = ConfigDict() + >>> c.load_dict({'some': {'namespace': {'key': 'value'} } }) + {'some.namespace.key': 'value'} + """ + for key, value in source.items(): + if isinstance(key, basestring): + nskey = (namespace + '.' + key).strip('.') + if isinstance(value, dict): + self.load_dict(value, namespace=nskey) + else: + self[nskey] = value + else: + raise TypeError('Key has type %r (not a string)' % type(key)) + return self + + def update(self, *a, **ka): + """ If the first parameter is a string, all keys are prefixed with this + namespace. Apart from that it works just as the usual dict.update(). + Example: ``update('some.namespace', key='value')`` """ + prefix = '' + if a and isinstance(a[0], basestring): + prefix = a[0].strip('.') + '.' + a = a[1:] + for key, value in dict(*a, **ka).items(): + self[prefix + key] = value + + def setdefault(self, key, value): + if key not in self: + self[key] = value + return self[key] + + def __setitem__(self, key, value): + if not isinstance(key, basestring): + raise TypeError('Key has type %r (not a string)' % type(key)) + value = self.meta_get(key, 'filter', lambda x: x)(value) + if key in self and self[key] is value: + return + self._on_change(key, value) + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + self._on_change(key, None) + dict.__delitem__(self, key) + + def meta_get(self, key, metafield, default=None): + """ Return the value of a meta field for a key. """ + return self._meta.get(key, {}).get(metafield, default) + + def meta_set(self, key, metafield, value): + """ Set the meta field for a key to a new value. This triggers the + on-change handler for existing keys. """ + self._meta.setdefault(key, {})[metafield] = value + if key in self: + self[key] = self[key] + + def meta_list(self, key): + """ Return an iterable of meta field names defined for a key. """ + return self._meta.get(key, {}).keys() + + +class AppStack(list): + """ A stack-like list. Calling it returns the head of the stack. """ + + def __call__(self): + """ Return the current default application. """ + return self[-1] + + def push(self, value=None): + """ Add a new :class:`Bottle` instance to the stack """ + if not isinstance(value, Bottle): + value = Bottle() + self.append(value) + return value + + +class WSGIFileWrapper(object): + def __init__(self, fp, buffer_size=1024 * 64): + self.fp, self.buffer_size = fp, buffer_size + for attr in ('fileno', 'close', 'read', 'readlines', 'tell', 'seek'): + if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr)) + + def __iter__(self): + buff, read = self.buffer_size, self.read + while True: + part = read(buff) + if not part: return + yield part + + +class _closeiter(object): + """ This only exists to be able to attach a .close method to iterators that + do not support attribute assignment (most of itertools). """ + + def __init__(self, iterator, close=None): + self.iterator = iterator + self.close_callbacks = makelist(close) + + def __iter__(self): + return iter(self.iterator) + + def close(self): + for func in self.close_callbacks: + func() + + +class ResourceManager(object): + """ This class manages a list of search paths and helps to find and open + application-bound resources (files). + + :param base: default value for :meth:`add_path` calls. + :param opener: callable used to open resources. + :param cachemode: controls which lookups are cached. One of 'all', + 'found' or 'none'. + """ + + def __init__(self, base='./', opener=open, cachemode='all'): + self.opener = opener + self.base = base + self.cachemode = cachemode + + #: A list of search paths. See :meth:`add_path` for details. + self.path = [] + #: A cache for resolved paths. ``res.cache.clear()`` clears the cache. + self.cache = {} + + def add_path(self, path, base=None, index=None, create=False): + """ Add a new path to the list of search paths. Return False if the + path does not exist. + + :param path: The new search path. Relative paths are turned into + an absolute and normalized form. If the path looks like a file + (not ending in `/`), the filename is stripped off. + :param base: Path used to absolutize relative search paths. + Defaults to :attr:`base` which defaults to ``os.getcwd()``. + :param index: Position within the list of search paths. Defaults + to last index (appends to the list). + + The `base` parameter makes it easy to reference files installed + along with a python module or package:: + + res.add_path('./resources/', __file__) + """ + base = os.path.abspath(os.path.dirname(base or self.base)) + path = os.path.abspath(os.path.join(base, os.path.dirname(path))) + path += os.sep + if path in self.path: + self.path.remove(path) + if create and not os.path.isdir(path): + os.makedirs(path) + if index is None: + self.path.append(path) + else: + self.path.insert(index, path) + self.cache.clear() + return os.path.exists(path) + + def __iter__(self): + """ Iterate over all existing files in all registered paths. """ + search = self.path[:] + while search: + path = search.pop() + if not os.path.isdir(path): continue + for name in os.listdir(path): + full = os.path.join(path, name) + if os.path.isdir(full): search.append(full) + else: yield full + + def lookup(self, name): + """ Search for a resource and return an absolute file path, or `None`. + + The :attr:`path` list is searched in order. The first match is + returend. Symlinks are followed. The result is cached to speed up + future lookups. """ + if name not in self.cache or DEBUG: + for path in self.path: + fpath = os.path.join(path, name) + if os.path.isfile(fpath): + if self.cachemode in ('all', 'found'): + self.cache[name] = fpath + return fpath + if self.cachemode == 'all': + self.cache[name] = None + return self.cache[name] + + def open(self, name, mode='r', *args, **kwargs): + """ Find a resource and return a file object, or raise IOError. """ + fname = self.lookup(name) + if not fname: raise IOError("Resource %r not found." % name) + return self.opener(fname, mode=mode, *args, **kwargs) + + +class FileUpload(object): + def __init__(self, fileobj, name, filename, headers=None): + """ Wrapper for file uploads. """ + #: Open file(-like) object (BytesIO buffer or temporary file) + self.file = fileobj + #: Name of the upload form field + self.name = name + #: Raw filename as sent by the client (may contain unsafe characters) + self.raw_filename = filename + #: A :class:`HeaderDict` with additional headers (e.g. content-type) + self.headers = HeaderDict(headers) if headers else HeaderDict() + + content_type = HeaderProperty('Content-Type') + content_length = HeaderProperty('Content-Length', reader=int, default=-1) + + @cached_property + def filename(self): + """ Name of the file on the client file system, but normalized to ensure + file system compatibility. An empty filename is returned as 'empty'. + + Only ASCII letters, digits, dashes, underscores and dots are + allowed in the final filename. Accents are removed, if possible. + Whitespace is replaced by a single dash. Leading or tailing dots + or dashes are removed. The filename is limited to 255 characters. + """ + fname = self.raw_filename + if not isinstance(fname, unicode): + fname = fname.decode('utf8', 'ignore') + fname = normalize('NFKD', fname) + fname = fname.encode('ASCII', 'ignore').decode('ASCII') + fname = os.path.basename(fname.replace('\\', os.path.sep)) + fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip() + fname = re.sub(r'[-\s]+', '-', fname).strip('.-') + return fname[:255] or 'empty' + + def _copy_file(self, fp, chunk_size=2 ** 16): + read, write, offset = self.file.read, fp.write, self.file.tell() + while 1: + buf = read(chunk_size) + if not buf: break + write(buf) + self.file.seek(offset) + + def save(self, destination, overwrite=False, chunk_size=2 ** 16): + """ Save file to disk or copy its content to an open file(-like) object. + If *destination* is a directory, :attr:`filename` is added to the + path. Existing files are not overwritten by default (IOError). + + :param destination: File path, directory or file(-like) object. + :param overwrite: If True, replace existing files. (default: False) + :param chunk_size: Bytes to read at a time. (default: 64kb) + """ + if isinstance(destination, basestring): # Except file-likes here + if os.path.isdir(destination): + destination = os.path.join(destination, self.filename) + if not overwrite and os.path.exists(destination): + raise IOError('File exists.') + with open(destination, 'wb') as fp: + self._copy_file(fp, chunk_size) + else: + self._copy_file(destination, chunk_size) + +############################################################################### +# Application Helper ########################################################### +############################################################################### + + +def abort(code=500, text='Unknown Error.'): + """ Aborts execution and causes a HTTP error. """ + raise HTTPError(code, text) + + +def redirect(url, code=None): + """ Aborts execution and causes a 303 or 302 redirect, depending on + the HTTP protocol version. """ + if not code: + code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 + res = response.copy(cls=HTTPResponse) + res.status = code + res.body = "" + res.set_header('Location', urljoin(request.url, url)) + raise res + + +def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024): + """ Yield chunks from a range in a file. No chunk is bigger than maxread.""" + fp.seek(offset) + while bytes > 0: + part = fp.read(min(bytes, maxread)) + if not part: break + bytes -= len(part) + yield part + + +def static_file(filename, root, + mimetype='auto', + download=False, + charset='UTF-8'): + """ Open a file in a safe way and return :exc:`HTTPResponse` with status + code 200, 305, 403 or 404. The ``Content-Type``, ``Content-Encoding``, + ``Content-Length`` and ``Last-Modified`` headers are set if possible. + Special support for ``If-Modified-Since``, ``Range`` and ``HEAD`` + requests. + + :param filename: Name or path of the file to send. + :param root: Root path for file lookups. Should be an absolute directory + path. + :param mimetype: Defines the content-type header (default: guess from + file extension) + :param download: If True, ask the browser to open a `Save as...` dialog + instead of opening the file with the associated program. You can + specify a custom filename as a string. If not specified, the + original filename is used (default: False). + :param charset: The charset to use for files with a ``text/*`` + mime-type. (default: UTF-8) + """ + + root = os.path.abspath(root) + os.sep + filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) + headers = dict() + + if not filename.startswith(root): + return HTTPError(403, "Access denied.") + if not os.path.exists(filename) or not os.path.isfile(filename): + return HTTPError(404, "File does not exist.") + if not os.access(filename, os.R_OK): + return HTTPError(403, "You do not have permission to access this file.") + + if mimetype == 'auto': + if download and download != True: + mimetype, encoding = mimetypes.guess_type(download) + else: + mimetype, encoding = mimetypes.guess_type(filename) + if encoding: headers['Content-Encoding'] = encoding + + if mimetype: + if mimetype[:5] == 'text/' and charset and 'charset' not in mimetype: + mimetype += '; charset=%s' % charset + headers['Content-Type'] = mimetype + + if download: + download = os.path.basename(filename if download == True else download) + headers['Content-Disposition'] = 'attachment; filename="%s"' % download + + stats = os.stat(filename) + headers['Content-Length'] = clen = stats.st_size + lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) + headers['Last-Modified'] = lm + + ims = request.environ.get('HTTP_IF_MODIFIED_SINCE') + if ims: + ims = parse_date(ims.split(";")[0].strip()) + if ims is not None and ims >= int(stats.st_mtime): + headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", + time.gmtime()) + return HTTPResponse(status=304, **headers) + + body = '' if request.method == 'HEAD' else open(filename, 'rb') + + headers["Accept-Ranges"] = "bytes" + ranges = request.environ.get('HTTP_RANGE') + if 'HTTP_RANGE' in request.environ: + ranges = list(parse_range_header(request.environ['HTTP_RANGE'], clen)) + if not ranges: + return HTTPError(416, "Requested Range Not Satisfiable") + offset, end = ranges[0] + headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) + headers["Content-Length"] = str(end - offset) + if body: body = _file_iter_range(body, offset, end - offset) + return HTTPResponse(body, status=206, **headers) + return HTTPResponse(body, **headers) + +############################################################################### +# HTTP Utilities and MISC (TODO) ############################################### +############################################################################### + + +def debug(mode=True): + """ Change the debug level. + There is only one debug level supported at the moment.""" + global DEBUG + if mode: warnings.simplefilter('default') + DEBUG = bool(mode) + + +def http_date(value): + if isinstance(value, (datedate, datetime)): + value = value.utctimetuple() + elif isinstance(value, (int, float)): + value = time.gmtime(value) + if not isinstance(value, basestring): + value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) + return value + + +def parse_date(ims): + """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ + try: + ts = email.utils.parsedate_tz(ims) + return time.mktime(ts[:8] + (0, )) - (ts[9] or 0) - time.timezone + except (TypeError, ValueError, IndexError, OverflowError): + return None + + +def parse_auth(header): + """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" + try: + method, data = header.split(None, 1) + if method.lower() == 'basic': + user, pwd = touni(base64.b64decode(tob(data))).split(':', 1) + return user, pwd + except (KeyError, ValueError): + return None + + +def parse_range_header(header, maxlen=0): + """ Yield (start, end) ranges parsed from a HTTP Range header. Skip + unsatisfiable ranges. The end index is non-inclusive.""" + if not header or header[:6] != 'bytes=': return + ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r] + for start, end in ranges: + try: + if not start: # bytes=-100 -> last 100 bytes + start, end = max(0, maxlen - int(end)), maxlen + elif not end: # bytes=100- -> all but the first 99 bytes + start, end = int(start), maxlen + else: # bytes=100-200 -> bytes 100-200 (inclusive) + start, end = int(start), min(int(end) + 1, maxlen) + if 0 <= start < end <= maxlen: + yield start, end + except ValueError: + pass + + +def _parse_qsl(qs): + r = [] + for pair in qs.replace(';', '&').split('&'): + if not pair: continue + nv = pair.split('=', 1) + if len(nv) != 2: nv.append('') + key = urlunquote(nv[0].replace('+', ' ')) + value = urlunquote(nv[1].replace('+', ' ')) + r.append((key, value)) + return r + + +def _lscmp(a, b): + """ Compares two strings in a cryptographically safe way: + Runtime is not affected by length of common prefix. """ + return not sum(0 if x == y else 1 + for x, y in zip(a, b)) and len(a) == len(b) + + +def cookie_encode(data, key): + """ Encode and sign a pickle-able object. Return a (byte) string """ + msg = base64.b64encode(pickle.dumps(data, -1)) + sig = base64.b64encode(hmac.new(tob(key), msg).digest()) + return tob('!') + sig + tob('?') + msg + + +def cookie_decode(data, key): + """ Verify and decode an encoded string. Return an object or None.""" + data = tob(data) + if cookie_is_encoded(data): + sig, msg = data.split(tob('?'), 1) + if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())): + return pickle.loads(base64.b64decode(msg)) + return None + + +def cookie_is_encoded(data): + """ Return True if the argument looks like a encoded cookie.""" + return bool(data.startswith(tob('!')) and tob('?') in data) + + +def html_escape(string): + """ Escape HTML special characters ``&<>`` and quotes ``'"``. """ + return string.replace('&', '&').replace('<', '<').replace('>', '>')\ + .replace('"', '"').replace("'", ''') + + +def html_quote(string): + """ Escape and quote a string to be used as an HTTP attribute.""" + return '"%s"' % html_escape(string).replace('\n', ' ')\ + .replace('\r', ' ').replace('\t', ' ') + + +def yieldroutes(func): + """ Return a generator for routes that match the signature (name, args) + of the func parameter. This may yield more than one route if the function + takes optional keyword arguments. The output is best described by example:: + + a() -> '/a' + b(x, y) -> '/b//' + c(x, y=5) -> '/c/' and '/c//' + d(x=5, y=6) -> '/d' and '/d/' and '/d//' + """ + path = '/' + func.__name__.replace('__', '/').lstrip('/') + spec = getargspec(func) + argc = len(spec[0]) - len(spec[3] or []) + path += ('/<%s>' * argc) % tuple(spec[0][:argc]) + yield path + for arg in spec[0][argc:]: + path += '/<%s>' % arg + yield path + + +def path_shift(script_name, path_info, shift=1): + """ Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. + + :return: The modified paths. + :param script_name: The SCRIPT_NAME path. + :param script_name: The PATH_INFO path. + :param shift: The number of path fragments to shift. May be negative to + change the shift direction. (default: 1) + """ + if shift == 0: return script_name, path_info + pathlist = path_info.strip('/').split('/') + scriptlist = script_name.strip('/').split('/') + if pathlist and pathlist[0] == '': pathlist = [] + if scriptlist and scriptlist[0] == '': scriptlist = [] + if 0 < shift <= len(pathlist): + moved = pathlist[:shift] + scriptlist = scriptlist + moved + pathlist = pathlist[shift:] + elif 0 > shift >= -len(scriptlist): + moved = scriptlist[shift:] + pathlist = moved + pathlist + scriptlist = scriptlist[:shift] + else: + empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO' + raise AssertionError("Cannot shift. Nothing left from %s" % empty) + new_script_name = '/' + '/'.join(scriptlist) + new_path_info = '/' + '/'.join(pathlist) + if path_info.endswith('/') and pathlist: new_path_info += '/' + return new_script_name, new_path_info + + +def auth_basic(check, realm="private", text="Access denied"): + """ Callback decorator to require HTTP auth (basic). + TODO: Add route(check_auth=...) parameter. """ + + def decorator(func): + + @functools.wraps(func) + def wrapper(*a, **ka): + user, password = request.auth or (None, None) + if user is None or not check(user, password): + err = HTTPError(401, text) + err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm) + return err + return func(*a, **ka) + + return wrapper + + return decorator + +# Shortcuts for common Bottle methods. +# They all refer to the current default application. + + +def make_default_app_wrapper(name): + """ Return a callable that relays calls to the current default app. """ + + @functools.wraps(getattr(Bottle, name)) + def wrapper(*a, **ka): + return getattr(app(), name)(*a, **ka) + + return wrapper + + +route = make_default_app_wrapper('route') +get = make_default_app_wrapper('get') +post = make_default_app_wrapper('post') +put = make_default_app_wrapper('put') +delete = make_default_app_wrapper('delete') +patch = make_default_app_wrapper('patch') +error = make_default_app_wrapper('error') +mount = make_default_app_wrapper('mount') +hook = make_default_app_wrapper('hook') +install = make_default_app_wrapper('install') +uninstall = make_default_app_wrapper('uninstall') +url = make_default_app_wrapper('get_url') + +############################################################################### +# Server Adapter ############################################################### +############################################################################### + + +class ServerAdapter(object): + quiet = False + + def __init__(self, host='127.0.0.1', port=8080, **options): + self.options = options + self.host = host + self.port = int(port) + + def run(self, handler): # pragma: no cover + pass + + def __repr__(self): + args = ', '.join(['%s=%s' % (k, repr(v)) + for k, v in self.options.items()]) + return "%s(%s)" % (self.__class__.__name__, args) + + +class CGIServer(ServerAdapter): + quiet = True + + def run(self, handler): # pragma: no cover + from wsgiref.handlers import CGIHandler + + def fixed_environ(environ, start_response): + environ.setdefault('PATH_INFO', '') + return handler(environ, start_response) + + CGIHandler().run(fixed_environ) + + +class FlupFCGIServer(ServerAdapter): + def run(self, handler): # pragma: no cover + import flup.server.fcgi + self.options.setdefault('bindAddress', (self.host, self.port)) + flup.server.fcgi.WSGIServer(handler, **self.options).run() + + +class WSGIRefServer(ServerAdapter): + def run(self, app): # pragma: no cover + from wsgiref.simple_server import make_server + from wsgiref.simple_server import WSGIRequestHandler, WSGIServer + import socket + + class FixedHandler(WSGIRequestHandler): + def address_string(self): # Prevent reverse DNS lookups please. + return self.client_address[0] + + def log_request(*args, **kw): + if not self.quiet: + return WSGIRequestHandler.log_request(*args, **kw) + + handler_cls = self.options.get('handler_class', FixedHandler) + server_cls = self.options.get('server_class', WSGIServer) + + if ':' in self.host: # Fix wsgiref for IPv6 addresses. + if getattr(server_cls, 'address_family') == socket.AF_INET: + + class server_cls(server_cls): + address_family = socket.AF_INET6 + + self.srv = make_server(self.host, self.port, app, server_cls, + handler_cls) + self.port = self.srv.server_port # update port actual port (0 means random) + try: + self.srv.serve_forever() + except KeyboardInterrupt: + self.srv.server_close() # Prevent ResourceWarning: unclosed socket + raise + + +class CherryPyServer(ServerAdapter): + def run(self, handler): # pragma: no cover + from cherrypy import wsgiserver + self.options['bind_addr'] = (self.host, self.port) + self.options['wsgi_app'] = handler + + certfile = self.options.get('certfile') + if certfile: + del self.options['certfile'] + keyfile = self.options.get('keyfile') + if keyfile: + del self.options['keyfile'] + + server = wsgiserver.CherryPyWSGIServer(**self.options) + if certfile: + server.ssl_certificate = certfile + if keyfile: + server.ssl_private_key = keyfile + + try: + server.start() + finally: + server.stop() + + +class WaitressServer(ServerAdapter): + def run(self, handler): + from waitress import serve + serve(handler, host=self.host, port=self.port, _quiet=self.quiet) + + +class PasteServer(ServerAdapter): + def run(self, handler): # pragma: no cover + from paste import httpserver + from paste.translogger import TransLogger + handler = TransLogger(handler, setup_console_handler=(not self.quiet)) + httpserver.serve(handler, + host=self.host, + port=str(self.port), **self.options) + + +class MeinheldServer(ServerAdapter): + def run(self, handler): + from meinheld import server + server.listen((self.host, self.port)) + server.run(handler) + + +class FapwsServer(ServerAdapter): + """ Extremely fast webserver using libev. See http://www.fapws.org/ """ + + def run(self, handler): # pragma: no cover + import fapws._evwsgi as evwsgi + from fapws import base, config + port = self.port + if float(config.SERVER_IDENT[-2:]) > 0.4: + # fapws3 silently changed its API in 0.5 + port = str(port) + evwsgi.start(self.host, port) + # fapws3 never releases the GIL. Complain upstream. I tried. No luck. + if 'BOTTLE_CHILD' in os.environ and not self.quiet: + _stderr("WARNING: Auto-reloading does not work with Fapws3.\n") + _stderr(" (Fapws3 breaks python thread support)\n") + evwsgi.set_base_module(base) + + def app(environ, start_response): + environ['wsgi.multiprocess'] = False + return handler(environ, start_response) + + evwsgi.wsgi_cb(('', app)) + evwsgi.run() + + +class TornadoServer(ServerAdapter): + """ The super hyped asynchronous server by facebook. Untested. """ + + def run(self, handler): # pragma: no cover + import tornado.wsgi, tornado.httpserver, tornado.ioloop + container = tornado.wsgi.WSGIContainer(handler) + server = tornado.httpserver.HTTPServer(container) + server.listen(port=self.port, address=self.host) + tornado.ioloop.IOLoop.instance().start() + + +class AppEngineServer(ServerAdapter): + """ Adapter for Google App Engine. """ + quiet = True + + def run(self, handler): + from google.appengine.ext.webapp import util + # A main() function in the handler script enables 'App Caching'. + # Lets makes sure it is there. This _really_ improves performance. + module = sys.modules.get('__main__') + if module and not hasattr(module, 'main'): + module.main = lambda: util.run_wsgi_app(handler) + util.run_wsgi_app(handler) + + +class TwistedServer(ServerAdapter): + """ Untested. """ + + def run(self, handler): + from twisted.web import server, wsgi + from twisted.python.threadpool import ThreadPool + from twisted.internet import reactor + thread_pool = ThreadPool() + thread_pool.start() + reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop) + factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler)) + reactor.listenTCP(self.port, factory, interface=self.host) + if not reactor.running: + reactor.run() + + +class DieselServer(ServerAdapter): + """ Untested. """ + + def run(self, handler): + from diesel.protocols.wsgi import WSGIApplication + app = WSGIApplication(handler, port=self.port) + app.run() + + +class GeventServer(ServerAdapter): + """ Untested. Options: + + * `fast` (default: False) uses libevent's http server, but has some + issues: No streaming, no pipelining, no SSL. + * See gevent.wsgi.WSGIServer() documentation for more options. + """ + + def run(self, handler): + from gevent import wsgi, pywsgi, local + if not isinstance(threading.local(), local.local): + msg = "Bottle requires gevent.monkey.patch_all() (before import)" + raise RuntimeError(msg) + if not self.options.pop('fast', None): wsgi = pywsgi + self.options['log'] = None if self.quiet else 'default' + address = (self.host, self.port) + server = wsgi.WSGIServer(address, handler, **self.options) + if 'BOTTLE_CHILD' in os.environ: + import signal + signal.signal(signal.SIGINT, lambda s, f: server.stop()) + server.serve_forever() + + +class GeventSocketIOServer(ServerAdapter): + def run(self, handler): + from socketio import server + address = (self.host, self.port) + server.SocketIOServer(address, handler, **self.options).serve_forever() + + +class GunicornServer(ServerAdapter): + """ Untested. See http://gunicorn.org/configure.html for options. """ + + def run(self, handler): + from gunicorn.app.base import Application + + config = {'bind': "%s:%d" % (self.host, int(self.port))} + config.update(self.options) + + class GunicornApplication(Application): + def init(self, parser, opts, args): + return config + + def load(self): + return handler + + GunicornApplication().run() + + +class EventletServer(ServerAdapter): + """ Untested. Options: + + * `backlog` adjust the eventlet backlog parameter which is the maximum + number of queued connections. Should be at least 1; the maximum + value is system-dependent. + * `family`: (default is 2) socket family, optional. See socket + documentation for available families. + """ + + def run(self, handler): + from eventlet import wsgi, listen, patcher + if not patcher.is_monkey_patched(os): + msg = "Bottle requires eventlet.monkey_patch() (before import)" + raise RuntimeError(msg) + socket_args = {} + for arg in ('backlog', 'family'): + try: + socket_args[arg] = self.options.pop(arg) + except KeyError: + pass + address = (self.host, self.port) + try: + wsgi.server(listen(address, **socket_args), handler, + log_output=(not self.quiet)) + except TypeError: + # Fallback, if we have old version of eventlet + wsgi.server(listen(address), handler) + + +class RocketServer(ServerAdapter): + """ Untested. """ + + def run(self, handler): + from rocket import Rocket + server = Rocket((self.host, self.port), 'wsgi', {'wsgi_app': handler}) + server.start() + + +class BjoernServer(ServerAdapter): + """ Fast server written in C: https://github.com/jonashaag/bjoern """ + + def run(self, handler): + from bjoern import run + run(handler, self.host, self.port) + + +class AiohttpServer(ServerAdapter): + """ Untested. + aiohttp + https://pypi.python.org/pypi/aiohttp/ + """ + + def run(self, handler): + import asyncio + from aiohttp.wsgi import WSGIServerHttpProtocol + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + protocol_factory = lambda: WSGIServerHttpProtocol( + handler, + readpayload=True, + debug=(not self.quiet)) + self.loop.run_until_complete(self.loop.create_server(protocol_factory, + self.host, + self.port)) + + if 'BOTTLE_CHILD' in os.environ: + import signal + signal.signal(signal.SIGINT, lambda s, f: self.loop.stop()) + + try: + self.loop.run_forever() + except KeyboardInterrupt: + self.loop.stop() + + +class AutoServer(ServerAdapter): + """ Untested. """ + adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, + WSGIRefServer] + + def run(self, handler): + for sa in self.adapters: + try: + return sa(self.host, self.port, **self.options).run(handler) + except ImportError: + pass + + +server_names = { + 'cgi': CGIServer, + 'flup': FlupFCGIServer, + 'wsgiref': WSGIRefServer, + 'waitress': WaitressServer, + 'cherrypy': CherryPyServer, + 'paste': PasteServer, + 'fapws3': FapwsServer, + 'tornado': TornadoServer, + 'gae': AppEngineServer, + 'twisted': TwistedServer, + 'diesel': DieselServer, + 'meinheld': MeinheldServer, + 'gunicorn': GunicornServer, + 'eventlet': EventletServer, + 'gevent': GeventServer, + 'geventSocketIO': GeventSocketIOServer, + 'rocket': RocketServer, + 'bjoern': BjoernServer, + 'aiohttp': AiohttpServer, + 'auto': AutoServer, +} + +############################################################################### +# Application Control ########################################################## +############################################################################### + + +def load(target, **namespace): + """ Import a module or fetch an object from a module. + + * ``package.module`` returns `module` as a module object. + * ``pack.mod:name`` returns the module variable `name` from `pack.mod`. + * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result. + + The last form accepts not only function calls, but any type of + expression. Keyword arguments passed to this function are available as + local variables. Example: ``import_string('re:compile(x)', x='[a-z]')`` + """ + module, target = target.split(":", 1) if ':' in target else (target, None) + if module not in sys.modules: __import__(module) + if not target: return sys.modules[module] + if target.isalnum(): return getattr(sys.modules[module], target) + package_name = module.split('.')[0] + namespace[package_name] = sys.modules[package_name] + return eval('%s.%s' % (module, target), namespace) + + +def load_app(target): + """ Load a bottle application from a module and make sure that the import + does not affect the current default application, but returns a separate + application object. See :func:`load` for the target parameter. """ + global NORUN + NORUN, nr_old = True, NORUN + tmp = default_app.push() # Create a new "default application" + try: + rv = load(target) # Import the target module + return rv if callable(rv) else tmp + finally: + default_app.remove(tmp) # Remove the temporary added default application + NORUN = nr_old + + +_debug = debug + + +def run(app=None, + server='wsgiref', + host='127.0.0.1', + port=8080, + interval=1, + reloader=False, + quiet=False, + plugins=None, + debug=None, + config=None, **kargs): + """ Start a server instance. This method blocks until the server terminates. + + :param app: WSGI application or target string supported by + :func:`load_app`. (default: :func:`default_app`) + :param server: Server adapter to use. See :data:`server_names` keys + for valid names or pass a :class:`ServerAdapter` subclass. + (default: `wsgiref`) + :param host: Server address to bind to. Pass ``0.0.0.0`` to listens on + all interfaces including the external one. (default: 127.0.0.1) + :param port: Server port to bind to. Values below 1024 require root + privileges. (default: 8080) + :param reloader: Start auto-reloading server? (default: False) + :param interval: Auto-reloader interval in seconds (default: 1) + :param quiet: Suppress output to stdout and stderr? (default: False) + :param options: Options passed to the server adapter. + """ + if NORUN: return + if reloader and not os.environ.get('BOTTLE_CHILD'): + import subprocess + lockfile = None + try: + fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') + os.close(fd) # We only need this file to exist. We never write to it + while os.path.exists(lockfile): + args = [sys.executable] + sys.argv + environ = os.environ.copy() + environ['BOTTLE_CHILD'] = 'true' + environ['BOTTLE_LOCKFILE'] = lockfile + p = subprocess.Popen(args, env=environ) + while p.poll() is None: # Busy wait... + os.utime(lockfile, None) # I am alive! + time.sleep(interval) + if p.poll() != 3: + if os.path.exists(lockfile): os.unlink(lockfile) + sys.exit(p.poll()) + except KeyboardInterrupt: + pass + finally: + if os.path.exists(lockfile): + os.unlink(lockfile) + return + + try: + if debug is not None: _debug(debug) + app = app or default_app() + if isinstance(app, basestring): + app = load_app(app) + if not callable(app): + raise ValueError("Application is not callable: %r" % app) + + for plugin in plugins or []: + if isinstance(plugin, basestring): + plugin = load(plugin) + app.install(plugin) + + if config: + app.config.update(config) + + if server in server_names: + server = server_names.get(server) + if isinstance(server, basestring): + server = load(server) + if isinstance(server, type): + server = server(host=host, port=port, **kargs) + if not isinstance(server, ServerAdapter): + raise ValueError("Unknown or unsupported server: %r" % server) + + server.quiet = server.quiet or quiet + if not server.quiet: + _stderr("Bottle v%s server starting up (using %s)...\n" % + (__version__, repr(server))) + _stderr("Listening on http://%s:%d/\n" % + (server.host, server.port)) + _stderr("Hit Ctrl-C to quit.\n\n") + + if reloader: + lockfile = os.environ.get('BOTTLE_LOCKFILE') + bgcheck = FileCheckerThread(lockfile, interval) + with bgcheck: + server.run(app) + if bgcheck.status == 'reload': + sys.exit(3) + else: + server.run(app) + except KeyboardInterrupt: + pass + except (SystemExit, MemoryError): + raise + except: + if not reloader: raise + if not getattr(server, 'quiet', quiet): + print_exc() + time.sleep(interval) + sys.exit(3) + + +class FileCheckerThread(threading.Thread): + """ Interrupt main-thread as soon as a changed module file is detected, + the lockfile gets deleted or gets to old. """ + + def __init__(self, lockfile, interval): + threading.Thread.__init__(self) + self.daemon = True + self.lockfile, self.interval = lockfile, interval + #: Is one of 'reload', 'error' or 'exit' + self.status = None + + def run(self): + exists = os.path.exists + mtime = lambda p: os.stat(p).st_mtime + files = dict() + + for module in list(sys.modules.values()): + path = getattr(module, '__file__', '') + if path[-4:] in ('.pyo', '.pyc'): path = path[:-1] + if path and exists(path): files[path] = mtime(path) + + while not self.status: + if not exists(self.lockfile)\ + or mtime(self.lockfile) < time.time() - self.interval - 5: + self.status = 'error' + thread.interrupt_main() + for path, lmtime in list(files.items()): + if not exists(path) or mtime(path) > lmtime: + self.status = 'reload' + thread.interrupt_main() + break + time.sleep(self.interval) + + def __enter__(self): + self.start() + + def __exit__(self, exc_type, *_): + if not self.status: self.status = 'exit' # silent exit + self.join() + return exc_type is not None and issubclass(exc_type, KeyboardInterrupt) + +############################################################################### +# Template Adapters ############################################################ +############################################################################### + + +class TemplateError(HTTPError): + def __init__(self, message): + HTTPError.__init__(self, 500, message) + + +class BaseTemplate(object): + """ Base class and minimal API for template adapters """ + extensions = ['tpl', 'html', 'thtml', 'stpl'] + settings = {} #used in prepare() + defaults = {} #used in render() + + def __init__(self, + source=None, + name=None, + lookup=None, + encoding='utf8', **settings): + """ Create a new template. + If the source parameter (str or buffer) is missing, the name argument + is used to guess a template filename. Subclasses can assume that + self.source and/or self.filename are set. Both are strings. + The lookup, encoding and settings parameters are stored as instance + variables. + The lookup parameter stores a list containing directory paths. + The encoding parameter should be used to decode byte strings or files. + The settings parameter contains a dict for engine-specific settings. + """ + self.name = name + self.source = source.read() if hasattr(source, 'read') else source + self.filename = source.filename if hasattr(source, 'filename') else None + self.lookup = [os.path.abspath(x) for x in lookup] if lookup else [] + self.encoding = encoding + self.settings = self.settings.copy() # Copy from class variable + self.settings.update(settings) # Apply + if not self.source and self.name: + self.filename = self.search(self.name, self.lookup) + if not self.filename: + raise TemplateError('Template %s not found.' % repr(name)) + if not self.source and not self.filename: + raise TemplateError('No template specified.') + self.prepare(**self.settings) + + @classmethod + def search(cls, name, lookup=None): + """ Search name in all directories specified in lookup. + First without, then with common extensions. Return first hit. """ + if not lookup: + depr('The template lookup path list should not be empty.', + True) #0.12 + lookup = ['.'] + + if os.path.isabs(name) and os.path.isfile(name): + depr('Absolute template path names are deprecated.', True) #0.12 + return os.path.abspath(name) + + for spath in lookup: + spath = os.path.abspath(spath) + os.sep + fname = os.path.abspath(os.path.join(spath, name)) + if not fname.startswith(spath): continue + if os.path.isfile(fname): return fname + for ext in cls.extensions: + if os.path.isfile('%s.%s' % (fname, ext)): + return '%s.%s' % (fname, ext) + + @classmethod + def global_config(cls, key, *args): + """ This reads or sets the global settings stored in class.settings. """ + if args: + cls.settings = cls.settings.copy() # Make settings local to class + cls.settings[key] = args[0] + else: + return cls.settings[key] + + def prepare(self, **options): + """ Run preparations (parsing, caching, ...). + It should be possible to call this again to refresh a template or to + update settings. + """ + raise NotImplementedError + + def render(self, *args, **kwargs): + """ Render the template with the specified local variables and return + a single byte or unicode string. If it is a byte string, the encoding + must match self.encoding. This method must be thread-safe! + Local variables may be provided in dictionaries (args) + or directly, as keywords (kwargs). + """ + raise NotImplementedError + + +class MakoTemplate(BaseTemplate): + def prepare(self, **options): + from mako.template import Template + from mako.lookup import TemplateLookup + options.update({'input_encoding': self.encoding}) + options.setdefault('format_exceptions', bool(DEBUG)) + lookup = TemplateLookup(directories=self.lookup, **options) + if self.source: + self.tpl = Template(self.source, lookup=lookup, **options) + else: + self.tpl = Template(uri=self.name, + filename=self.filename, + lookup=lookup, **options) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + _defaults = self.defaults.copy() + _defaults.update(kwargs) + return self.tpl.render(**_defaults) + + +class CheetahTemplate(BaseTemplate): + def prepare(self, **options): + from Cheetah.Template import Template + self.context = threading.local() + self.context.vars = {} + options['searchList'] = [self.context.vars] + if self.source: + self.tpl = Template(source=self.source, **options) + else: + self.tpl = Template(file=self.filename, **options) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + self.context.vars.update(self.defaults) + self.context.vars.update(kwargs) + out = str(self.tpl) + self.context.vars.clear() + return out + + +class Jinja2Template(BaseTemplate): + def prepare(self, filters=None, tests=None, globals={}, **kwargs): + from jinja2 import Environment, FunctionLoader + self.env = Environment(loader=FunctionLoader(self.loader), **kwargs) + if filters: self.env.filters.update(filters) + if tests: self.env.tests.update(tests) + if globals: self.env.globals.update(globals) + if self.source: + self.tpl = self.env.from_string(self.source) + else: + self.tpl = self.env.get_template(self.filename) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + _defaults = self.defaults.copy() + _defaults.update(kwargs) + return self.tpl.render(**_defaults) + + def loader(self, name): + fname = self.search(name, self.lookup) + if not fname: return + with open(fname, "rb") as f: + return f.read().decode(self.encoding) + + +class SimpleTemplate(BaseTemplate): + def prepare(self, + escape_func=html_escape, + noescape=False, + syntax=None, **ka): + self.cache = {} + enc = self.encoding + self._str = lambda x: touni(x, enc) + self._escape = lambda x: escape_func(touni(x, enc)) + self.syntax = syntax + if noescape: + self._str, self._escape = self._escape, self._str + + @cached_property + def co(self): + return compile(self.code, self.filename or '', 'exec') + + @cached_property + def code(self): + source = self.source + if not source: + with open(self.filename, 'rb') as f: + source = f.read() + try: + source, encoding = touni(source), 'utf8' + except UnicodeError: + depr('Template encodings other than utf8 are not supported.') #0.11 + source, encoding = touni(source, 'latin1'), 'latin1' + parser = StplParser(source, encoding=encoding, syntax=self.syntax) + code = parser.translate() + self.encoding = parser.encoding + return code + + def _rebase(self, _env, _name=None, **kwargs): + _env['_rebase'] = (_name, kwargs) + + def _include(self, _env, _name=None, **kwargs): + env = _env.copy() + env.update(kwargs) + if _name not in self.cache: + self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) + return self.cache[_name].execute(env['_stdout'], env) + + def execute(self, _stdout, kwargs): + env = self.defaults.copy() + env.update(kwargs) + env.update({ + '_stdout': _stdout, + '_printlist': _stdout.extend, + 'include': functools.partial(self._include, env), + 'rebase': functools.partial(self._rebase, env), + '_rebase': None, + '_str': self._str, + '_escape': self._escape, + 'get': env.get, + 'setdefault': env.setdefault, + 'defined': env.__contains__ + }) + eval(self.co, env) + if env.get('_rebase'): + subtpl, rargs = env.pop('_rebase') + rargs['base'] = ''.join(_stdout) #copy stdout + del _stdout[:] # clear stdout + return self._include(env, subtpl, **rargs) + return env + + def render(self, *args, **kwargs): + """ Render the template using keyword arguments as local variables. """ + env = {} + stdout = [] + for dictarg in args: + env.update(dictarg) + env.update(kwargs) + self.execute(stdout, env) + return ''.join(stdout) + + +class StplSyntaxError(TemplateError): + + pass + + +class StplParser(object): + """ Parser for stpl templates. """ + _re_cache = {} #: Cache for compiled re patterns + + # This huge pile of voodoo magic splits python code into 8 different tokens. + # We use the verbose (?x) regex mode to make this more manageable + + _re_tok = _re_inl = r'''((?mx) # verbose and dot-matches-newline mode + [urbURB]* + (?: ''(?!') + |""(?!") + |'{6} + |"{6} + |'(?:[^\\']|\\.)+?' + |"(?:[^\\"]|\\.)+?" + |'{3}(?:[^\\]|\\.|\n)+?'{3} + |"{3}(?:[^\\]|\\.|\n)+?"{3} + ) + )''' + + _re_inl = _re_tok.replace(r'|\n', '') # We re-use this string pattern later + + _re_tok += r''' + # 2: Comments (until end of line, but not the newline itself) + |(\#.*) + + # 3: Open and close (4) grouping tokens + |([\[\{\(]) + |([\]\}\)]) + + # 5,6: Keywords that start or continue a python block (only start of line) + |^([\ \t]*(?:if|for|while|with|try|def|class)\b) + |^([\ \t]*(?:elif|else|except|finally)\b) + + # 7: Our special 'end' keyword (but only if it stands alone) + |((?:^|;)[\ \t]*end[\ \t]*(?=(?:%(block_close)s[\ \t]*)?\r?$|;|\#)) + + # 8: A customizable end-of-code-block template token (only end of line) + |(%(block_close)s[\ \t]*(?=\r?$)) + + # 9: And finally, a single newline. The 10th token is 'everything else' + |(\r?\n) + ''' + + # Match the start tokens of code areas in a template + _re_split = r'''(?m)^[ \t]*(\\?)((%(line_start)s)|(%(block_start)s))''' + # Match inline statements (may contain python strings) + _re_inl = r'''%%(inline_start)s((?:%s|[^'"\n]+?)*?)%%(inline_end)s''' % _re_inl + + default_syntax = '<% %> % {{ }}' + + def __init__(self, source, syntax=None, encoding='utf8'): + self.source, self.encoding = touni(source, encoding), encoding + self.set_syntax(syntax or self.default_syntax) + self.code_buffer, self.text_buffer = [], [] + self.lineno, self.offset = 1, 0 + self.indent, self.indent_mod = 0, 0 + self.paren_depth = 0 + + def get_syntax(self): + """ Tokens as a space separated string (default: <% %> % {{ }}) """ + return self._syntax + + def set_syntax(self, syntax): + self._syntax = syntax + self._tokens = syntax.split() + if not syntax in self._re_cache: + names = 'block_start block_close line_start inline_start inline_end' + etokens = map(re.escape, self._tokens) + pattern_vars = dict(zip(names.split(), etokens)) + patterns = (self._re_split, self._re_tok, self._re_inl) + patterns = [re.compile(p % pattern_vars) for p in patterns] + self._re_cache[syntax] = patterns + self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax] + + syntax = property(get_syntax, set_syntax) + + def translate(self): + if self.offset: raise RuntimeError('Parser is a one time instance.') + while True: + m = self.re_split.search(self.source, pos=self.offset) + if m: + text = self.source[self.offset:m.start()] + self.text_buffer.append(text) + self.offset = m.end() + if m.group(1): # Escape syntax + line, sep, _ = self.source[self.offset:].partition('\n') + self.text_buffer.append(self.source[m.start():m.start(1)] + + m.group(2) + line + sep) + self.offset += len(line + sep) + continue + self.flush_text() + self.offset += self.read_code(self.source[self.offset:], + multiline=bool(m.group(4))) + else: + break + self.text_buffer.append(self.source[self.offset:]) + self.flush_text() + return ''.join(self.code_buffer) + + def read_code(self, pysource, multiline): + code_line, comment = '', '' + offset = 0 + while True: + m = self.re_tok.search(pysource, pos=offset) + if not m: + code_line += pysource[offset:] + offset = len(pysource) + self.write_code(code_line.strip(), comment) + break + code_line += pysource[offset:m.start()] + offset = m.end() + _str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups() + if self.paren_depth > 0 and (_blk1 or _blk2): # a if b else c + code_line += _blk1 or _blk2 + continue + if _str: # Python string + code_line += _str + elif _com: # Python comment (up to EOL) + comment = _com + if multiline and _com.strip().endswith(self._tokens[1]): + multiline = False # Allow end-of-block in comments + elif _po: # open parenthesis + self.paren_depth += 1 + code_line += _po + elif _pc: # close parenthesis + if self.paren_depth > 0: + # we could check for matching parentheses here, but it's + # easier to leave that to python - just check counts + self.paren_depth -= 1 + code_line += _pc + elif _blk1: # Start-block keyword (if/for/while/def/try/...) + code_line, self.indent_mod = _blk1, -1 + self.indent += 1 + elif _blk2: # Continue-block keyword (else/elif/except/...) + code_line, self.indent_mod = _blk2, -1 + elif _end: # The non-standard 'end'-keyword (ends a block) + self.indent -= 1 + elif _cend: # The end-code-block template token (usually '%>') + if multiline: multiline = False + else: code_line += _cend + else: # \n + self.write_code(code_line.strip(), comment) + self.lineno += 1 + code_line, comment, self.indent_mod = '', '', 0 + if not multiline: + break + + return offset + + def flush_text(self): + text = ''.join(self.text_buffer) + del self.text_buffer[:] + if not text: return + parts, pos, nl = [], 0, '\\\n' + ' ' * self.indent + for m in self.re_inl.finditer(text): + prefix, pos = text[pos:m.start()], m.end() + if prefix: + parts.append(nl.join(map(repr, prefix.splitlines(True)))) + if prefix.endswith('\n'): parts[-1] += nl + parts.append(self.process_inline(m.group(1).strip())) + if pos < len(text): + prefix = text[pos:] + lines = prefix.splitlines(True) + if lines[-1].endswith('\\\\\n'): lines[-1] = lines[-1][:-3] + elif lines[-1].endswith('\\\\\r\n'): lines[-1] = lines[-1][:-4] + parts.append(nl.join(map(repr, lines))) + code = '_printlist((%s,))' % ', '.join(parts) + self.lineno += code.count('\n') + 1 + self.write_code(code) + + @staticmethod + def process_inline(chunk): + if chunk[0] == '!': return '_str(%s)' % chunk[1:] + return '_escape(%s)' % chunk + + def write_code(self, line, comment=''): + code = ' ' * (self.indent + self.indent_mod) + code += line.lstrip() + comment + '\n' + self.code_buffer.append(code) + + +def template(*args, **kwargs): + """ + Get a rendered template as a string iterator. + You can use a name, a filename or a template string as first parameter. + Template rendering arguments can be passed as dictionaries + or directly (as keyword arguments). + """ + tpl = args[0] if args else None + adapter = kwargs.pop('template_adapter', SimpleTemplate) + lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) + tplid = (id(lookup), tpl) + if tplid not in TEMPLATES or DEBUG: + settings = kwargs.pop('template_settings', {}) + if isinstance(tpl, adapter): + TEMPLATES[tplid] = tpl + if settings: TEMPLATES[tplid].prepare(**settings) + elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: + TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings) + else: + TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) + if not TEMPLATES[tplid]: + abort(500, 'Template (%s) not found' % tpl) + for dictarg in args[1:]: + kwargs.update(dictarg) + return TEMPLATES[tplid].render(kwargs) + + +mako_template = functools.partial(template, template_adapter=MakoTemplate) +cheetah_template = functools.partial(template, + template_adapter=CheetahTemplate) +jinja2_template = functools.partial(template, template_adapter=Jinja2Template) + + +def view(tpl_name, **defaults): + """ Decorator: renders a template for a handler. + The handler can control its behavior like that: + + - return a dict of template vars to fill out the template + - return something other than a dict and the view decorator will not + process the template, but return the handler result as is. + This includes returning a HTTPResponse(dict) to get, + for instance, JSON with autojson or other castfilters. + """ + + def decorator(func): + + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if isinstance(result, (dict, DictMixin)): + tplvars = defaults.copy() + tplvars.update(result) + return template(tpl_name, **tplvars) + elif result is None: + return template(tpl_name, defaults) + return result + + return wrapper + + return decorator + + +mako_view = functools.partial(view, template_adapter=MakoTemplate) +cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) +jinja2_view = functools.partial(view, template_adapter=Jinja2Template) + +############################################################################### +# Constants and Globals ######################################################## +############################################################################### + +TEMPLATE_PATH = ['./', './views/'] +TEMPLATES = {} +DEBUG = False +NORUN = False # If set, run() does nothing. Used by load_app() + +#: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') +HTTP_CODES = httplib.responses.copy() +HTTP_CODES[418] = "I'm a teapot" # RFC 2324 +HTTP_CODES[428] = "Precondition Required" +HTTP_CODES[429] = "Too Many Requests" +HTTP_CODES[431] = "Request Header Fields Too Large" +HTTP_CODES[511] = "Network Authentication Required" +_HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v)) + for (k, v) in HTTP_CODES.items()) + +#: The default template used for error pages. Override with @error() +ERROR_PAGE_TEMPLATE = """ +%%try: + %%from %s import DEBUG, request + + + + Error: {{e.status}} + + + +

    Error: {{e.status}}

    +

    Sorry, the requested URL {{repr(request.url)}} + caused an error:

    +
    {{e.body}}
    + %%if DEBUG and e.exception: +

    Exception:

    +
    {{repr(e.exception)}}
    + %%end + %%if DEBUG and e.traceback: +

    Traceback:

    +
    {{e.traceback}}
    + %%end + + +%%except ImportError: + ImportError: Could not generate the error page. Please add bottle to + the import path. +%%end +""" % __name__ + +#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a +#: request callback, this instance always refers to the *current* request +#: (even on a multi-threaded server). +request = LocalRequest() + +#: A thread-safe instance of :class:`LocalResponse`. It is used to change the +#: HTTP response for the *current* request. +response = LocalResponse() + +#: A thread-safe namespace. Not used by Bottle. +local = threading.local() + +# Initialize app stack (create first empty Bottle app) +# BC: 0.6.4 and needed for run() +app = default_app = AppStack() +app.push() + +#: A virtual package that redirects import statements. +#: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. +ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else + __name__ + ".ext", 'bottle_%s').module + + + +if __name__ == '__main__': + opt, args, parser = _cli_parse(sys.argv) + + def _cli_error(msg): + parser.print_help() + _stderr('\nError: %s\n' % msg) + sys.exit(1) + + if opt.version: + _stdout('Bottle %s\n' % __version__) + sys.exit(0) + if not args: + _cli_error("No application entry point specified.") + + sys.path.insert(0, '.') + sys.modules.setdefault('bottle', sys.modules['__main__']) + + host, port = (opt.bind or 'localhost'), 8080 + if ':' in host and host.rfind(']') < host.rfind(':'): + host, port = host.rsplit(':', 1) + host = host.strip('[]') + + config = ConfigDict() + + for cfile in opt.conf or []: + try: + if cfile.endswith('.json'): + with open(cfile, 'rb') as fp: + config.load_dict(json_loads(fp.read())) + else: + config.load_config(cfile) + except ConfigParserError: + _cli_error(str(_e())) + except IOError: + _cli_error("Unable to read config file %r" % cfile) + except (UnicodeError, TypeError, ValueError): + _cli_error("Unable to parse config file %r: %s" % (cfile, _e())) + + for cval in opt.param or []: + if '=' in cval: + config.update((cval.split('=', 1),)) + else: + config[cval] = True + + run(args[0], + host=host, + port=int(port), + server=opt.server, + reloader=opt.reload, + plugins=opt.plugin, + debug=opt.debug, + config=config) + +# THE END \ No newline at end of file diff --git a/_src/om2py6w/6wex0/sae/config.py b/_src/om2py6w/6wex0/sae/config.py new file mode 100644 index 000000000..e82fde5fc --- /dev/null +++ b/_src/om2py6w/6wex0/sae/config.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +import sys +import os.path + +app_root = os.path.dirname(__file__) +sys.path.insert(0, os.path.join(app_root, "module/")) +sys.path.insert(0, os.path.join(app_root, "web/")) + +class Borg(): + '''base http://blog.youxu.info/2010/04/29/borg + - 单例式配置收集类 + ''' + __collective_mind = {} + def __init__(self): + self.__dict__ = self.__collective_mind + + TPL_TEXT=''' + + + %(tStamp)s + + + ''' + +CFG = Borg() \ No newline at end of file diff --git a/_src/om2py6w/6wex0/sae/config.yaml b/_src/om2py6w/6wex0/sae/config.yaml new file mode 100644 index 000000000..29b8dcfa7 --- /dev/null +++ b/_src/om2py6w/6wex0/sae/config.yaml @@ -0,0 +1,2 @@ +name: beiyastudy +version: 1 \ No newline at end of file diff --git a/_src/om2py6w/6wex0/sae/index.wsgi b/_src/om2py6w/6wex0/sae/index.wsgi new file mode 100644 index 000000000..9441b6330 --- /dev/null +++ b/_src/om2py6w/6wex0/sae/index.wsgi @@ -0,0 +1,8 @@ +# -*- coding:utf-8 -*- +from bottle import * +import sae + +import config +from web import APP + +application = sae.create_wsgi_app(APP) \ No newline at end of file diff --git a/_src/om2py6w/6wex2/main.py b/_src/om2py6w/6wex0/sae/module/utility.py similarity index 100% rename from _src/om2py6w/6wex2/main.py rename to _src/om2py6w/6wex0/sae/module/utility.py diff --git a/_src/om2py7w/7wex0/__init__.py b/_src/om2py6w/6wex0/sae/templates/404.html similarity index 100% rename from _src/om2py7w/7wex0/__init__.py rename to _src/om2py6w/6wex0/sae/templates/404.html diff --git a/_src/om2py7w/7wex0/main.py b/_src/om2py6w/6wex0/sae/templates/base.html similarity index 100% rename from _src/om2py7w/7wex0/main.py rename to _src/om2py6w/6wex0/sae/templates/base.html diff --git a/_src/om2py6w/6wex0/sae/web/__init__.py b/_src/om2py6w/6wex0/sae/web/__init__.py new file mode 100644 index 000000000..2fde81767 --- /dev/null +++ b/_src/om2py6w/6wex0/sae/web/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from bottle import * + +APP = Bottle() +APP.mount('/api', __import__('mana4api').APP) \ No newline at end of file diff --git a/_src/om2py6w/6wex0/sae/web/mana4api.py b/_src/om2py6w/6wex0/sae/web/mana4api.py new file mode 100644 index 000000000..1aaa81b10 --- /dev/null +++ b/_src/om2py6w/6wex0/sae/web/mana4api.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +import sae +from bottle import * +from config import CFG +import time + +import xml.etree.ElementTree as ET + +import hashlib +import sae.kvdb + +KV = sae.kvdb.KVClient() + +debug(True) +APP = Bottle() + +def _help(): + ''' 是也乎, 俺是 极简日志交互 帮助文档: + - 0 查看俺 输入 h + - 1 想输入笔记 按这样的格式 {n:这是我的笔记} + 注意 不包括{} & :是英文冒号 是也乎 + - 2 想看你输入的所有历史笔记 请输入 hist + - 3 想清除你的所有数据? 请输入 del ''' + +def _save_note(userid, note, timestamp): + + notekey = timestamp # 不同时刻笔记 set 字典中的key one by one + note_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(timestamp))) + usr = KV.get(userid) + usr[note_time] = note + KV.replace(userid, usr) + +def _hist_note(userid): + result = [] + usr = KV.get(userid) # 返回字典是无序的 + sorted_usr = sorted(usr) # sorted is list then + for key in sorted_usr: # key is the time key in kvdb + v = key + "\n" + usr[key] + result.append(v) + result_str = "\n".join(result) + return result_str + +# message wechat +@APP.post('/echo/') +def wechat_post(): + # print request.forms.keys()[0] # get xml message from wechat setdata + xml = ET.XML(request.forms.keys()[0]) + fromUser = xml.findtext("ToUserName") + toUser = xml.findtext("FromUserName") + __MsgTpye = xml.findtext("MsgType") + __Content = xml.findtext("Content") + tStamp = xml.findtext("CreateTime") # str timestampe convert to strtime need to int(tStamp) + print type(tStamp) + print tStamp + + if "text" == __MsgTpye: + if "h" == __Content: + content = _help.__doc__ # parameters should be the same in CFG.TPL_TEXT + # print CFG.TPL_TEXT% locals() # in sae debug can print + return CFG.TPL_TEXT% locals() # Use CFG in config.py, post to wechat + elif "l" == __Content: + content = "Would you like be my girlfriend? yes or no?" + return CFG.TPL_TEXT% locals() + elif "yes" == __Content: + content = ":-) Thank God\nI finally find you." + return CFG.TPL_TEXT% locals() + elif "no" == __Content: + content = ":-) Waiting you! :-) " + return CFG.TPL_TEXT% locals() + elif "n" in __Content.split(":"): + uid = hashlib.sha1(toUser).hexdigest() + usr = KV.get(uid) + if None == usr: + KV.set(uid, {}) + note_str = __Content[2:] + _save_note(uid, note_str, tStamp) + else: + note_str = __Content[2:] + _save_note(uid, note_str, tStamp) + content = "已经存入:-)\n还有很多idea?\n是也乎-继续输入:-)" + return CFG.TPL_TEXT% locals() + elif "hist" == __Content: + uid = hashlib.sha1(toUser).hexdigest() + content = _hist_note(uid) + return CFG.TPL_TEXT% locals() + elif "del" == __Content: + uid = hashlib.sha1(toUser).hexdigest() + KV.delete(uid) + content = "已经清除 所有数据\n想要重新记录?\n输入 n:这里添加你的笔记试试" + return CFG.TPL_TEXT% locals() + else: + content = _help.__doc__ + return CFG.TPL_TEXT% locals() + return None + + # print xml.findtext("Content") \ No newline at end of file diff --git a/_src/om2py7w/7wex1/main.py b/_src/om2py6w/6wex0/sae/web/mana4cli.py similarity index 100% rename from _src/om2py7w/7wex1/main.py rename to _src/om2py6w/6wex0/sae/web/mana4cli.py diff --git a/_src/om2py6w/README.md b/_src/om2py6w/README.md deleted file mode 100644 index 61deaa78c..000000000 --- a/_src/om2py6w/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# OMOOC.py 周任务代码试作 - -## 6w - -- 私人笔记: - + 移动 web 应用 \ No newline at end of file diff --git a/_src/om2py7w/7wex0/README.md b/_src/om2py7w/7wex0/README.md new file mode 100644 index 000000000..27c4f6029 --- /dev/null +++ b/_src/om2py7w/7wex0/README.md @@ -0,0 +1,18 @@ +# OMOOC.py Qpython + +## 7w Qpython 日志交互 + +- 私人笔记:[日记交互 移动版 Qpython](https://jeremiahzhang.gitbooks.io/omooc2py/content/2nDev/week07_qpy.html) + +## 使用 + +- 下载Qpython + - 安装 sqlite3 包 +- 将本目录下 放入 Qpython项目文件下 + - qpymain.py + - creatdb.py + - home.html +- 执行 + - 首先运行 creatdb.py 生成mydiary.db数据文件 + - 然后点击qpymain.py 运行 进行日志交互 + diff --git a/_src/om2py7w/7wex0/creatdb.py b/_src/om2py7w/7wex0/creatdb.py new file mode 100644 index 000000000..5050eed2d --- /dev/null +++ b/_src/om2py7w/7wex0/creatdb.py @@ -0,0 +1,12 @@ +# -*- coding:utf-8 -*- +import sqlite3 +import os + +ROOT = os.path.dirname(os.path.abspath(__file__)) + + +conn = sqlite3.connect(ROOT+'/mydiary.db') +c = conn.cursor() +c.execute("CREATE TABLE diarys(diarytag text, diary_date text, diary_content text)") +conn.commit +c.close() \ No newline at end of file diff --git a/_src/om2py7w/7wex0/fabfile.py b/_src/om2py7w/7wex0/fabfile.py new file mode 100644 index 000000000..b9cc11fce --- /dev/null +++ b/_src/om2py7w/7wex0/fabfile.py @@ -0,0 +1,70 @@ +from fabric.api import local, env, run +#import os +env.shell = "/system/bin/sh -c" +# Local path configuration (can be absolute or relative to fabfile) +# +# Remote server configuration +PY = '/data/data/com.hipipal.qpyplus/files/bin/python' +PYQ = 'root@192.168.2.101' +env.hosts= [PYQ] +env.user = "root" +PYQ_ROOT = '/storage/sdcard0/com.hipipal.qpyplus/projects' +PROJ_NAME = 'chaos' +CRT_PROJ = "%(PYQ_ROOT)s/%(PROJ_NAME)s"% locals() +SCP_UP = "scp *.py %(PYQ)s:%(CRT_PROJ)s/ "% locals() + +# Actions define. +#def pushproj(ports='22', name='chaos'): +def pushproj(): + '''scp all .py into Android QPython projects dir + ''' + print SCP_UP + local(SCP_UP) + +def qpy_run_it(script="hello.py"): + '''fab qpy_run_it:script=MY.py + ''' + run('pwd') + run('ls -la ./') + #run('export PYTHONHOME=/data/data/com.hipipal.qpyplus/files') + print '%s %s/%s'% (PY, CRT_PROJ, script) + #run('%s %s/%s'% (PY, CRT_PROJ, script)) + run('source %s/qpy_profile && %s %s/%s'% (PYQ_ROOT + , PY + , CRT_PROJ + , script + )) + #run('%s %s/%s'% (PY, CRT_PROJ, script)) + +'''main develop loop usage : +$ fab qpy:script=MY_developing.py +so fab will auto: + - scp all local .py up into mobile QPython projects fold + - and source right sys. env + - and call the 'MY_developing.py' + - so wiil see the script running in mobile desktop ;-) +''' +def qpy(script="hello.py"): + '''main develop tools, auto upload and running in Android + ''' + pushproj() + qpy_run_it(script) + env() + +def uname(): + '''print Android sys. info. + ''' + run('uname -a') + +def env(): + '''print Android sys. env + ''' + print 'source %s/qpy_profile'% PYQ_ROOT + run('env') + #run('source %s/qpy_profile && env'% PYQ_ROOT) + +def genenv(script="gen_env.py"): + '''gen qpy need env into: /storage/sdcard0/com.hipipal.qpyplus/projects/qpy_profile + ''' + qpy_run_it(script) + run('ls -la %s'% CRT_PROJ) diff --git a/_src/om2py7w/7wex0/gen_qpy_env.py b/_src/om2py7w/7wex0/gen_qpy_env.py new file mode 100644 index 000000000..4d15d9beb --- /dev/null +++ b/_src/om2py7w/7wex0/gen_qpy_env.py @@ -0,0 +1,44 @@ +#qpy:console +'''for QPy can usage normal system python , gen. all need Adnorid /etc/profile +usage: + - upload this script into mobile + - normal in /storage/sdcard0/com.hipipal.qpyplus/projects/YouProject + - call in QPython "My QPython->projects->YouProject->gen_qpy_env.py" +config as default env: +# mount -o remount,rw /dev/block/mtdblock3 /system +# ln -s /storage/sdcard0/com.hipipal.qpyplus/projects/qpy_profil /etc/profile +# mount -o remount,ro /dev/block/mtdblock3 /system +so every time restart SSH in Android, will load the /etc/profile +can test every thing is good now: +# python +Python 2.7.2 (default, Dec 27 2013, 23:19:48) +[GCC 4.6 20120106 (prerelease)] on linux2 +Type "help", "copyright", "credits" or "license" for more information. +>>> +''' +from os import environ as env +_exp = "/storage/sdcard0/com.hipipal.qpyplus/projects/qpy_profile" +_pre = "export" + +fd = open(_exp, "w") +fd.write("%s PATH=%s\n" % (_pre, env.get("PATH") )) + +fd.write("%s PYTHONHOME=%s\n" % (_pre, env.get("PYTHONHOME") )) +fd.write("%s PYTHONPATH=%s\n" % (_pre, env.get("PYTHONPATH") )) +fd.write("%s PYTHONOPTIMIZE=%s\n" % (_pre, env.get("PYTHONOPTIMIZE") )) + +fd.write("%s AP_HOST=%s\n" % (_pre, env.get("AP_HOST") )) +fd.write("%s AP_PORT=%s\n" % (_pre, env.get("AP_PORT") )) +fd.write("%s AP_HANDSHAKE=%s\n" % (_pre, env.get("AP_HANDSHAKE") )) + +fd.write("%s ANDROID_PUBLIC=%s\n" % (_pre, env.get("ANDROID_PUBLIC") )) +fd.write("%s ANDROID_PRIVATE=%s\n" % (_pre, env.get("ANDROID_PRIVATE") )) +fd.write("%s ANDROID_ARGUMENT=%s\n" % (_pre, env.get("ANDROID_ARGUMENT") )) +fd.write("%s LD_LIBRARY_PATH=%s\n" % (_pre, env.get("LD_LIBRARY_PATH") )) +fd.write("%s TERM=%s\n" % (_pre, env.get("TERM") )) +fd.write("%s TMPDIR=%s\n" % (_pre, env.get("TMPDIR") )) + +print '''gen all nedd env in qpy:console +exp. as %s +enjoy it ;-) +'''% _exp \ No newline at end of file diff --git a/_src/om2py7w/7wex0/home.html b/_src/om2py7w/7wex0/home.html new file mode 100644 index 000000000..aaf1b836c --- /dev/null +++ b/_src/om2py7w/7wex0/home.html @@ -0,0 +1,19 @@ + + + + MyDiary + + + + + +

    welcome to your diary WebAPP:

    +
    + 标签:
    + 日志: + +
    +

    Darling!Here is your diary content!

    + + + \ No newline at end of file diff --git a/_src/om2py7w/7wex0/qpycli.py b/_src/om2py7w/7wex0/qpycli.py new file mode 100644 index 000000000..09f62e52e --- /dev/null +++ b/_src/om2py7w/7wex0/qpycli.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +''' +Qpython webapp: Diary APP CLI control of @Jeremiah Zhang +Email: zhangleisuda@gmail.com +Version 1.0 +''' +import xml.etree.ElementTree as ET + +TPL_TEXT=''' + + +%(tStamp)s + + +''' diff --git a/_src/om2py7w/7wex0/qpymain.py b/_src/om2py7w/7wex0/qpymain.py new file mode 100644 index 000000000..5334389be --- /dev/null +++ b/_src/om2py7w/7wex0/qpymain.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +#qpy:2 +#qpy:webapp:Mydiary APP +#qpy://127.0.0.1:8081/ +''' +Qpython webapp: Diary APP of @Jeremiah Zhang +Email: zhangleisuda@gmail.com +Version 1.0 +''' +# 全局引用 +import os +import sqlite3 +from bottle import Bottle, ServerAdapter +from bottle import route, run, template, request, debug +import time + +### 常量定义 ### +ROOT = os.path.dirname(os.path.abspath(__file__)) + +""" +由于默认的 bottle 在处理退出时比较难出来, +所以我们引入了自定义的 MyWSGIRefServer, +这能很好实现自我关闭 +""" +### qpython web server ### +class MyWSGIRefServer(ServerAdapter): + server = None + + def run(self, handler): + from wsgiref.simple_server import make_server, WSGIRequestHandler + if self.quiet: + class QuietHandler(WSGIRequestHandler): + def log_request(*args, **kw): pass + self.options['handler_class'] = QuietHandler + self.server = make_server(self.host, self.port, handler, **self.options) + self.server.serve_forever() + + def stop(self): + #sys.stderr.close() + import threading + threading.Thread(target=self.server.shutdown).start() + #self.server.shutdown() + self.server.server_close() + print "# QWEB" + + +### Build-in routers ### +def __exit(): + global server + server.stop() + +# 健康监测 +def __ping(): + return "ok" + +def _save_diary(data): + # save into db + db_conn = sqlite3.connect(ROOT + '/mydiary.db') + c = db_conn.cursor() + c.execute('INSERT INTO diarys VALUES (?,?,?)', data) + db_conn.commit() + db_conn.close() + +def _hist_diary(): + db_conn = sqlite3.connect(ROOT + '/mydiary.db') + c = db_conn.cursor() + c.execute('SELECT * FROM diarys ORDER BY diary_date') + content = c.fetchall() + db_conn.close() + + str_content = "\n".join("%s,%s,%s" % tup for tup in content) # unicode format + return str_content + +# webapp routers +app = Bottle() + +@app.route('/') +@app.route('/home', method="GET") +def write(): + + if request.GET.get('save','').strip(): + diary_tag = request.GET.get('tag') + diary_content = request.GET.get('content') + t = time.time() + timestamp = int(t) + diary_date = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp)) + data= (diary_tag.decode('utf-8'), diary_date, diary_content.decode('utf-8')) + _save_diary(data) + history = _hist_diary() + return template(ROOT + '/home.html', log_content=history) + else: + history = _hist_diary() + return template(ROOT + '/home.html', log_content=history) + +app.route('/__exit', method=['GET', 'HEAD'])(__exit) +app.route('/__ping', method=['GET', 'HEAD'])(__ping) +try: + server = MyWSGIRefServer(host="127.0.0.1", port="8081") + app.run(server=server, reloader=True, debug=True) +except Exception,ex: + print "Exception: %s" % repr(ex) \ No newline at end of file diff --git a/_src/om2py7w/7wex0/try/baidu.tpl b/_src/om2py7w/7wex0/try/baidu.tpl new file mode 100644 index 000000000..4f5fde7bd --- /dev/null +++ b/_src/om2py7w/7wex0/try/baidu.tpl @@ -0,0 +1,120 @@ + + + + + + +百度地图API自定义地图 + + + + + + + +
    + + + \ No newline at end of file diff --git a/_src/om2py7w/7wex0/try/hello.py b/_src/om2py7w/7wex0/try/hello.py new file mode 100644 index 000000000..07b147ae5 --- /dev/null +++ b/_src/om2py7w/7wex0/try/hello.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +import androidhelper +droid = androidhelper.Android() +droid.makeToast('Hello, JeremiahZhang :-) TEST warnings!') \ No newline at end of file diff --git a/_src/om2py7w/7wex0/try/mini-web.py b/_src/om2py7w/7wex0/try/mini-web.py new file mode 100644 index 000000000..a13441ad5 --- /dev/null +++ b/_src/om2py7w/7wex0/try/mini-web.py @@ -0,0 +1,40 @@ +#-*-coding:utf8;-*- +#qpy:console +#Author:QPython Developer +# +import urllib2 +import androidhelper +import web +import socket +urls = ( + '/location/(.*)', 'location', + '/(.*)', 'hello' +) + +App = web.application(urls, globals()) +Droid = androidhelper.Android() +localIP = socket.gethostbyname(socket.gethostname()) +externalIP = '0.0.0.0' + +class hello: + def GET(self, name): + Droid.vibrate() # tell self that some one was access + Droid.makeToast("User <%s> vist Home" % web.ctx.ip) + return "Hello from my phone, You can follow my location : )" + +class location: + def GET(self, name): + Droid.vibrate() # tell self that some one was access + Droid.makeToast("User <%s> visit Location" % web.ctx.ip) + location = Droid.getLastKnownLocation().result + location = location.get('network', location.get('gps')) + + return "Back to Homepage

    I am here

    " % (location['latitude'],location['longitude'],location['latitude'],location['longitude']) + + + +if __name__ == "__main__": + info = "Web Server serve at (%s:%s)" % (externalIP,'8080') + Droid.makeToast(info) + + App.run() diff --git a/_src/om2py7w/7wex0/try/testmain.py b/_src/om2py7w/7wex0/try/testmain.py new file mode 100644 index 000000000..f5beac2d4 --- /dev/null +++ b/_src/om2py7w/7wex0/try/testmain.py @@ -0,0 +1,66 @@ +#-*- coding:utf-8 -*- +#qpy:2 +#qpy:webapp:Sample +#qpy://localhost:8080/ +""" +This is a sample for qpython webapp +""" +import os.path +from bottle import Bottle, ServerAdapter +from bottle import template,request,response,redirect,HTTPResponse +root = os.path.dirname(os.path.abspath(__file__)) + +import androidhelper +Droid = androidhelper.Android() +Droid.startLocating(5000,5) + +class MyWSGIRefServer(ServerAdapter): + server = None + def run(self, handler): + from wsgiref.simple_server import make_server, WSGIRequestHandler + if self.quiet: + class QuietHandler(WSGIRequestHandler): + def log_request(*args, **kw): pass + self.options['handler_class'] = QuietHandler + self.server = make_server(self.host, self.port, handler, **self.options) + self.server.serve_forever() + + def stop(self): + #sys.stderr.close() + import threading + threading.Thread(target=self.server.shutdown).start() + #self.server.shutdown() + self.server.server_close() #<--- alternative but causes bad fd exception + print "# qpyhttpd stop" + +def __exit(): + Droid.stopLocating() + global server + server.stop() + +def __ping(): + return "ok" + + +def index(): + Droid.vibrate() + return """""" + +def hello(): + location = Droid.getLastKnownLocation().result + location = location.get('network', location.get('gps')) # you should open gps in your phone or it will NoneType + # location = {"latitude":"116.387884","longitude":"39.929986"} + return template(root+'/baidu.tpl',lat=location['latitude'],lng=location['longitude']) + +if __name__ == '__main__': + app = Bottle() + app.route('/', method='GET')(index) + app.route('/hello', method='GET')(hello) + app.route('/__exit', method=['GET','HEAD'])(__exit) + app.route('/__ping', method=['GET','HEAD'])(__ping) + + try: + server = MyWSGIRefServer(host="127.0.0.1", port="8080") + app.run(server=server,reloader=False) + except Exception,ex: + print "Exception: %s" % repr(ex) \ No newline at end of file diff --git a/_src/om2py7w/7wex2/main.py b/_src/om2py7w/7wex2/main.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/_src/om2py7w/7wex2/__init__.py b/_src/om2py7w/7wex2_sql_try/__init__.py similarity index 100% rename from _src/om2py7w/7wex2/__init__.py rename to _src/om2py7w/7wex2_sql_try/__init__.py diff --git a/_src/om2py7w/7wex2_sql_try/main.py b/_src/om2py7w/7wex2_sql_try/main.py new file mode 100644 index 000000000..a5f3702b6 --- /dev/null +++ b/_src/om2py7w/7wex2_sql_try/main.py @@ -0,0 +1,32 @@ +# -*- coding:utf-8 -*- +import sqlite3 +conn = sqlite3.connect("ex.db") +c = conn.cursor() + +# create table +"""c.execute("CREATE TABLE stocks \ + (date text, trans text, symbol text, qty real, price real)") + +# insert a row of data refer to the execute above +c.execute("INSERT INTO stocks Values ('2015-12-02', 'BUY', 'RHAT', 100, 35.14)") + +# save or ci the changes +conn.commit()""" + +"""# do this +t = ('RHAT',) +c.execute('SELECT * FROM stocks WHERE symbol=?', t) +print c.fetchone()""" + +# Larger example that inserts many records at a time + +purchases = [('2015-12-04', 'BUY', 'IBM', 1000, 45.00), + ] + +c.executemany('INSERT INTO stocks VALUES (?,?,?,?,?)', purchases) +conn.commit() + +print '*'*10 +print c.fetchall() +# close +conn.close() \ No newline at end of file diff --git a/_src/om2py7w/README.md b/_src/om2py7w/README.md deleted file mode 100644 index c3261f148..000000000 --- a/_src/om2py7w/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# OMOOC.py 周任务代码试作 - -## 7w - -- 私人笔记: - + 多端复合 私人笔记 \ No newline at end of file diff --git a/_src/om2pyItem/README.md b/_src/om2pyItem/README.md index 882682723..4766babac 100644 --- a/_src/om2pyItem/README.md +++ b/_src/om2pyItem/README.md @@ -1,6 +1,106 @@ -# OMOOC.py 周任务团队试作 +# 爱搭配 APP -## 10w +- 项目 + - 中文名: 爱搭配 + - 英文名: Imatch +- 实现功能(最小功能) + - 默认页面: 3个图片框(衣服-裤子-鞋子) + - 添加图片 + - 显示图片 + - 含保存 + - 保存时自动截图 + - 开始新一轮搭配 +- me: 主要负责开发 Android 移动app版 + +## 技术难点 + +已知 + +- APP 网页框架 + - 图片添加框效果 + - 读取手机图片 +- 数据库(到底要不要用数据库呢 不是很清晰) + - 添加在图片框中的图片是放在数据库呢? + - 还是直接保存在 相关文件目录中? +- 关于图片: + - 图片大小 比例 要不要限制 +- 保存功能 + - 保存自动截图 +- 编译成APK + +## 任务分解 + +- [x] Qpython 基本框架 +- [] APP web 设置 + - [x] 上载图片 use bottle + - [] 主要功能 + - [x] 页面的 layout html + - [x] 添加图片 + - [x] 实现图片添加功能 + - [x] 添加图片的显示 + - [] 保存功能 截图 +> 这里面思路是 使用jinja2 使用父模板和子模板 来实现 +现在 就是在攻克这个问题:(在这里卡壳了 技术还不够) +0-1个layout.html +1-3个添加图片 使用 button +2-每个button之后 就显示添加的图片 +3-图片上载使用 input file 通过 request 来get 图片 并要显示 + +- [] 数据库 +- [] 编译成APK + +## 迭代进行 + +### 8w +- 8wd0 目标初步分解 Qpython webapp 开发框架确定 +- 8wd2 + - [x] 图片上载 + - 参考 [google keywords search](https://www.google.com.sg/search?client=ubuntu&channel=fs&q=python+bottle+image+upload&ie=utf-8&oe=utf-8&gfe_rd=cr&ei=ZItmVprZD-yW8QeR84TwBg) + - [bottle simple example](https://gist.github.com/Arthraim/994641) + - succeed **host and port should be noted** + - [html forms](http://www.w3schools.com/html/html_forms.asp) + - [div](http://www.w3schools.com/html/html_classes.asp) +- 8wd3 + - [x] 构思如何行 + - 使用button来link多个template + - [x] [android app layout](http://www.idangero.us/framework7/docs/app-layout.html#basic-android-material-app-layout) + - [x] [Web Fundamentals](https://developers.google.com/web/fundamentals/?hl=en) this need to dive in + - [x]forms +- 8wd4 + - [x] 可以成功添加图片 + - ![添加图片成功](http://dn-jeremiahzhang.qbox.me/imatch_index.png) + - [x] 问题:8wd5 解决 在 local 浏览器显示成功 + - [x] 在主index 页面 显示图片 template 中 src = {{ item }} need to be solved (use route static file) + - [x] 已经存在的图片 需要添加识别 不在存入本地 目录中 直接显示 +- 8wd5 显示图片成功 每添加一张就可以显示 使用图片存储在了本地 + - [x] 深入理解 bottle route 的 dynamic route + - [x] static file 解决添加后一张图片显示问题 + - [x] 添加2-3 多张后 同时显示添加的图片该如何处理呢 + - [x] dynaimc route [request-routing](http://bottlepy.org/docs/dev/routing.html#request-routing) +> - /action/ +> - /action/ +> - / + - [x] [filter can be this! wildcard](http://bottlepy.org/docs/dev/routing.html#wildcard-filters) + - 可以自己添加 filter + - 自己对 router 进行配置 [Explicit routing configuration](http://bottlepy.org/docs/dev/routing.html#explicit-routing-configuration) + - [x] use python loop in template + - 效果![显示图片成功](http://dn-jeremiahzhang.qbox.me/imatch_showimg.png) + - **Qpython** + - [x] 问题 bottle 没有 jinja2 模块 + - bottle 推到Qpython 项目文件目录下 未果 + - qpython pip install jinja2 +- 8wd6 + - [] HTML 和 CSS 美化 这个可以挪到最后 恩 + - [] 退出和保存 + - [] 保存截图 + - [X] 确定 [PILLOW](https://pillow.readthedocs.org/en/3.0.x/handbook/tutorial.html) + - [] 功能实现 + - [] layout save button + - [] 截图 route 并返回 index html 页面 + +## 8-10w - 作品测试 -- 集成调试 \ No newline at end of file +- 集成调试 + + diff --git a/_src/om2pyItem/iMatch/bottle.py b/_src/om2pyItem/iMatch/bottle.py new file mode 100644 index 000000000..dcd20136a --- /dev/null +++ b/_src/om2pyItem/iMatch/bottle.py @@ -0,0 +1,4011 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Bottle is a fast and simple micro-framework for small web applications. It +offers request dispatching (Routes) with URL parameter support, templates, +a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and +template engines - all in a single file and with no dependencies other than the +Python Standard Library. + +Homepage and documentation: http://bottlepy.org/ + +Copyright (c) 2014, Marcel Hellkamp. +License: MIT (see LICENSE for details) +""" + +from __future__ import with_statement +import sys + +__author__ = 'Marcel Hellkamp' +__version__ = '0.13-dev' +__license__ = 'MIT' + +############################################################################### +# Command-line interface ######################################################## +############################################################################### +# INFO: Some server adapters need to monkey-patch std-lib modules before they +# are imported. This is why some of the command-line handling is done here, but +# the actual call to main() is at the end of the file. + + +def _cli_parse(args): + from optparse import OptionParser + parser = OptionParser( + usage="usage: %prog [options] package.module:app") + opt = parser.add_option + opt("--version", action="store_true", help="show version number.") + opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") + opt("-s", "--server", default='wsgiref', help="use SERVER as backend.") + opt("-p", "--plugin", action="append", help="install additional plugin/s.") + opt("-c", "--conf", action="append", metavar="FILE", + help="load config values from FILE.") + opt("-C", "--param", action="append", metavar="NAME=VALUE", + help="override config values.") + opt("--debug", action="store_true", help="start server in debug mode.") + opt("--reload", action="store_true", help="auto-reload on file changes.") + opts, args = parser.parse_args(args[1:]) + + return opts, args, parser + + +def _cli_patch(args): + opts, _, _ = _cli_parse(args) + if opts.server: + if opts.server.startswith('gevent'): + import gevent.monkey + gevent.monkey.patch_all() + elif opts.server.startswith('eventlet'): + import eventlet + eventlet.monkey_patch() + + +if __name__ == '__main__': + _cli_patch(sys.argv) + +############################################################################### +# Imports and Python 2/3 unification ########################################### +############################################################################### + + +import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\ + os, re, tempfile, threading, time, warnings + +from types import FunctionType +from datetime import date as datedate, datetime, timedelta +from tempfile import TemporaryFile +from traceback import format_exc, print_exc +from unicodedata import normalize + +# inspect.getargspec was removed in Python 3.6, use +# Signature-based version where we can (Python 3.3+) +try: + from inspect import signature + def getargspec(func): + params = signature(func).parameters + args, varargs, keywords, defaults = [], None, None, [] + for name, param in params.items(): + if param.kind == param.VAR_POSITIONAL: + varargs = name + elif param.kind == param.VAR_KEYWORD: + keywords = name + else: + args.append(name) + if param.default is not param.empty: + defaults.append(param.default) + return (args, varargs, keywords, tuple(defaults) or None) +except ImportError: + from inspect import getargspec + +try: + from simplejson import dumps as json_dumps, loads as json_lds +except ImportError: # pragma: no cover + try: + from json import dumps as json_dumps, loads as json_lds + except ImportError: + try: + from django.utils.simplejson import dumps as json_dumps, loads as json_lds + except ImportError: + + def json_dumps(data): + raise ImportError( + "JSON support requires Python 2.6 or simplejson.") + + json_lds = json_dumps + +# We now try to fix 2.5/2.6/3.1/3.2 incompatibilities. +# It ain't pretty but it works... Sorry for the mess. + +py = sys.version_info +py3k = py >= (3, 0, 0) +py25 = py < (2, 6, 0) +py31 = (3, 1, 0) <= py < (3, 2, 0) + +# Workaround for the missing "as" keyword in py3k. +def _e(): + return sys.exc_info()[1] + +# Workaround for the "print is a keyword/function" Python 2/3 dilemma +# and a fallback for mod_wsgi (resticts stdout/err attribute access) +try: + _stdout, _stderr = sys.stdout.write, sys.stderr.write +except IOError: + _stdout = lambda x: sys.stdout.write(x) + _stderr = lambda x: sys.stderr.write(x) + +# Lots of stdlib and builtin differences. +if py3k: + import http.client as httplib + import _thread as thread + from urllib.parse import urljoin, SplitResult as UrlSplitResult + from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote + urlunquote = functools.partial(urlunquote, encoding='latin1') + from http.cookies import SimpleCookie + from collections import MutableMapping as DictMixin + import pickle + from io import BytesIO + from configparser import ConfigParser, Error as ConfigParserError + basestring = str + unicode = str + json_loads = lambda s: json_lds(touni(s)) + callable = lambda x: hasattr(x, '__call__') + imap = map + + def _raise(*a): + raise a[0](a[1]).with_traceback(a[2]) +else: # 2.x + import httplib + import thread + from urlparse import urljoin, SplitResult as UrlSplitResult + from urllib import urlencode, quote as urlquote, unquote as urlunquote + from Cookie import SimpleCookie + from itertools import imap + import cPickle as pickle + from StringIO import StringIO as BytesIO + from ConfigParser import SafeConfigParser as ConfigParser, \ + Error as ConfigParserError + if py25: + msg = "Python 2.5 support may be dropped in future versions of Bottle." + warnings.warn(msg, DeprecationWarning) + from UserDict import DictMixin + + def next(it): + return it.next() + + bytes = str + else: # 2.6, 2.7 + from collections import MutableMapping as DictMixin + unicode = unicode + json_loads = json_lds + eval(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec')) + + +# Some helpers for string/byte handling +def tob(s, enc='utf8'): + return s.encode(enc) if isinstance(s, unicode) else bytes(s) + + +def touni(s, enc='utf8', err='strict'): + if isinstance(s, bytes): + return s.decode(enc, err) + else: + return unicode(s or ("" if s is None else s)) + + +tonat = touni if py3k else tob + +# 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense). +# 3.1 needs a workaround. +if py31: + from io import TextIOWrapper + + class NCTextIOWrapper(TextIOWrapper): + def close(self): + pass # Keep wrapped buffer open. + + +# A bug in functools causes it to break if the wrapper is an instance method +def update_wrapper(wrapper, wrapped, *a, **ka): + try: + functools.update_wrapper(wrapper, wrapped, *a, **ka) + except AttributeError: + pass + +# These helpers are used at module level and need to be defined first. +# And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. + + +def depr(message, strict=False): + warnings.warn(message, DeprecationWarning, stacklevel=3) + + +def makelist(data): # This is just too handy + if isinstance(data, (tuple, list, set, dict)): + return list(data) + elif data: + return [data] + else: + return [] + + +class DictProperty(object): + """ Property that maps to a key in a local dict-like attribute. """ + + def __init__(self, attr, key=None, read_only=False): + self.attr, self.key, self.read_only = attr, key, read_only + + def __call__(self, func): + functools.update_wrapper(self, func, updated=[]) + self.getter, self.key = func, self.key or func.__name__ + return self + + def __get__(self, obj, cls): + if obj is None: return self + key, storage = self.key, getattr(obj, self.attr) + if key not in storage: storage[key] = self.getter(obj) + return storage[key] + + def __set__(self, obj, value): + if self.read_only: raise AttributeError("Read-Only property.") + getattr(obj, self.attr)[self.key] = value + + def __delete__(self, obj): + if self.read_only: raise AttributeError("Read-Only property.") + del getattr(obj, self.attr)[self.key] + + +class cached_property(object): + """ A property that is only computed once per instance and then replaces + itself with an ordinary attribute. Deleting the attribute resets the + property. """ + + def __init__(self, func): + self.__doc__ = getattr(func, '__doc__') + self.func = func + + def __get__(self, obj, cls): + if obj is None: return self + value = obj.__dict__[self.func.__name__] = self.func(obj) + return value + + +class lazy_attribute(object): + """ A property that caches itself to the class object. """ + + def __init__(self, func): + functools.update_wrapper(self, func, updated=[]) + self.getter = func + + def __get__(self, obj, cls): + value = self.getter(cls) + setattr(cls, self.__name__, value) + return value + +############################################################################### +# Exceptions and Events ######################################################## +############################################################################### + + +class BottleException(Exception): + """ A base class for exceptions used by bottle. """ + pass + +############################################################################### +# Routing ###################################################################### +############################################################################### + + +class RouteError(BottleException): + """ This is a base class for all routing related exceptions """ + + +class RouteReset(BottleException): + """ If raised by a plugin or request handler, the route is reset and all + plugins are re-applied. """ + + +class RouterUnknownModeError(RouteError): + + pass + + +class RouteSyntaxError(RouteError): + """ The route parser found something not supported by this router. """ + + +class RouteBuildError(RouteError): + """ The route could not be built. """ + + +def _re_flatten(p): + """ Turn all capturing groups in a regular expression pattern into + non-capturing groups. """ + if '(' not in p: + return p + return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', lambda m: m.group(0) if + len(m.group(1)) % 2 else m.group(1) + '(?:', p) + + +class Router(object): + """ A Router is an ordered collection of route->target pairs. It is used to + efficiently match WSGI requests against a number of routes and return + the first target that satisfies the request. The target may be anything, + usually a string, ID or callable object. A route consists of a path-rule + and a HTTP method. + + The path-rule is either a static path (e.g. `/contact`) or a dynamic + path that contains wildcards (e.g. `/wiki/`). The wildcard syntax + and details on the matching order are described in docs:`routing`. + """ + + default_pattern = '[^/]+' + default_filter = 're' + + #: The current CPython regexp implementation does not allow more + #: than 99 matching groups per regular expression. + _MAX_GROUPS_PER_PATTERN = 99 + + def __init__(self, strict=False): + self.rules = [] # All rules in order + self._groups = {} # index of regexes to find them in dyna_routes + self.builder = {} # Data structure for the url builder + self.static = {} # Search structure for static routes + self.dyna_routes = {} + self.dyna_regexes = {} # Search structure for dynamic routes + #: If true, static routes are no longer checked first. + self.strict_order = strict + self.filters = { + 're': lambda conf: (_re_flatten(conf or self.default_pattern), + None, None), + 'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))), + 'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))), + 'path': lambda conf: (r'.+?', None, None) + } + + def add_filter(self, name, func): + """ Add a filter. The provided function is called with the configuration + string as parameter and must return a (regexp, to_python, to_url) tuple. + The first element is a string, the last two are callables or None. """ + self.filters[name] = func + + rule_syntax = re.compile('(\\\\*)' + '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)' + '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)' + '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') + + def _itertokens(self, rule): + offset, prefix = 0, '' + for match in self.rule_syntax.finditer(rule): + prefix += rule[offset:match.start()] + g = match.groups() + if len(g[0]) % 2: # Escaped wildcard + prefix += match.group(0)[len(g[0]):] + offset = match.end() + continue + if prefix: + yield prefix, None, None + name, filtr, conf = g[4:7] if g[2] is None else g[1:4] + yield name, filtr or 'default', conf or None + offset, prefix = match.end(), '' + if offset <= len(rule) or prefix: + yield prefix + rule[offset:], None, None + + def add(self, rule, method, target, name=None): + """ Add a new rule or replace the target for an existing rule. """ + anons = 0 # Number of anonymous wildcards found + keys = [] # Names of keys + pattern = '' # Regular expression pattern with named groups + filters = [] # Lists of wildcard input filters + builder = [] # Data structure for the URL builder + is_static = True + + for key, mode, conf in self._itertokens(rule): + if mode: + is_static = False + if mode == 'default': mode = self.default_filter + mask, in_filter, out_filter = self.filters[mode](conf) + if not key: + pattern += '(?:%s)' % mask + key = 'anon%d' % anons + anons += 1 + else: + pattern += '(?P<%s>%s)' % (key, mask) + keys.append(key) + if in_filter: filters.append((key, in_filter)) + builder.append((key, out_filter or str)) + elif key: + pattern += re.escape(key) + builder.append((None, key)) + + self.builder[rule] = builder + if name: self.builder[name] = builder + + if is_static and not self.strict_order: + self.static.setdefault(method, {}) + self.static[method][self.build(rule)] = (target, None) + return + + try: + re_pattern = re.compile('^(%s)$' % pattern) + re_match = re_pattern.match + except re.error: + raise RouteSyntaxError("Could not add Route: %s (%s)" % + (rule, _e())) + + if filters: + + def getargs(path): + url_args = re_match(path).groupdict() + for name, wildcard_filter in filters: + try: + url_args[name] = wildcard_filter(url_args[name]) + except ValueError: + raise HTTPError(400, 'Path has wrong format.') + return url_args + elif re_pattern.groupindex: + + def getargs(path): + return re_match(path).groupdict() + else: + getargs = None + + flatpat = _re_flatten(pattern) + whole_rule = (rule, flatpat, target, getargs) + + if (flatpat, method) in self._groups: + if DEBUG: + msg = 'Route <%s %s> overwrites a previously defined route' + warnings.warn(msg % (method, rule), RuntimeWarning) + self.dyna_routes[method][ + self._groups[flatpat, method]] = whole_rule + else: + self.dyna_routes.setdefault(method, []).append(whole_rule) + self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1 + + self._compile(method) + + def _compile(self, method): + all_rules = self.dyna_routes[method] + comborules = self.dyna_regexes[method] = [] + maxgroups = self._MAX_GROUPS_PER_PATTERN + for x in range(0, len(all_rules), maxgroups): + some = all_rules[x:x + maxgroups] + combined = (flatpat for (_, flatpat, _, _) in some) + combined = '|'.join('(^%s$)' % flatpat for flatpat in combined) + combined = re.compile(combined).match + rules = [(target, getargs) for (_, _, target, getargs) in some] + comborules.append((combined, rules)) + + def build(self, _name, *anons, **query): + """ Build an URL by filling the wildcards in a rule. """ + builder = self.builder.get(_name) + if not builder: + raise RouteBuildError("No route with that name.", _name) + try: + for i, value in enumerate(anons): + query['anon%d' % i] = value + url = ''.join([f(query.pop(n)) if n else f for (n, f) in builder]) + return url if not query else url + '?' + urlencode(query) + except KeyError: + raise RouteBuildError('Missing URL argument: %r' % _e().args[0]) + + def match(self, environ): + """ Return a (target, url_args) tuple or raise HTTPError(400/404/405). """ + verb = environ['REQUEST_METHOD'].upper() + path = environ['PATH_INFO'] or '/' + + if verb == 'HEAD': + methods = ['PROXY', verb, 'GET', 'ANY'] + else: + methods = ['PROXY', verb, 'ANY'] + + for method in methods: + if method in self.static and path in self.static[method]: + target, getargs = self.static[method][path] + return target, getargs(path) if getargs else {} + elif method in self.dyna_regexes: + for combined, rules in self.dyna_regexes[method]: + match = combined(path) + if match: + target, getargs = rules[match.lastindex - 1] + return target, getargs(path) if getargs else {} + + # No matching route found. Collect alternative methods for 405 response + allowed = set([]) + nocheck = set(methods) + for method in set(self.static) - nocheck: + if path in self.static[method]: + allowed.add(verb) + for method in set(self.dyna_regexes) - allowed - nocheck: + for combined, rules in self.dyna_regexes[method]: + match = combined(path) + if match: + allowed.add(method) + if allowed: + allow_header = ",".join(sorted(allowed)) + raise HTTPError(405, "Method not allowed.", Allow=allow_header) + + # No matching route and no alternative method found. We give up + raise HTTPError(404, "Not found: " + repr(path)) + + +class Route(object): + """ This class wraps a route callback along with route specific metadata and + configuration and applies Plugins on demand. It is also responsible for + turing an URL path rule into a regular expression usable by the Router. + """ + + def __init__(self, app, rule, method, callback, + name=None, + plugins=None, + skiplist=None, **config): + #: The application this route is installed to. + self.app = app + #: The path-rule string (e.g. ``/wiki/``). + self.rule = rule + #: The HTTP method as a string (e.g. ``GET``). + self.method = method + #: The original callback with no plugins applied. Useful for introspection. + self.callback = callback + #: The name of the route (if specified) or ``None``. + self.name = name or None + #: A list of route-specific plugins (see :meth:`Bottle.route`). + self.plugins = plugins or [] + #: A list of plugins to not apply to this route (see :meth:`Bottle.route`). + self.skiplist = skiplist or [] + #: Additional keyword arguments passed to the :meth:`Bottle.route` + #: decorator are stored in this dictionary. Used for route-specific + #: plugin configuration and meta-data. + self.config = ConfigDict().load_dict(config) + + @cached_property + def call(self): + """ The route callback with all plugins applied. This property is + created on demand and then cached to speed up subsequent requests.""" + return self._make_callback() + + def reset(self): + """ Forget any cached values. The next time :attr:`call` is accessed, + all plugins are re-applied. """ + self.__dict__.pop('call', None) + + def prepare(self): + """ Do all on-demand work immediately (useful for debugging).""" + self.call + + def all_plugins(self): + """ Yield all Plugins affecting this route. """ + unique = set() + for p in reversed(self.app.plugins + self.plugins): + if True in self.skiplist: break + name = getattr(p, 'name', False) + if name and (name in self.skiplist or name in unique): continue + if p in self.skiplist or type(p) in self.skiplist: continue + if name: unique.add(name) + yield p + + def _make_callback(self): + callback = self.callback + for plugin in self.all_plugins(): + try: + if hasattr(plugin, 'apply'): + callback = plugin.apply(callback, self) + else: + callback = plugin(callback) + except RouteReset: # Try again with changed configuration. + return self._make_callback() + if not callback is self.callback: + update_wrapper(callback, self.callback) + return callback + + def get_undecorated_callback(self): + """ Return the callback. If the callback is a decorated function, try to + recover the original function. """ + func = self.callback + func = getattr(func, '__func__' if py3k else 'im_func', func) + closure_attr = '__closure__' if py3k else 'func_closure' + while hasattr(func, closure_attr) and getattr(func, closure_attr): + attributes = getattr(func, closure_attr) + func = attributes[0].cell_contents + + # in case of decorators with multiple arguments + if not isinstance(func, FunctionType): + # pick first FunctionType instance from multiple arguments + func = filter(lambda x: isinstance(x, FunctionType), + map(lambda x: x.cell_contents, attributes)) + func = list(func)[0] # py3 support + return func + + def get_callback_args(self): + """ Return a list of argument names the callback (most likely) accepts + as keyword arguments. If the callback is a decorated function, try + to recover the original function before inspection. """ + return getargspec(self.get_undecorated_callback())[0] + + def get_config(self, key, default=None): + """ Lookup a config field and return its value, first checking the + route.config, then route.app.config.""" + for conf in (self.config, self.app.config): + if key in conf: return conf[key] + return default + + def __repr__(self): + cb = self.get_undecorated_callback() + return '<%s %r %r>' % (self.method, self.rule, cb) + +############################################################################### +# Application Object ########################################################### +############################################################################### + + +class Bottle(object): + """ Each Bottle object represents a single, distinct web application and + consists of routes, callbacks, plugins, resources and configuration. + Instances are callable WSGI applications. + + :param catchall: If true (default), handle all exceptions. Turn off to + let debugging middleware handle exceptions. + """ + + def __init__(self, catchall=True, autojson=True): + #: A :class:`ConfigDict` for app specific configuration. + self.config = ConfigDict() + self.config._on_change = functools.partial(self.trigger_hook, 'config') + self.config.meta_set('autojson', 'validate', bool) + self.config.meta_set('catchall', 'validate', bool) + self.config['catchall'] = catchall + self.config['autojson'] = autojson + + #: A :class:`ResourceManager` for application files + self.resources = ResourceManager() + + self.routes = [] # List of installed :class:`Route` instances. + self.router = Router() # Maps requests to :class:`Route` instances. + self.error_handler = {} + + # Core plugins + self.plugins = [] # List of installed plugins. + if self.config['autojson']: + self.install(JSONPlugin()) + self.install(TemplatePlugin()) + + #: If true, most exceptions are caught and returned as :exc:`HTTPError` + catchall = DictProperty('config', 'catchall') + + __hook_names = 'before_request', 'after_request', 'app_reset', 'config' + __hook_reversed = 'after_request' + + @cached_property + def _hooks(self): + return dict((name, []) for name in self.__hook_names) + + def add_hook(self, name, func): + """ Attach a callback to a hook. Three hooks are currently implemented: + + before_request + Executed once before each request. The request context is + available, but no routing has happened yet. + after_request + Executed once after each request regardless of its outcome. + app_reset + Called whenever :meth:`Bottle.reset` is called. + """ + if name in self.__hook_reversed: + self._hooks[name].insert(0, func) + else: + self._hooks[name].append(func) + + def remove_hook(self, name, func): + """ Remove a callback from a hook. """ + if name in self._hooks and func in self._hooks[name]: + self._hooks[name].remove(func) + return True + + def trigger_hook(self, __name, *args, **kwargs): + """ Trigger a hook and return a list of results. """ + return [hook(*args, **kwargs) for hook in self._hooks[__name][:]] + + def hook(self, name): + """ Return a decorator that attaches a callback to a hook. See + :meth:`add_hook` for details.""" + + def decorator(func): + self.add_hook(name, func) + return func + + return decorator + + def mount(self, prefix, app, **options): + """ Mount an application (:class:`Bottle` or plain WSGI) to a specific + URL prefix. Example:: + + root_app.mount('/admin/', admin_app) + + :param prefix: path prefix or `mount-point`. If it ends in a slash, + that slash is mandatory. + :param app: an instance of :class:`Bottle` or a WSGI application. + + All other parameters are passed to the underlying :meth:`route` call. + """ + + segments = [p for p in prefix.split('/') if p] + if not segments: raise ValueError('Empty path prefix.') + path_depth = len(segments) + + def mountpoint_wrapper(): + try: + request.path_shift(path_depth) + rs = HTTPResponse([]) + + def start_response(status, headerlist, exc_info=None): + if exc_info: + _raise(*exc_info) + rs.status = status + for name, value in headerlist: + rs.add_header(name, value) + return rs.body.append + + body = app(request.environ, start_response) + rs.body = itertools.chain(rs.body, body) if rs.body else body + return rs + finally: + request.path_shift(-path_depth) + + options.setdefault('skip', True) + options.setdefault('method', 'PROXY') + options.setdefault('mountpoint', {'prefix': prefix, 'target': app}) + options['callback'] = mountpoint_wrapper + + self.route('/%s/<:re:.*>' % '/'.join(segments), **options) + if not prefix.endswith('/'): + self.route('/' + '/'.join(segments), **options) + + def merge(self, routes): + """ Merge the routes of another :class:`Bottle` application or a list of + :class:`Route` objects into this application. The routes keep their + 'owner', meaning that the :data:`Route.app` attribute is not + changed. """ + if isinstance(routes, Bottle): + routes = routes.routes + for route in routes: + self.add_route(route) + + def install(self, plugin): + """ Add a plugin to the list of plugins and prepare it for being + applied to all routes of this application. A plugin may be a simple + decorator or an object that implements the :class:`Plugin` API. + """ + if hasattr(plugin, 'setup'): plugin.setup(self) + if not callable(plugin) and not hasattr(plugin, 'apply'): + raise TypeError("Plugins must be callable or implement .apply()") + self.plugins.append(plugin) + self.reset() + return plugin + + def uninstall(self, plugin): + """ Uninstall plugins. Pass an instance to remove a specific plugin, a type + object to remove all plugins that match that type, a string to remove + all plugins with a matching ``name`` attribute or ``True`` to remove all + plugins. Return the list of removed plugins. """ + removed, remove = [], plugin + for i, plugin in list(enumerate(self.plugins))[::-1]: + if remove is True or remove is plugin or remove is type(plugin) \ + or getattr(plugin, 'name', True) == remove: + removed.append(plugin) + del self.plugins[i] + if hasattr(plugin, 'close'): plugin.close() + if removed: self.reset() + return removed + + def reset(self, route=None): + """ Reset all routes (force plugins to be re-applied) and clear all + caches. If an ID or route object is given, only that specific route + is affected. """ + if route is None: routes = self.routes + elif isinstance(route, Route): routes = [route] + else: routes = [self.routes[route]] + for route in routes: + route.reset() + if DEBUG: + for route in routes: + route.prepare() + self.trigger_hook('app_reset') + + def close(self): + """ Close the application and all installed plugins. """ + for plugin in self.plugins: + if hasattr(plugin, 'close'): plugin.close() + + def run(self, **kwargs): + """ Calls :func:`run` with the same parameters. """ + run(self, **kwargs) + + def match(self, environ): + """ Search for a matching route and return a (:class:`Route` , urlargs) + tuple. The second value is a dictionary with parameters extracted + from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" + return self.router.match(environ) + + def get_url(self, routename, **kargs): + """ Return a string that matches a named route """ + scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/' + location = self.router.build(routename, **kargs).lstrip('/') + return urljoin(urljoin('/', scriptname), location) + + def add_route(self, route): + """ Add a route object, but do not change the :data:`Route.app` + attribute.""" + self.routes.append(route) + self.router.add(route.rule, route.method, route, name=route.name) + if DEBUG: route.prepare() + + def route(self, + path=None, + method='GET', + callback=None, + name=None, + apply=None, + skip=None, **config): + """ A decorator to bind a function to a request URL. Example:: + + @app.route('/hello/') + def hello(name): + return 'Hello %s' % name + + The ```` part is a wildcard. See :class:`Router` for syntax + details. + + :param path: Request path or a list of paths to listen to. If no + path is specified, it is automatically generated from the + signature of the function. + :param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of + methods to listen to. (default: `GET`) + :param callback: An optional shortcut to avoid the decorator + syntax. ``route(..., callback=func)`` equals ``route(...)(func)`` + :param name: The name for this route. (default: None) + :param apply: A decorator or plugin or a list of plugins. These are + applied to the route callback in addition to installed plugins. + :param skip: A list of plugins, plugin classes or names. Matching + plugins are not installed to this route. ``True`` skips all. + + Any additional keyword arguments are stored as route-specific + configuration and passed to plugins (see :meth:`Plugin.apply`). + """ + if callable(path): path, callback = None, path + plugins = makelist(apply) + skiplist = makelist(skip) + + def decorator(callback): + if isinstance(callback, basestring): callback = load(callback) + for rule in makelist(path) or yieldroutes(callback): + for verb in makelist(method): + verb = verb.upper() + route = Route(self, rule, verb, callback, + name=name, + plugins=plugins, + skiplist=skiplist, **config) + self.add_route(route) + return callback + + return decorator(callback) if callback else decorator + + def get(self, path=None, method='GET', **options): + """ Equals :meth:`route`. """ + return self.route(path, method, **options) + + def post(self, path=None, method='POST', **options): + """ Equals :meth:`route` with a ``POST`` method parameter. """ + return self.route(path, method, **options) + + def put(self, path=None, method='PUT', **options): + """ Equals :meth:`route` with a ``PUT`` method parameter. """ + return self.route(path, method, **options) + + def delete(self, path=None, method='DELETE', **options): + """ Equals :meth:`route` with a ``DELETE`` method parameter. """ + return self.route(path, method, **options) + + def patch(self, path=None, method='PATCH', **options): + """ Equals :meth:`route` with a ``PATCH`` method parameter. """ + return self.route(path, method, **options) + + def error(self, code=500): + """ Decorator: Register an output handler for a HTTP error code""" + + def wrapper(handler): + self.error_handler[int(code)] = handler + return handler + + return wrapper + + def default_error_handler(self, res): + return tob(template(ERROR_PAGE_TEMPLATE, e=res)) + + def _handle(self, environ): + path = environ['bottle.raw_path'] = environ['PATH_INFO'] + if py3k: + environ['PATH_INFO'] = path.encode('latin1').decode('utf8', 'ignore') + + def _inner_handle(): + # Maybe pass variables as locals for better performance? + try: + route, args = self.router.match(environ) + environ['route.handle'] = route + environ['bottle.route'] = route + environ['route.url_args'] = args + return route.call(**args) + except HTTPResponse: + return _e() + except RouteReset: + route.reset() + return _inner_handle() + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception: + if not self.catchall: raise + stacktrace = format_exc() + environ['wsgi.errors'].write(stacktrace) + return HTTPError(500, "Internal Server Error", _e(), stacktrace) + + try: + out = None + environ['bottle.app'] = self + request.bind(environ) + response.bind() + self.trigger_hook('before_request') + out = _inner_handle() + return out; + finally: + if isinstance(out, HTTPResponse): + out.apply(response) + self.trigger_hook('after_request') + + def _cast(self, out, peek=None): + """ Try to convert the parameter into something WSGI compatible and set + correct HTTP headers when possible. + Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like, + iterable of strings and iterable of unicodes + """ + + # Empty output is done here + if not out: + if 'Content-Length' not in response: + response['Content-Length'] = 0 + return [] + # Join lists of byte or unicode strings. Mixed lists are NOT supported + if isinstance(out, (tuple, list))\ + and isinstance(out[0], (bytes, unicode)): + out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' + # Encode unicode strings + if isinstance(out, unicode): + out = out.encode(response.charset) + # Byte Strings are just returned + if isinstance(out, bytes): + if 'Content-Length' not in response: + response['Content-Length'] = len(out) + return [out] + # HTTPError or HTTPException (recursive, because they may wrap anything) + # TODO: Handle these explicitly in handle() or make them iterable. + if isinstance(out, HTTPError): + out.apply(response) + out = self.error_handler.get(out.status_code, + self.default_error_handler)(out) + return self._cast(out) + if isinstance(out, HTTPResponse): + out.apply(response) + return self._cast(out.body) + + # File-like objects. + if hasattr(out, 'read'): + if 'wsgi.file_wrapper' in request.environ: + return request.environ['wsgi.file_wrapper'](out) + elif hasattr(out, 'close') or not hasattr(out, '__iter__'): + return WSGIFileWrapper(out) + + # Handle Iterables. We peek into them to detect their inner type. + try: + iout = iter(out) + first = next(iout) + while not first: + first = next(iout) + except StopIteration: + return self._cast('') + except HTTPResponse: + first = _e() + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except: + if not self.catchall: raise + first = HTTPError(500, 'Unhandled exception', _e(), format_exc()) + + # These are the inner types allowed in iterator or generator objects. + if isinstance(first, HTTPResponse): + return self._cast(first) + elif isinstance(first, bytes): + new_iter = itertools.chain([first], iout) + elif isinstance(first, unicode): + encoder = lambda x: x.encode(response.charset) + new_iter = imap(encoder, itertools.chain([first], iout)) + else: + msg = 'Unsupported response type: %s' % type(first) + return self._cast(HTTPError(500, msg)) + if hasattr(out, 'close'): + new_iter = _closeiter(new_iter, out.close) + return new_iter + + def wsgi(self, environ, start_response): + """ The bottle WSGI-interface. """ + try: + out = self._cast(self._handle(environ)) + # rfc2616 section 4.3 + if response._status_code in (100, 101, 204, 304)\ + or environ['REQUEST_METHOD'] == 'HEAD': + if hasattr(out, 'close'): out.close() + out = [] + start_response(response._status_line, response.headerlist) + return out + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except: + if not self.catchall: raise + err = '

    Critical error while processing request: %s

    ' \ + % html_escape(environ.get('PATH_INFO', '/')) + if DEBUG: + err += '

    Error:

    \n
    \n%s\n
    \n' \ + '

    Traceback:

    \n
    \n%s\n
    \n' \ + % (html_escape(repr(_e())), html_escape(format_exc())) + environ['wsgi.errors'].write(err) + headers = [('Content-Type', 'text/html; charset=UTF-8')] + start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info()) + return [tob(err)] + + def __call__(self, environ, start_response): + """ Each instance of :class:'Bottle' is a WSGI application. """ + return self.wsgi(environ, start_response) + + def __enter__(self): + """ Use this application as default for all module-level shortcuts. """ + default_app.push(self) + return self + + def __exit__(self, exc_type, exc_value, traceback): + default_app.pop() + + def __setattr__(self, name, value): + if name in self.__dict__: + raise AttributeError("Attribute %s already defined. Plugin conflict?" % name) + self.__dict__[name] = value + + +############################################################################### +# HTTP and WSGI Tools ########################################################## +############################################################################### + + +class BaseRequest(object): + """ A wrapper for WSGI environment dictionaries that adds a lot of + convenient access methods and properties. Most of them are read-only. + + Adding new attributes to a request actually adds them to the environ + dictionary (as 'bottle.request.ext.'). This is the recommended + way to store and access request-specific data. + """ + + __slots__ = ('environ', ) + + #: Maximum size of memory buffer for :attr:`body` in bytes. + MEMFILE_MAX = 102400 + + def __init__(self, environ=None): + """ Wrap a WSGI environ dictionary. """ + #: The wrapped WSGI environ dictionary. This is the only real attribute. + #: All other attributes actually are read-only properties. + self.environ = {} if environ is None else environ + self.environ['bottle.request'] = self + + @DictProperty('environ', 'bottle.app', read_only=True) + def app(self): + """ Bottle application handling this request. """ + raise RuntimeError('This request is not connected to an application.') + + @DictProperty('environ', 'bottle.route', read_only=True) + def route(self): + """ The bottle :class:`Route` object that matches this request. """ + raise RuntimeError('This request is not connected to a route.') + + @DictProperty('environ', 'route.url_args', read_only=True) + def url_args(self): + """ The arguments extracted from the URL. """ + raise RuntimeError('This request is not connected to a route.') + + @property + def path(self): + """ The value of ``PATH_INFO`` with exactly one prefixed slash (to fix + broken clients and avoid the "empty path" edge case). """ + return '/' + self.environ.get('PATH_INFO', '').lstrip('/') + + @property + def method(self): + """ The ``REQUEST_METHOD`` value as an uppercase string. """ + return self.environ.get('REQUEST_METHOD', 'GET').upper() + + @DictProperty('environ', 'bottle.request.headers', read_only=True) + def headers(self): + """ A :class:`WSGIHeaderDict` that provides case-insensitive access to + HTTP request headers. """ + return WSGIHeaderDict(self.environ) + + def get_header(self, name, default=None): + """ Return the value of a request header, or a given default value. """ + return self.headers.get(name, default) + + @DictProperty('environ', 'bottle.request.cookies', read_only=True) + def cookies(self): + """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT + decoded. Use :meth:`get_cookie` if you expect signed cookies. """ + cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values() + return FormsDict((c.key, c.value) for c in cookies) + + def get_cookie(self, key, default=None, secret=None): + """ Return the content of a cookie. To read a `Signed Cookie`, the + `secret` must match the one used to create the cookie (see + :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing + cookie or wrong signature), return a default value. """ + value = self.cookies.get(key) + if secret and value: + dec = cookie_decode(value, secret) # (key, value) tuple or None + return dec[1] if dec and dec[0] == key else default + return value or default + + @DictProperty('environ', 'bottle.request.query', read_only=True) + def query(self): + """ The :attr:`query_string` parsed into a :class:`FormsDict`. These + values are sometimes called "URL arguments" or "GET parameters", but + not to be confused with "URL wildcards" as they are provided by the + :class:`Router`. """ + get = self.environ['bottle.get'] = FormsDict() + pairs = _parse_qsl(self.environ.get('QUERY_STRING', '')) + for key, value in pairs: + get[key] = value + return get + + @DictProperty('environ', 'bottle.request.forms', read_only=True) + def forms(self): + """ Form values parsed from an `url-encoded` or `multipart/form-data` + encoded POST or PUT request body. The result is returned as a + :class:`FormsDict`. All keys and values are strings. File uploads + are stored separately in :attr:`files`. """ + forms = FormsDict() + for name, item in self.POST.allitems(): + if not isinstance(item, FileUpload): + forms[name] = item + return forms + + @DictProperty('environ', 'bottle.request.params', read_only=True) + def params(self): + """ A :class:`FormsDict` with the combined values of :attr:`query` and + :attr:`forms`. File uploads are stored in :attr:`files`. """ + params = FormsDict() + for key, value in self.query.allitems(): + params[key] = value + for key, value in self.forms.allitems(): + params[key] = value + return params + + @DictProperty('environ', 'bottle.request.files', read_only=True) + def files(self): + """ File uploads parsed from `multipart/form-data` encoded POST or PUT + request body. The values are instances of :class:`FileUpload`. + + """ + files = FormsDict() + for name, item in self.POST.allitems(): + if isinstance(item, FileUpload): + files[name] = item + return files + + @DictProperty('environ', 'bottle.request.json', read_only=True) + def json(self): + """ If the ``Content-Type`` header is ``application/json``, this + property holds the parsed content of the request body. Only requests + smaller than :attr:`MEMFILE_MAX` are processed to avoid memory + exhaustion. Invalid JSON raises a 400 error response. """ + ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0] + if ctype == 'application/json': + b = self._get_body_string() + if not b: + return None + try: + return json_loads(b) + except (ValueError, TypeError): + raise HTTPError(400, 'Invalid JSON') + return None + + def _iter_body(self, read, bufsize): + maxread = max(0, self.content_length) + while maxread: + part = read(min(maxread, bufsize)) + if not part: break + yield part + maxread -= len(part) + + @staticmethod + def _iter_chunked(read, bufsize): + err = HTTPError(400, 'Error while parsing chunked transfer body.') + rn, sem, bs = tob('\r\n'), tob(';'), tob('') + while True: + header = read(1) + while header[-2:] != rn: + c = read(1) + header += c + if not c: raise err + if len(header) > bufsize: raise err + size, _, _ = header.partition(sem) + try: + maxread = int(tonat(size.strip()), 16) + except ValueError: + raise err + if maxread == 0: break + buff = bs + while maxread > 0: + if not buff: + buff = read(min(maxread, bufsize)) + part, buff = buff[:maxread], buff[maxread:] + if not part: raise err + yield part + maxread -= len(part) + if read(2) != rn: + raise err + + @DictProperty('environ', 'bottle.request.body', read_only=True) + def _body(self): + try: + read_func = self.environ['wsgi.input'].read + except KeyError: + self.environ['wsgi.input'] = BytesIO() + return self.environ['wsgi.input'] + body_iter = self._iter_chunked if self.chunked else self._iter_body + body, body_size, is_temp_file = BytesIO(), 0, False + for part in body_iter(read_func, self.MEMFILE_MAX): + body.write(part) + body_size += len(part) + if not is_temp_file and body_size > self.MEMFILE_MAX: + body, tmp = TemporaryFile(mode='w+b'), body + body.write(tmp.getvalue()) + del tmp + is_temp_file = True + self.environ['wsgi.input'] = body + body.seek(0) + return body + + def _get_body_string(self): + """ read body until content-length or MEMFILE_MAX into a string. Raise + HTTPError(413) on requests that are to large. """ + clen = self.content_length + if clen > self.MEMFILE_MAX: + raise HTTPError(413, 'Request entity too large') + if clen < 0: clen = self.MEMFILE_MAX + 1 + data = self.body.read(clen) + if len(data) > self.MEMFILE_MAX: # Fail fast + raise HTTPError(413, 'Request entity too large') + return data + + @property + def body(self): + """ The HTTP request body as a seek-able file-like object. Depending on + :attr:`MEMFILE_MAX`, this is either a temporary file or a + :class:`io.BytesIO` instance. Accessing this property for the first + time reads and replaces the ``wsgi.input`` environ variable. + Subsequent accesses just do a `seek(0)` on the file object. """ + self._body.seek(0) + return self._body + + @property + def chunked(self): + """ True if Chunked transfer encoding was. """ + return 'chunked' in self.environ.get( + 'HTTP_TRANSFER_ENCODING', '').lower() + + #: An alias for :attr:`query`. + GET = query + + @DictProperty('environ', 'bottle.request.post', read_only=True) + def POST(self): + """ The values of :attr:`forms` and :attr:`files` combined into a single + :class:`FormsDict`. Values are either strings (form values) or + instances of :class:`cgi.FieldStorage` (file uploads). + """ + post = FormsDict() + # We default to application/x-www-form-urlencoded for everything that + # is not multipart and take the fast path (also: 3.1 workaround) + if not self.content_type.startswith('multipart/'): + pairs = _parse_qsl(tonat(self._get_body_string(), 'latin1')) + for key, value in pairs: + post[key] = value + return post + + safe_env = {'QUERY_STRING': ''} # Build a safe environment for cgi + for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): + if key in self.environ: safe_env[key] = self.environ[key] + args = dict(fp=self.body, environ=safe_env, keep_blank_values=True) + if py31: + args['fp'] = NCTextIOWrapper(args['fp'], + encoding='utf8', + newline='\n') + elif py3k: + args['encoding'] = 'utf8' + data = cgi.FieldStorage(**args) + self['_cgi.FieldStorage'] = data #http://bugs.python.org/issue18394 + data = data.list or [] + for item in data: + if item.filename: + post[item.name] = FileUpload(item.file, item.name, + item.filename, item.headers) + else: + post[item.name] = item.value + return post + + @property + def url(self): + """ The full request URI including hostname and scheme. If your app + lives behind a reverse proxy or load balancer and you get confusing + results, make sure that the ``X-Forwarded-Host`` header is set + correctly. """ + return self.urlparts.geturl() + + @DictProperty('environ', 'bottle.request.urlparts', read_only=True) + def urlparts(self): + """ The :attr:`url` string as an :class:`urlparse.SplitResult` tuple. + The tuple contains (scheme, host, path, query_string and fragment), + but the fragment is always empty because it is not visible to the + server. """ + env = self.environ + http = env.get('HTTP_X_FORWARDED_PROTO') \ + or env.get('wsgi.url_scheme', 'http') + host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') + if not host: + # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. + host = env.get('SERVER_NAME', '127.0.0.1') + port = env.get('SERVER_PORT') + if port and port != ('80' if http == 'http' else '443'): + host += ':' + port + path = urlquote(self.fullpath) + return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '') + + @property + def fullpath(self): + """ Request path including :attr:`script_name` (if present). """ + return urljoin(self.script_name, self.path.lstrip('/')) + + @property + def query_string(self): + """ The raw :attr:`query` part of the URL (everything in between ``?`` + and ``#``) as a string. """ + return self.environ.get('QUERY_STRING', '') + + @property + def script_name(self): + """ The initial portion of the URL's `path` that was removed by a higher + level (server or routing middleware) before the application was + called. This script path is returned with leading and tailing + slashes. """ + script_name = self.environ.get('SCRIPT_NAME', '').strip('/') + return '/' + script_name + '/' if script_name else '/' + + def path_shift(self, shift=1): + """ Shift path segments from :attr:`path` to :attr:`script_name` and + vice versa. + + :param shift: The number of path segments to shift. May be negative + to change the shift direction. (default: 1) + """ + script, path = path_shift(self.environ.get('SCRIPT_NAME', '/'), self.path, shift) + self['SCRIPT_NAME'], self['PATH_INFO'] = script, path + + @property + def content_length(self): + """ The request body length as an integer. The client is responsible to + set this header. Otherwise, the real length of the body is unknown + and -1 is returned. In this case, :attr:`body` will be empty. """ + return int(self.environ.get('CONTENT_LENGTH') or -1) + + @property + def content_type(self): + """ The Content-Type header as a lowercase-string (default: empty). """ + return self.environ.get('CONTENT_TYPE', '').lower() + + @property + def is_xhr(self): + """ True if the request was triggered by a XMLHttpRequest. This only + works with JavaScript libraries that support the `X-Requested-With` + header (most of the popular libraries do). """ + requested_with = self.environ.get('HTTP_X_REQUESTED_WITH', '') + return requested_with.lower() == 'xmlhttprequest' + + @property + def is_ajax(self): + """ Alias for :attr:`is_xhr`. "Ajax" is not the right term. """ + return self.is_xhr + + @property + def auth(self): + """ HTTP authentication data as a (user, password) tuple. This + implementation currently supports basic (not digest) authentication + only. If the authentication happened at a higher level (e.g. in the + front web-server or a middleware), the password field is None, but + the user field is looked up from the ``REMOTE_USER`` environ + variable. On any errors, None is returned. """ + basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION', '')) + if basic: return basic + ruser = self.environ.get('REMOTE_USER') + if ruser: return (ruser, None) + return None + + @property + def remote_route(self): + """ A list of all IPs that were involved in this request, starting with + the client IP and followed by zero or more proxies. This does only + work if all proxies support the ```X-Forwarded-For`` header. Note + that this information can be forged by malicious clients. """ + proxy = self.environ.get('HTTP_X_FORWARDED_FOR') + if proxy: return [ip.strip() for ip in proxy.split(',')] + remote = self.environ.get('REMOTE_ADDR') + return [remote] if remote else [] + + @property + def remote_addr(self): + """ The client IP as a string. Note that this information can be forged + by malicious clients. """ + route = self.remote_route + return route[0] if route else None + + def copy(self): + """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """ + return Request(self.environ.copy()) + + def get(self, value, default=None): + return self.environ.get(value, default) + + def __getitem__(self, key): + return self.environ[key] + + def __delitem__(self, key): + self[key] = "" + del (self.environ[key]) + + def __iter__(self): + return iter(self.environ) + + def __len__(self): + return len(self.environ) + + def keys(self): + return self.environ.keys() + + def __setitem__(self, key, value): + """ Change an environ value and clear all caches that depend on it. """ + + if self.environ.get('bottle.request.readonly'): + raise KeyError('The environ dictionary is read-only.') + + self.environ[key] = value + todelete = () + + if key == 'wsgi.input': + todelete = ('body', 'forms', 'files', 'params', 'post', 'json') + elif key == 'QUERY_STRING': + todelete = ('query', 'params') + elif key.startswith('HTTP_'): + todelete = ('headers', 'cookies') + + for key in todelete: + self.environ.pop('bottle.request.' + key, None) + + def __repr__(self): + return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url) + + def __getattr__(self, name): + """ Search in self.environ for additional user defined attributes. """ + try: + var = self.environ['bottle.request.ext.%s' % name] + return var.__get__(self) if hasattr(var, '__get__') else var + except KeyError: + raise AttributeError('Attribute %r not defined.' % name) + + def __setattr__(self, name, value): + if name == 'environ': return object.__setattr__(self, name, value) + key = 'bottle.request.ext.%s' % name + if key in self.environ: + raise AttributeError("Attribute already defined: %s" % name) + self.environ[key] = value + + def __delattr__(self, name, value): + try: + del self.environ['bottle.request.ext.%s' % name] + except KeyError: + raise AttributeError("Attribute not defined: %s" % name) + +def _hkey(s): + return s.title().replace('_', '-') + + +class HeaderProperty(object): + def __init__(self, name, reader=None, writer=str, default=''): + self.name, self.default = name, default + self.reader, self.writer = reader, writer + self.__doc__ = 'Current value of the %r header.' % name.title() + + def __get__(self, obj, _): + if obj is None: return self + value = obj.headers.get(self.name, self.default) + return self.reader(value) if self.reader else value + + def __set__(self, obj, value): + obj.headers[self.name] = self.writer(value) + + def __delete__(self, obj): + del obj.headers[self.name] + + +class BaseResponse(object): + """ Storage class for a response body as well as headers and cookies. + + This class does support dict-like case-insensitive item-access to + headers, but is NOT a dict. Most notably, iterating over a response + yields parts of the body and not the headers. + + :param body: The response body as one of the supported types. + :param status: Either an HTTP status code (e.g. 200) or a status line + including the reason phrase (e.g. '200 OK'). + :param headers: A dictionary or a list of name-value pairs. + + Additional keyword arguments are added to the list of headers. + Underscores in the header name are replaced with dashes. + """ + + default_status = 200 + default_content_type = 'text/html; charset=UTF-8' + + # Header blacklist for specific response codes + # (rfc2616 section 10.2.3 and 10.3.5) + bad_headers = { + 204: set(('Content-Type', )), + 304: set(('Allow', 'Content-Encoding', 'Content-Language', + 'Content-Length', 'Content-Range', 'Content-Type', + 'Content-Md5', 'Last-Modified')) + } + + def __init__(self, body='', status=None, headers=None, **more_headers): + self._cookies = None + self._headers = {} + self.body = body + self.status = status or self.default_status + if headers: + if isinstance(headers, dict): + headers = headers.items() + for name, value in headers: + self.add_header(name, value) + if more_headers: + for name, value in more_headers.items(): + self.add_header(name, value) + + def copy(self, cls=None): + """ Returns a copy of self. """ + cls = cls or BaseResponse + assert issubclass(cls, BaseResponse) + copy = cls() + copy.status = self.status + copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) + if self._cookies: + copy._cookies = SimpleCookie() + copy._cookies.load(self._cookies.output(header='')) + return copy + + def __iter__(self): + return iter(self.body) + + def close(self): + if hasattr(self.body, 'close'): + self.body.close() + + @property + def status_line(self): + """ The HTTP status line as a string (e.g. ``404 Not Found``).""" + return self._status_line + + @property + def status_code(self): + """ The HTTP status code as an integer (e.g. 404).""" + return self._status_code + + def _set_status(self, status): + if isinstance(status, int): + code, status = status, _HTTP_STATUS_LINES.get(status) + elif ' ' in status: + status = status.strip() + code = int(status.split()[0]) + else: + raise ValueError('String status line without a reason phrase.') + if not 100 <= code <= 999: + raise ValueError('Status code out of range.') + self._status_code = code + self._status_line = str(status or ('%d Unknown' % code)) + + def _get_status(self): + return self._status_line + + status = property( + _get_status, _set_status, None, + ''' A writeable property to change the HTTP response status. It accepts + either a numeric code (100-999) or a string with a custom reason + phrase (e.g. "404 Brain not found"). Both :data:`status_line` and + :data:`status_code` are updated accordingly. The return value is + always a status string. ''') + del _get_status, _set_status + + @property + def headers(self): + """ An instance of :class:`HeaderDict`, a case-insensitive dict-like + view on the response headers. """ + hdict = HeaderDict() + hdict.dict = self._headers + return hdict + + def __contains__(self, name): + return _hkey(name) in self._headers + + def __delitem__(self, name): + del self._headers[_hkey(name)] + + def __getitem__(self, name): + return self._headers[_hkey(name)][-1] + + def __setitem__(self, name, value): + self._headers[_hkey(name)] = [value if isinstance(value, unicode) else + str(value)] + + def get_header(self, name, default=None): + """ Return the value of a previously defined header. If there is no + header with that name, return a default value. """ + return self._headers.get(_hkey(name), [default])[-1] + + def set_header(self, name, value): + """ Create a new response header, replacing any previously defined + headers with the same name. """ + self._headers[_hkey(name)] = [value if isinstance(value, unicode) + else str(value)] + + def add_header(self, name, value): + """ Add an additional response header, not removing duplicates. """ + self._headers.setdefault(_hkey(name), []).append( + value if isinstance(value, unicode) else str(value)) + + def iter_headers(self): + """ Yield (header, value) tuples, skipping headers that are not + allowed with the current response status code. """ + return self.headerlist + + @property + def headerlist(self): + """ WSGI conform list of (header, value) tuples. """ + out = [] + headers = list(self._headers.items()) + if 'Content-Type' not in self._headers: + headers.append(('Content-Type', [self.default_content_type])) + if self._status_code in self.bad_headers: + bad_headers = self.bad_headers[self._status_code] + headers = [h for h in headers if h[0] not in bad_headers] + out += [(name, val) for (name, vals) in headers for val in vals] + if self._cookies: + for c in self._cookies.values(): + out.append(('Set-Cookie', c.OutputString())) + if py3k: + return [(k, v.encode('utf8').decode('latin1')) for (k, v) in out] + else: + return [(k, v.encode('utf8') if isinstance(v, unicode) else v) + for (k, v) in out] + + content_type = HeaderProperty('Content-Type') + content_length = HeaderProperty('Content-Length', reader=int) + expires = HeaderProperty( + 'Expires', + reader=lambda x: datetime.utcfromtimestamp(parse_date(x)), + writer=lambda x: http_date(x)) + + @property + def charset(self, default='UTF-8'): + """ Return the charset specified in the content-type header (default: utf8). """ + if 'charset=' in self.content_type: + return self.content_type.split('charset=')[-1].split(';')[0].strip() + return default + + def set_cookie(self, name, value, secret=None, **options): + """ Create a new cookie or replace an old one. If the `secret` parameter is + set, create a `Signed Cookie` (described below). + + :param name: the name of the cookie. + :param value: the value of the cookie. + :param secret: a signature key required for signed cookies. + + Additionally, this method accepts all RFC 2109 attributes that are + supported by :class:`cookie.Morsel`, including: + + :param max_age: maximum age in seconds. (default: None) + :param expires: a datetime object or UNIX timestamp. (default: None) + :param domain: the domain that is allowed to read the cookie. + (default: current domain) + :param path: limits the cookie to a given path (default: current path) + :param secure: limit the cookie to HTTPS connections (default: off). + :param httponly: prevents client-side javascript to read this cookie + (default: off, requires Python 2.6 or newer). + + If neither `expires` nor `max_age` is set (default), the cookie will + expire at the end of the browser session (as soon as the browser + window is closed). + + Signed cookies may store any pickle-able object and are + cryptographically signed to prevent manipulation. Keep in mind that + cookies are limited to 4kb in most browsers. + + Warning: Signed cookies are not encrypted (the client can still see + the content) and not copy-protected (the client can restore an old + cookie). The main intention is to make pickling and unpickling + save, not to store secret information at client side. + """ + if not self._cookies: + self._cookies = SimpleCookie() + + if secret: + value = touni(cookie_encode((name, value), secret)) + elif not isinstance(value, basestring): + raise TypeError('Secret key missing for non-string Cookie.') + + # Cookie size plus options must not exceed 4kb. + if len(name) + len(value) > 3800: + raise ValueError('Content does not fit into a cookie.') + + self._cookies[name] = value + + for key, value in options.items(): + if key == 'max_age': + if isinstance(value, timedelta): + value = value.seconds + value.days * 24 * 3600 + if key == 'expires': + if isinstance(value, (datedate, datetime)): + value = value.timetuple() + elif isinstance(value, (int, float)): + value = time.gmtime(value) + value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) + if key in ('secure', 'httponly') and not value: + continue + self._cookies[name][key.replace('_', '-')] = value + + def delete_cookie(self, key, **kwargs): + """ Delete a cookie. Be sure to use the same `domain` and `path` + settings as used to create the cookie. """ + kwargs['max_age'] = -1 + kwargs['expires'] = 0 + self.set_cookie(key, '', **kwargs) + + def __repr__(self): + out = '' + for name, value in self.headerlist: + out += '%s: %s\n' % (name.title(), value.strip()) + return out + + +def _local_property(): + ls = threading.local() + + def fget(_): + try: + return ls.var + except AttributeError: + raise RuntimeError("Request context not initialized.") + + def fset(_, value): + ls.var = value + + def fdel(_): + del ls.var + + return property(fget, fset, fdel, 'Thread-local property') + + +class LocalRequest(BaseRequest): + """ A thread-local subclass of :class:`BaseRequest` with a different + set of attributes for each thread. There is usually only one global + instance of this class (:data:`request`). If accessed during a + request/response cycle, this instance always refers to the *current* + request (even on a multithreaded server). """ + bind = BaseRequest.__init__ + environ = _local_property() + + +class LocalResponse(BaseResponse): + """ A thread-local subclass of :class:`BaseResponse` with a different + set of attributes for each thread. There is usually only one global + instance of this class (:data:`response`). Its attributes are used + to build the HTTP response at the end of the request/response cycle. + """ + bind = BaseResponse.__init__ + _status_line = _local_property() + _status_code = _local_property() + _cookies = _local_property() + _headers = _local_property() + body = _local_property() + + +Request = BaseRequest +Response = BaseResponse + + +class HTTPResponse(Response, BottleException): + def __init__(self, body='', status=None, headers=None, **more_headers): + super(HTTPResponse, self).__init__(body, status, headers, **more_headers) + + def apply(self, other): + other._status_code = self._status_code + other._status_line = self._status_line + other._headers = self._headers + other._cookies = self._cookies + other.body = self.body + + +class HTTPError(HTTPResponse): + default_status = 500 + + def __init__(self, + status=None, + body=None, + exception=None, + traceback=None, **more_headers): + self.exception = exception + self.traceback = traceback + super(HTTPError, self).__init__(body, status, **more_headers) + +############################################################################### +# Plugins ###################################################################### +############################################################################### + + +class PluginError(BottleException): + pass + + +class JSONPlugin(object): + name = 'json' + api = 2 + + def __init__(self, json_dumps=json_dumps): + self.json_dumps = json_dumps + + def apply(self, callback, _): + dumps = self.json_dumps + if not dumps: return callback + + def wrapper(*a, **ka): + try: + rv = callback(*a, **ka) + except HTTPError: + rv = _e() + + if isinstance(rv, dict): + #Attempt to serialize, raises exception on failure + json_response = dumps(rv) + #Set content type only if serialization successful + response.content_type = 'application/json' + return json_response + elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict): + rv.body = dumps(rv.body) + rv.content_type = 'application/json' + return rv + + return wrapper + + +class TemplatePlugin(object): + """ This plugin applies the :func:`view` decorator to all routes with a + `template` config parameter. If the parameter is a tuple, the second + element must be a dict with additional options (e.g. `template_engine`) + or default variables for the template. """ + name = 'template' + api = 2 + + def setup(self, app): + app.tpl = self + + def apply(self, callback, route): + conf = route.config.get('template') + if isinstance(conf, (tuple, list)) and len(conf) == 2: + return view(conf[0], **conf[1])(callback) + elif isinstance(conf, str): + return view(conf)(callback) + else: + return callback + + +#: Not a plugin, but part of the plugin API. TODO: Find a better place. +class _ImportRedirect(object): + def __init__(self, name, impmask): + """ Create a virtual package that redirects imports (see PEP 302). """ + self.name = name + self.impmask = impmask + self.module = sys.modules.setdefault(name, imp.new_module(name)) + self.module.__dict__.update({ + '__file__': __file__, + '__path__': [], + '__all__': [], + '__loader__': self + }) + sys.meta_path.append(self) + + def find_module(self, fullname, path=None): + if '.' not in fullname: return + packname = fullname.rsplit('.', 1)[0] + if packname != self.name: return + return self + + def load_module(self, fullname): + if fullname in sys.modules: return sys.modules[fullname] + modname = fullname.rsplit('.', 1)[1] + realname = self.impmask % modname + __import__(realname) + module = sys.modules[fullname] = sys.modules[realname] + setattr(self.module, modname, module) + module.__loader__ = self + return module + +############################################################################### +# Common Utilities ############################################################# +############################################################################### + + +class MultiDict(DictMixin): + """ This dict stores multiple values per key, but behaves exactly like a + normal dict in that it returns only the newest value for any given key. + There are special methods available to access the full list of values. + """ + + def __init__(self, *a, **k): + self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items()) + + def __len__(self): + return len(self.dict) + + def __iter__(self): + return iter(self.dict) + + def __contains__(self, key): + return key in self.dict + + def __delitem__(self, key): + del self.dict[key] + + def __getitem__(self, key): + return self.dict[key][-1] + + def __setitem__(self, key, value): + self.append(key, value) + + def keys(self): + return self.dict.keys() + + if py3k: + + def values(self): + return (v[-1] for v in self.dict.values()) + + def items(self): + return ((k, v[-1]) for k, v in self.dict.items()) + + def allitems(self): + return ((k, v) for k, vl in self.dict.items() for v in vl) + + iterkeys = keys + itervalues = values + iteritems = items + iterallitems = allitems + + else: + + def values(self): + return [v[-1] for v in self.dict.values()] + + def items(self): + return [(k, v[-1]) for k, v in self.dict.items()] + + def iterkeys(self): + return self.dict.iterkeys() + + def itervalues(self): + return (v[-1] for v in self.dict.itervalues()) + + def iteritems(self): + return ((k, v[-1]) for k, v in self.dict.iteritems()) + + def iterallitems(self): + return ((k, v) for k, vl in self.dict.iteritems() for v in vl) + + def allitems(self): + return [(k, v) for k, vl in self.dict.iteritems() for v in vl] + + def get(self, key, default=None, index=-1, type=None): + """ Return the most recent value for a key. + + :param default: The default value to be returned if the key is not + present or the type conversion fails. + :param index: An index for the list of available values. + :param type: If defined, this callable is used to cast the value + into a specific type. Exception are suppressed and result in + the default value to be returned. + """ + try: + val = self.dict[key][index] + return type(val) if type else val + except Exception: + pass + return default + + def append(self, key, value): + """ Add a new value to the list of values for this key. """ + self.dict.setdefault(key, []).append(value) + + def replace(self, key, value): + """ Replace the list of values with a single value. """ + self.dict[key] = [value] + + def getall(self, key): + """ Return a (possibly empty) list of values for a key. """ + return self.dict.get(key) or [] + + #: Aliases for WTForms to mimic other multi-dict APIs (Django) + getone = get + getlist = getall + + +class FormsDict(MultiDict): + """ This :class:`MultiDict` subclass is used to store request form data. + Additionally to the normal dict-like item access methods (which return + unmodified data as native strings), this container also supports + attribute-like access to its values. Attributes are automatically de- + or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing + attributes default to an empty string. """ + + #: Encoding used for attribute values. + input_encoding = 'utf8' + #: If true (default), unicode strings are first encoded with `latin1` + #: and then decoded to match :attr:`input_encoding`. + recode_unicode = True + + def _fix(self, s, encoding=None): + if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI + return s.encode('latin1').decode(encoding or self.input_encoding) + elif isinstance(s, bytes): # Python 2 WSGI + return s.decode(encoding or self.input_encoding) + else: + return s + + def decode(self, encoding=None): + """ Returns a copy with all keys and values de- or recoded to match + :attr:`input_encoding`. Some libraries (e.g. WTForms) want a + unicode dictionary. """ + copy = FormsDict() + enc = copy.input_encoding = encoding or self.input_encoding + copy.recode_unicode = False + for key, value in self.allitems(): + copy.append(self._fix(key, enc), self._fix(value, enc)) + return copy + + def getunicode(self, name, default=None, encoding=None): + """ Return the value as a unicode string, or the default. """ + try: + return self._fix(self[name], encoding) + except (UnicodeError, KeyError): + return default + + def __getattr__(self, name, default=unicode()): + # Without this guard, pickle generates a cryptic TypeError: + if name.startswith('__') and name.endswith('__'): + return super(FormsDict, self).__getattr__(name) + return self.getunicode(name, default=default) + + +class HeaderDict(MultiDict): + """ A case-insensitive version of :class:`MultiDict` that defaults to + replace the old value instead of appending it. """ + + def __init__(self, *a, **ka): + self.dict = {} + if a or ka: self.update(*a, **ka) + + def __contains__(self, key): + return _hkey(key) in self.dict + + def __delitem__(self, key): + del self.dict[_hkey(key)] + + def __getitem__(self, key): + return self.dict[_hkey(key)][-1] + + def __setitem__(self, key, value): + self.dict[_hkey(key)] = [value if isinstance(value, unicode) else + str(value)] + + def append(self, key, value): + self.dict.setdefault(_hkey(key), []).append( + value if isinstance(value, unicode) else str(value)) + + def replace(self, key, value): + self.dict[_hkey(key)] = [value if isinstance(value, unicode) else + str(value)] + + def getall(self, key): + return self.dict.get(_hkey(key)) or [] + + def get(self, key, default=None, index=-1): + return MultiDict.get(self, _hkey(key), default, index) + + def filter(self, names): + for name in [_hkey(n) for n in names]: + if name in self.dict: + del self.dict[name] + + +class WSGIHeaderDict(DictMixin): + """ This dict-like class wraps a WSGI environ dict and provides convenient + access to HTTP_* fields. Keys and values are native strings + (2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI + environment contains non-native string values, these are de- or encoded + using a lossless 'latin1' character set. + + The API will remain stable even on changes to the relevant PEPs. + Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one + that uses non-native strings.) + """ + #: List of keys that do not have a ``HTTP_`` prefix. + cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH') + + def __init__(self, environ): + self.environ = environ + + def _ekey(self, key): + """ Translate header field name to CGI/WSGI environ key. """ + key = key.replace('-', '_').upper() + if key in self.cgikeys: + return key + return 'HTTP_' + key + + def raw(self, key, default=None): + """ Return the header value as is (may be bytes or unicode). """ + return self.environ.get(self._ekey(key), default) + + def __getitem__(self, key): + val = self.environ[self._ekey(key)] + if py3k: + if isinstance(val, unicode): + val = val.encode('latin1').decode('utf8') + else: + val = val.decode('utf8') + return val + + def __setitem__(self, key, value): + raise TypeError("%s is read-only." % self.__class__) + + def __delitem__(self, key): + raise TypeError("%s is read-only." % self.__class__) + + def __iter__(self): + for key in self.environ: + if key[:5] == 'HTTP_': + yield _hkey(key[5:]) + elif key in self.cgikeys: + yield _hkey(key) + + def keys(self): + return [x for x in self] + + def __len__(self): + return len(self.keys()) + + def __contains__(self, key): + return self._ekey(key) in self.environ + + +class ConfigDict(dict): + """ A dict-like configuration storage with additional support for + namespaces, validators, meta-data, on_change listeners and more. + """ + + __slots__ = ('_meta', '_on_change') + + def __init__(self): + self._meta = {} + self._on_change = lambda name, value: None + + def load_module(self, path, squash): + """ Load values from a Python module. + :param squash: Squash nested dicts into namespaces by using + load_dict(), otherwise use update() + Example: load_config('my.app.settings', True) + Example: load_config('my.app.settings', False) + """ + config_obj = __import__(path) + obj = dict([(key, getattr(config_obj, key)) + for key in dir(config_obj) if key.isupper()]) + if squash: + self.load_dict(obj) + else: + self.update(obj) + return self + + def load_config(self, filename): + """ Load values from an ``*.ini`` style config file. + + If the config file contains sections, their names are used as + namespaces for the values within. The two special sections + ``DEFAULT`` and ``bottle`` refer to the root namespace (no prefix). + """ + conf = ConfigParser() + conf.read(filename) + for section in conf.sections(): + for key, value in conf.items(section): + if section not in ('DEFAULT', 'bottle'): + key = section + '.' + key + self[key] = value + return self + + def load_dict(self, source, namespace=''): + """ Load values from a dictionary structure. Nesting can be used to + represent namespaces. + + >>> c = ConfigDict() + >>> c.load_dict({'some': {'namespace': {'key': 'value'} } }) + {'some.namespace.key': 'value'} + """ + for key, value in source.items(): + if isinstance(key, basestring): + nskey = (namespace + '.' + key).strip('.') + if isinstance(value, dict): + self.load_dict(value, namespace=nskey) + else: + self[nskey] = value + else: + raise TypeError('Key has type %r (not a string)' % type(key)) + return self + + def update(self, *a, **ka): + """ If the first parameter is a string, all keys are prefixed with this + namespace. Apart from that it works just as the usual dict.update(). + Example: ``update('some.namespace', key='value')`` """ + prefix = '' + if a and isinstance(a[0], basestring): + prefix = a[0].strip('.') + '.' + a = a[1:] + for key, value in dict(*a, **ka).items(): + self[prefix + key] = value + + def setdefault(self, key, value): + if key not in self: + self[key] = value + return self[key] + + def __setitem__(self, key, value): + if not isinstance(key, basestring): + raise TypeError('Key has type %r (not a string)' % type(key)) + value = self.meta_get(key, 'filter', lambda x: x)(value) + if key in self and self[key] is value: + return + self._on_change(key, value) + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + self._on_change(key, None) + dict.__delitem__(self, key) + + def meta_get(self, key, metafield, default=None): + """ Return the value of a meta field for a key. """ + return self._meta.get(key, {}).get(metafield, default) + + def meta_set(self, key, metafield, value): + """ Set the meta field for a key to a new value. This triggers the + on-change handler for existing keys. """ + self._meta.setdefault(key, {})[metafield] = value + if key in self: + self[key] = self[key] + + def meta_list(self, key): + """ Return an iterable of meta field names defined for a key. """ + return self._meta.get(key, {}).keys() + + +class AppStack(list): + """ A stack-like list. Calling it returns the head of the stack. """ + + def __call__(self): + """ Return the current default application. """ + return self[-1] + + def push(self, value=None): + """ Add a new :class:`Bottle` instance to the stack """ + if not isinstance(value, Bottle): + value = Bottle() + self.append(value) + return value + + +class WSGIFileWrapper(object): + def __init__(self, fp, buffer_size=1024 * 64): + self.fp, self.buffer_size = fp, buffer_size + for attr in ('fileno', 'close', 'read', 'readlines', 'tell', 'seek'): + if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr)) + + def __iter__(self): + buff, read = self.buffer_size, self.read + while True: + part = read(buff) + if not part: return + yield part + + +class _closeiter(object): + """ This only exists to be able to attach a .close method to iterators that + do not support attribute assignment (most of itertools). """ + + def __init__(self, iterator, close=None): + self.iterator = iterator + self.close_callbacks = makelist(close) + + def __iter__(self): + return iter(self.iterator) + + def close(self): + for func in self.close_callbacks: + func() + + +class ResourceManager(object): + """ This class manages a list of search paths and helps to find and open + application-bound resources (files). + + :param base: default value for :meth:`add_path` calls. + :param opener: callable used to open resources. + :param cachemode: controls which lookups are cached. One of 'all', + 'found' or 'none'. + """ + + def __init__(self, base='./', opener=open, cachemode='all'): + self.opener = opener + self.base = base + self.cachemode = cachemode + + #: A list of search paths. See :meth:`add_path` for details. + self.path = [] + #: A cache for resolved paths. ``res.cache.clear()`` clears the cache. + self.cache = {} + + def add_path(self, path, base=None, index=None, create=False): + """ Add a new path to the list of search paths. Return False if the + path does not exist. + + :param path: The new search path. Relative paths are turned into + an absolute and normalized form. If the path looks like a file + (not ending in `/`), the filename is stripped off. + :param base: Path used to absolutize relative search paths. + Defaults to :attr:`base` which defaults to ``os.getcwd()``. + :param index: Position within the list of search paths. Defaults + to last index (appends to the list). + + The `base` parameter makes it easy to reference files installed + along with a python module or package:: + + res.add_path('./resources/', __file__) + """ + base = os.path.abspath(os.path.dirname(base or self.base)) + path = os.path.abspath(os.path.join(base, os.path.dirname(path))) + path += os.sep + if path in self.path: + self.path.remove(path) + if create and not os.path.isdir(path): + os.makedirs(path) + if index is None: + self.path.append(path) + else: + self.path.insert(index, path) + self.cache.clear() + return os.path.exists(path) + + def __iter__(self): + """ Iterate over all existing files in all registered paths. """ + search = self.path[:] + while search: + path = search.pop() + if not os.path.isdir(path): continue + for name in os.listdir(path): + full = os.path.join(path, name) + if os.path.isdir(full): search.append(full) + else: yield full + + def lookup(self, name): + """ Search for a resource and return an absolute file path, or `None`. + + The :attr:`path` list is searched in order. The first match is + returend. Symlinks are followed. The result is cached to speed up + future lookups. """ + if name not in self.cache or DEBUG: + for path in self.path: + fpath = os.path.join(path, name) + if os.path.isfile(fpath): + if self.cachemode in ('all', 'found'): + self.cache[name] = fpath + return fpath + if self.cachemode == 'all': + self.cache[name] = None + return self.cache[name] + + def open(self, name, mode='r', *args, **kwargs): + """ Find a resource and return a file object, or raise IOError. """ + fname = self.lookup(name) + if not fname: raise IOError("Resource %r not found." % name) + return self.opener(fname, mode=mode, *args, **kwargs) + + +class FileUpload(object): + def __init__(self, fileobj, name, filename, headers=None): + """ Wrapper for file uploads. """ + #: Open file(-like) object (BytesIO buffer or temporary file) + self.file = fileobj + #: Name of the upload form field + self.name = name + #: Raw filename as sent by the client (may contain unsafe characters) + self.raw_filename = filename + #: A :class:`HeaderDict` with additional headers (e.g. content-type) + self.headers = HeaderDict(headers) if headers else HeaderDict() + + content_type = HeaderProperty('Content-Type') + content_length = HeaderProperty('Content-Length', reader=int, default=-1) + + @cached_property + def filename(self): + """ Name of the file on the client file system, but normalized to ensure + file system compatibility. An empty filename is returned as 'empty'. + + Only ASCII letters, digits, dashes, underscores and dots are + allowed in the final filename. Accents are removed, if possible. + Whitespace is replaced by a single dash. Leading or tailing dots + or dashes are removed. The filename is limited to 255 characters. + """ + fname = self.raw_filename + if not isinstance(fname, unicode): + fname = fname.decode('utf8', 'ignore') + fname = normalize('NFKD', fname) + fname = fname.encode('ASCII', 'ignore').decode('ASCII') + fname = os.path.basename(fname.replace('\\', os.path.sep)) + fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip() + fname = re.sub(r'[-\s]+', '-', fname).strip('.-') + return fname[:255] or 'empty' + + def _copy_file(self, fp, chunk_size=2 ** 16): + read, write, offset = self.file.read, fp.write, self.file.tell() + while 1: + buf = read(chunk_size) + if not buf: break + write(buf) + self.file.seek(offset) + + def save(self, destination, overwrite=False, chunk_size=2 ** 16): + """ Save file to disk or copy its content to an open file(-like) object. + If *destination* is a directory, :attr:`filename` is added to the + path. Existing files are not overwritten by default (IOError). + + :param destination: File path, directory or file(-like) object. + :param overwrite: If True, replace existing files. (default: False) + :param chunk_size: Bytes to read at a time. (default: 64kb) + """ + if isinstance(destination, basestring): # Except file-likes here + if os.path.isdir(destination): + destination = os.path.join(destination, self.filename) + if not overwrite and os.path.exists(destination): + raise IOError('File exists.') + with open(destination, 'wb') as fp: + self._copy_file(fp, chunk_size) + else: + self._copy_file(destination, chunk_size) + +############################################################################### +# Application Helper ########################################################### +############################################################################### + + +def abort(code=500, text='Unknown Error.'): + """ Aborts execution and causes a HTTP error. """ + raise HTTPError(code, text) + + +def redirect(url, code=None): + """ Aborts execution and causes a 303 or 302 redirect, depending on + the HTTP protocol version. """ + if not code: + code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 + res = response.copy(cls=HTTPResponse) + res.status = code + res.body = "" + res.set_header('Location', urljoin(request.url, url)) + raise res + + +def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024): + """ Yield chunks from a range in a file. No chunk is bigger than maxread.""" + fp.seek(offset) + while bytes > 0: + part = fp.read(min(bytes, maxread)) + if not part: break + bytes -= len(part) + yield part + + +def static_file(filename, root, + mimetype='auto', + download=False, + charset='UTF-8'): + """ Open a file in a safe way and return :exc:`HTTPResponse` with status + code 200, 305, 403 or 404. The ``Content-Type``, ``Content-Encoding``, + ``Content-Length`` and ``Last-Modified`` headers are set if possible. + Special support for ``If-Modified-Since``, ``Range`` and ``HEAD`` + requests. + + :param filename: Name or path of the file to send. + :param root: Root path for file lookups. Should be an absolute directory + path. + :param mimetype: Defines the content-type header (default: guess from + file extension) + :param download: If True, ask the browser to open a `Save as...` dialog + instead of opening the file with the associated program. You can + specify a custom filename as a string. If not specified, the + original filename is used (default: False). + :param charset: The charset to use for files with a ``text/*`` + mime-type. (default: UTF-8) + """ + + root = os.path.abspath(root) + os.sep + filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) + headers = dict() + + if not filename.startswith(root): + return HTTPError(403, "Access denied.") + if not os.path.exists(filename) or not os.path.isfile(filename): + return HTTPError(404, "File does not exist.") + if not os.access(filename, os.R_OK): + return HTTPError(403, "You do not have permission to access this file.") + + if mimetype == 'auto': + if download and download != True: + mimetype, encoding = mimetypes.guess_type(download) + else: + mimetype, encoding = mimetypes.guess_type(filename) + if encoding: headers['Content-Encoding'] = encoding + + if mimetype: + if mimetype[:5] == 'text/' and charset and 'charset' not in mimetype: + mimetype += '; charset=%s' % charset + headers['Content-Type'] = mimetype + + if download: + download = os.path.basename(filename if download == True else download) + headers['Content-Disposition'] = 'attachment; filename="%s"' % download + + stats = os.stat(filename) + headers['Content-Length'] = clen = stats.st_size + lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) + headers['Last-Modified'] = lm + + ims = request.environ.get('HTTP_IF_MODIFIED_SINCE') + if ims: + ims = parse_date(ims.split(";")[0].strip()) + if ims is not None and ims >= int(stats.st_mtime): + headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", + time.gmtime()) + return HTTPResponse(status=304, **headers) + + body = '' if request.method == 'HEAD' else open(filename, 'rb') + + headers["Accept-Ranges"] = "bytes" + ranges = request.environ.get('HTTP_RANGE') + if 'HTTP_RANGE' in request.environ: + ranges = list(parse_range_header(request.environ['HTTP_RANGE'], clen)) + if not ranges: + return HTTPError(416, "Requested Range Not Satisfiable") + offset, end = ranges[0] + headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) + headers["Content-Length"] = str(end - offset) + if body: body = _file_iter_range(body, offset, end - offset) + return HTTPResponse(body, status=206, **headers) + return HTTPResponse(body, **headers) + +############################################################################### +# HTTP Utilities and MISC (TODO) ############################################### +############################################################################### + + +def debug(mode=True): + """ Change the debug level. + There is only one debug level supported at the moment.""" + global DEBUG + if mode: warnings.simplefilter('default') + DEBUG = bool(mode) + + +def http_date(value): + if isinstance(value, (datedate, datetime)): + value = value.utctimetuple() + elif isinstance(value, (int, float)): + value = time.gmtime(value) + if not isinstance(value, basestring): + value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) + return value + + +def parse_date(ims): + """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ + try: + ts = email.utils.parsedate_tz(ims) + return time.mktime(ts[:8] + (0, )) - (ts[9] or 0) - time.timezone + except (TypeError, ValueError, IndexError, OverflowError): + return None + + +def parse_auth(header): + """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" + try: + method, data = header.split(None, 1) + if method.lower() == 'basic': + user, pwd = touni(base64.b64decode(tob(data))).split(':', 1) + return user, pwd + except (KeyError, ValueError): + return None + + +def parse_range_header(header, maxlen=0): + """ Yield (start, end) ranges parsed from a HTTP Range header. Skip + unsatisfiable ranges. The end index is non-inclusive.""" + if not header or header[:6] != 'bytes=': return + ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r] + for start, end in ranges: + try: + if not start: # bytes=-100 -> last 100 bytes + start, end = max(0, maxlen - int(end)), maxlen + elif not end: # bytes=100- -> all but the first 99 bytes + start, end = int(start), maxlen + else: # bytes=100-200 -> bytes 100-200 (inclusive) + start, end = int(start), min(int(end) + 1, maxlen) + if 0 <= start < end <= maxlen: + yield start, end + except ValueError: + pass + + +def _parse_qsl(qs): + r = [] + for pair in qs.replace(';', '&').split('&'): + if not pair: continue + nv = pair.split('=', 1) + if len(nv) != 2: nv.append('') + key = urlunquote(nv[0].replace('+', ' ')) + value = urlunquote(nv[1].replace('+', ' ')) + r.append((key, value)) + return r + + +def _lscmp(a, b): + """ Compares two strings in a cryptographically safe way: + Runtime is not affected by length of common prefix. """ + return not sum(0 if x == y else 1 + for x, y in zip(a, b)) and len(a) == len(b) + + +def cookie_encode(data, key): + """ Encode and sign a pickle-able object. Return a (byte) string """ + msg = base64.b64encode(pickle.dumps(data, -1)) + sig = base64.b64encode(hmac.new(tob(key), msg).digest()) + return tob('!') + sig + tob('?') + msg + + +def cookie_decode(data, key): + """ Verify and decode an encoded string. Return an object or None.""" + data = tob(data) + if cookie_is_encoded(data): + sig, msg = data.split(tob('?'), 1) + if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())): + return pickle.loads(base64.b64decode(msg)) + return None + + +def cookie_is_encoded(data): + """ Return True if the argument looks like a encoded cookie.""" + return bool(data.startswith(tob('!')) and tob('?') in data) + + +def html_escape(string): + """ Escape HTML special characters ``&<>`` and quotes ``'"``. """ + return string.replace('&', '&').replace('<', '<').replace('>', '>')\ + .replace('"', '"').replace("'", ''') + + +def html_quote(string): + """ Escape and quote a string to be used as an HTTP attribute.""" + return '"%s"' % html_escape(string).replace('\n', ' ')\ + .replace('\r', ' ').replace('\t', ' ') + + +def yieldroutes(func): + """ Return a generator for routes that match the signature (name, args) + of the func parameter. This may yield more than one route if the function + takes optional keyword arguments. The output is best described by example:: + + a() -> '/a' + b(x, y) -> '/b//' + c(x, y=5) -> '/c/' and '/c//' + d(x=5, y=6) -> '/d' and '/d/' and '/d//' + """ + path = '/' + func.__name__.replace('__', '/').lstrip('/') + spec = getargspec(func) + argc = len(spec[0]) - len(spec[3] or []) + path += ('/<%s>' * argc) % tuple(spec[0][:argc]) + yield path + for arg in spec[0][argc:]: + path += '/<%s>' % arg + yield path + + +def path_shift(script_name, path_info, shift=1): + """ Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. + + :return: The modified paths. + :param script_name: The SCRIPT_NAME path. + :param script_name: The PATH_INFO path. + :param shift: The number of path fragments to shift. May be negative to + change the shift direction. (default: 1) + """ + if shift == 0: return script_name, path_info + pathlist = path_info.strip('/').split('/') + scriptlist = script_name.strip('/').split('/') + if pathlist and pathlist[0] == '': pathlist = [] + if scriptlist and scriptlist[0] == '': scriptlist = [] + if 0 < shift <= len(pathlist): + moved = pathlist[:shift] + scriptlist = scriptlist + moved + pathlist = pathlist[shift:] + elif 0 > shift >= -len(scriptlist): + moved = scriptlist[shift:] + pathlist = moved + pathlist + scriptlist = scriptlist[:shift] + else: + empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO' + raise AssertionError("Cannot shift. Nothing left from %s" % empty) + new_script_name = '/' + '/'.join(scriptlist) + new_path_info = '/' + '/'.join(pathlist) + if path_info.endswith('/') and pathlist: new_path_info += '/' + return new_script_name, new_path_info + + +def auth_basic(check, realm="private", text="Access denied"): + """ Callback decorator to require HTTP auth (basic). + TODO: Add route(check_auth=...) parameter. """ + + def decorator(func): + + @functools.wraps(func) + def wrapper(*a, **ka): + user, password = request.auth or (None, None) + if user is None or not check(user, password): + err = HTTPError(401, text) + err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm) + return err + return func(*a, **ka) + + return wrapper + + return decorator + +# Shortcuts for common Bottle methods. +# They all refer to the current default application. + + +def make_default_app_wrapper(name): + """ Return a callable that relays calls to the current default app. """ + + @functools.wraps(getattr(Bottle, name)) + def wrapper(*a, **ka): + return getattr(app(), name)(*a, **ka) + + return wrapper + + +route = make_default_app_wrapper('route') +get = make_default_app_wrapper('get') +post = make_default_app_wrapper('post') +put = make_default_app_wrapper('put') +delete = make_default_app_wrapper('delete') +patch = make_default_app_wrapper('patch') +error = make_default_app_wrapper('error') +mount = make_default_app_wrapper('mount') +hook = make_default_app_wrapper('hook') +install = make_default_app_wrapper('install') +uninstall = make_default_app_wrapper('uninstall') +url = make_default_app_wrapper('get_url') + +############################################################################### +# Server Adapter ############################################################### +############################################################################### + + +class ServerAdapter(object): + quiet = False + + def __init__(self, host='127.0.0.1', port=8080, **options): + self.options = options + self.host = host + self.port = int(port) + + def run(self, handler): # pragma: no cover + pass + + def __repr__(self): + args = ', '.join(['%s=%s' % (k, repr(v)) + for k, v in self.options.items()]) + return "%s(%s)" % (self.__class__.__name__, args) + + +class CGIServer(ServerAdapter): + quiet = True + + def run(self, handler): # pragma: no cover + from wsgiref.handlers import CGIHandler + + def fixed_environ(environ, start_response): + environ.setdefault('PATH_INFO', '') + return handler(environ, start_response) + + CGIHandler().run(fixed_environ) + + +class FlupFCGIServer(ServerAdapter): + def run(self, handler): # pragma: no cover + import flup.server.fcgi + self.options.setdefault('bindAddress', (self.host, self.port)) + flup.server.fcgi.WSGIServer(handler, **self.options).run() + + +class WSGIRefServer(ServerAdapter): + def run(self, app): # pragma: no cover + from wsgiref.simple_server import make_server + from wsgiref.simple_server import WSGIRequestHandler, WSGIServer + import socket + + class FixedHandler(WSGIRequestHandler): + def address_string(self): # Prevent reverse DNS lookups please. + return self.client_address[0] + + def log_request(*args, **kw): + if not self.quiet: + return WSGIRequestHandler.log_request(*args, **kw) + + handler_cls = self.options.get('handler_class', FixedHandler) + server_cls = self.options.get('server_class', WSGIServer) + + if ':' in self.host: # Fix wsgiref for IPv6 addresses. + if getattr(server_cls, 'address_family') == socket.AF_INET: + + class server_cls(server_cls): + address_family = socket.AF_INET6 + + self.srv = make_server(self.host, self.port, app, server_cls, + handler_cls) + self.port = self.srv.server_port # update port actual port (0 means random) + try: + self.srv.serve_forever() + except KeyboardInterrupt: + self.srv.server_close() # Prevent ResourceWarning: unclosed socket + raise + + +class CherryPyServer(ServerAdapter): + def run(self, handler): # pragma: no cover + from cherrypy import wsgiserver + self.options['bind_addr'] = (self.host, self.port) + self.options['wsgi_app'] = handler + + certfile = self.options.get('certfile') + if certfile: + del self.options['certfile'] + keyfile = self.options.get('keyfile') + if keyfile: + del self.options['keyfile'] + + server = wsgiserver.CherryPyWSGIServer(**self.options) + if certfile: + server.ssl_certificate = certfile + if keyfile: + server.ssl_private_key = keyfile + + try: + server.start() + finally: + server.stop() + + +class WaitressServer(ServerAdapter): + def run(self, handler): + from waitress import serve + serve(handler, host=self.host, port=self.port, _quiet=self.quiet) + + +class PasteServer(ServerAdapter): + def run(self, handler): # pragma: no cover + from paste import httpserver + from paste.translogger import TransLogger + handler = TransLogger(handler, setup_console_handler=(not self.quiet)) + httpserver.serve(handler, + host=self.host, + port=str(self.port), **self.options) + + +class MeinheldServer(ServerAdapter): + def run(self, handler): + from meinheld import server + server.listen((self.host, self.port)) + server.run(handler) + + +class FapwsServer(ServerAdapter): + """ Extremely fast webserver using libev. See http://www.fapws.org/ """ + + def run(self, handler): # pragma: no cover + import fapws._evwsgi as evwsgi + from fapws import base, config + port = self.port + if float(config.SERVER_IDENT[-2:]) > 0.4: + # fapws3 silently changed its API in 0.5 + port = str(port) + evwsgi.start(self.host, port) + # fapws3 never releases the GIL. Complain upstream. I tried. No luck. + if 'BOTTLE_CHILD' in os.environ and not self.quiet: + _stderr("WARNING: Auto-reloading does not work with Fapws3.\n") + _stderr(" (Fapws3 breaks python thread support)\n") + evwsgi.set_base_module(base) + + def app(environ, start_response): + environ['wsgi.multiprocess'] = False + return handler(environ, start_response) + + evwsgi.wsgi_cb(('', app)) + evwsgi.run() + + +class TornadoServer(ServerAdapter): + """ The super hyped asynchronous server by facebook. Untested. """ + + def run(self, handler): # pragma: no cover + import tornado.wsgi, tornado.httpserver, tornado.ioloop + container = tornado.wsgi.WSGIContainer(handler) + server = tornado.httpserver.HTTPServer(container) + server.listen(port=self.port, address=self.host) + tornado.ioloop.IOLoop.instance().start() + + +class AppEngineServer(ServerAdapter): + """ Adapter for Google App Engine. """ + quiet = True + + def run(self, handler): + from google.appengine.ext.webapp import util + # A main() function in the handler script enables 'App Caching'. + # Lets makes sure it is there. This _really_ improves performance. + module = sys.modules.get('__main__') + if module and not hasattr(module, 'main'): + module.main = lambda: util.run_wsgi_app(handler) + util.run_wsgi_app(handler) + + +class TwistedServer(ServerAdapter): + """ Untested. """ + + def run(self, handler): + from twisted.web import server, wsgi + from twisted.python.threadpool import ThreadPool + from twisted.internet import reactor + thread_pool = ThreadPool() + thread_pool.start() + reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop) + factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler)) + reactor.listenTCP(self.port, factory, interface=self.host) + if not reactor.running: + reactor.run() + + +class DieselServer(ServerAdapter): + """ Untested. """ + + def run(self, handler): + from diesel.protocols.wsgi import WSGIApplication + app = WSGIApplication(handler, port=self.port) + app.run() + + +class GeventServer(ServerAdapter): + """ Untested. Options: + + * `fast` (default: False) uses libevent's http server, but has some + issues: No streaming, no pipelining, no SSL. + * See gevent.wsgi.WSGIServer() documentation for more options. + """ + + def run(self, handler): + from gevent import wsgi, pywsgi, local + if not isinstance(threading.local(), local.local): + msg = "Bottle requires gevent.monkey.patch_all() (before import)" + raise RuntimeError(msg) + if not self.options.pop('fast', None): wsgi = pywsgi + self.options['log'] = None if self.quiet else 'default' + address = (self.host, self.port) + server = wsgi.WSGIServer(address, handler, **self.options) + if 'BOTTLE_CHILD' in os.environ: + import signal + signal.signal(signal.SIGINT, lambda s, f: server.stop()) + server.serve_forever() + + +class GeventSocketIOServer(ServerAdapter): + def run(self, handler): + from socketio import server + address = (self.host, self.port) + server.SocketIOServer(address, handler, **self.options).serve_forever() + + +class GunicornServer(ServerAdapter): + """ Untested. See http://gunicorn.org/configure.html for options. """ + + def run(self, handler): + from gunicorn.app.base import Application + + config = {'bind': "%s:%d" % (self.host, int(self.port))} + config.update(self.options) + + class GunicornApplication(Application): + def init(self, parser, opts, args): + return config + + def load(self): + return handler + + GunicornApplication().run() + + +class EventletServer(ServerAdapter): + """ Untested. Options: + + * `backlog` adjust the eventlet backlog parameter which is the maximum + number of queued connections. Should be at least 1; the maximum + value is system-dependent. + * `family`: (default is 2) socket family, optional. See socket + documentation for available families. + """ + + def run(self, handler): + from eventlet import wsgi, listen, patcher + if not patcher.is_monkey_patched(os): + msg = "Bottle requires eventlet.monkey_patch() (before import)" + raise RuntimeError(msg) + socket_args = {} + for arg in ('backlog', 'family'): + try: + socket_args[arg] = self.options.pop(arg) + except KeyError: + pass + address = (self.host, self.port) + try: + wsgi.server(listen(address, **socket_args), handler, + log_output=(not self.quiet)) + except TypeError: + # Fallback, if we have old version of eventlet + wsgi.server(listen(address), handler) + + +class RocketServer(ServerAdapter): + """ Untested. """ + + def run(self, handler): + from rocket import Rocket + server = Rocket((self.host, self.port), 'wsgi', {'wsgi_app': handler}) + server.start() + + +class BjoernServer(ServerAdapter): + """ Fast server written in C: https://github.com/jonashaag/bjoern """ + + def run(self, handler): + from bjoern import run + run(handler, self.host, self.port) + + +class AiohttpServer(ServerAdapter): + """ Untested. + aiohttp + https://pypi.python.org/pypi/aiohttp/ + """ + + def run(self, handler): + import asyncio + from aiohttp.wsgi import WSGIServerHttpProtocol + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + protocol_factory = lambda: WSGIServerHttpProtocol( + handler, + readpayload=True, + debug=(not self.quiet)) + self.loop.run_until_complete(self.loop.create_server(protocol_factory, + self.host, + self.port)) + + if 'BOTTLE_CHILD' in os.environ: + import signal + signal.signal(signal.SIGINT, lambda s, f: self.loop.stop()) + + try: + self.loop.run_forever() + except KeyboardInterrupt: + self.loop.stop() + + +class AutoServer(ServerAdapter): + """ Untested. """ + adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, + WSGIRefServer] + + def run(self, handler): + for sa in self.adapters: + try: + return sa(self.host, self.port, **self.options).run(handler) + except ImportError: + pass + + +server_names = { + 'cgi': CGIServer, + 'flup': FlupFCGIServer, + 'wsgiref': WSGIRefServer, + 'waitress': WaitressServer, + 'cherrypy': CherryPyServer, + 'paste': PasteServer, + 'fapws3': FapwsServer, + 'tornado': TornadoServer, + 'gae': AppEngineServer, + 'twisted': TwistedServer, + 'diesel': DieselServer, + 'meinheld': MeinheldServer, + 'gunicorn': GunicornServer, + 'eventlet': EventletServer, + 'gevent': GeventServer, + 'geventSocketIO': GeventSocketIOServer, + 'rocket': RocketServer, + 'bjoern': BjoernServer, + 'aiohttp': AiohttpServer, + 'auto': AutoServer, +} + +############################################################################### +# Application Control ########################################################## +############################################################################### + + +def load(target, **namespace): + """ Import a module or fetch an object from a module. + + * ``package.module`` returns `module` as a module object. + * ``pack.mod:name`` returns the module variable `name` from `pack.mod`. + * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result. + + The last form accepts not only function calls, but any type of + expression. Keyword arguments passed to this function are available as + local variables. Example: ``import_string('re:compile(x)', x='[a-z]')`` + """ + module, target = target.split(":", 1) if ':' in target else (target, None) + if module not in sys.modules: __import__(module) + if not target: return sys.modules[module] + if target.isalnum(): return getattr(sys.modules[module], target) + package_name = module.split('.')[0] + namespace[package_name] = sys.modules[package_name] + return eval('%s.%s' % (module, target), namespace) + + +def load_app(target): + """ Load a bottle application from a module and make sure that the import + does not affect the current default application, but returns a separate + application object. See :func:`load` for the target parameter. """ + global NORUN + NORUN, nr_old = True, NORUN + tmp = default_app.push() # Create a new "default application" + try: + rv = load(target) # Import the target module + return rv if callable(rv) else tmp + finally: + default_app.remove(tmp) # Remove the temporary added default application + NORUN = nr_old + + +_debug = debug + + +def run(app=None, + server='wsgiref', + host='127.0.0.1', + port=8080, + interval=1, + reloader=False, + quiet=False, + plugins=None, + debug=None, + config=None, **kargs): + """ Start a server instance. This method blocks until the server terminates. + + :param app: WSGI application or target string supported by + :func:`load_app`. (default: :func:`default_app`) + :param server: Server adapter to use. See :data:`server_names` keys + for valid names or pass a :class:`ServerAdapter` subclass. + (default: `wsgiref`) + :param host: Server address to bind to. Pass ``0.0.0.0`` to listens on + all interfaces including the external one. (default: 127.0.0.1) + :param port: Server port to bind to. Values below 1024 require root + privileges. (default: 8080) + :param reloader: Start auto-reloading server? (default: False) + :param interval: Auto-reloader interval in seconds (default: 1) + :param quiet: Suppress output to stdout and stderr? (default: False) + :param options: Options passed to the server adapter. + """ + if NORUN: return + if reloader and not os.environ.get('BOTTLE_CHILD'): + import subprocess + lockfile = None + try: + fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') + os.close(fd) # We only need this file to exist. We never write to it + while os.path.exists(lockfile): + args = [sys.executable] + sys.argv + environ = os.environ.copy() + environ['BOTTLE_CHILD'] = 'true' + environ['BOTTLE_LOCKFILE'] = lockfile + p = subprocess.Popen(args, env=environ) + while p.poll() is None: # Busy wait... + os.utime(lockfile, None) # I am alive! + time.sleep(interval) + if p.poll() != 3: + if os.path.exists(lockfile): os.unlink(lockfile) + sys.exit(p.poll()) + except KeyboardInterrupt: + pass + finally: + if os.path.exists(lockfile): + os.unlink(lockfile) + return + + try: + if debug is not None: _debug(debug) + app = app or default_app() + if isinstance(app, basestring): + app = load_app(app) + if not callable(app): + raise ValueError("Application is not callable: %r" % app) + + for plugin in plugins or []: + if isinstance(plugin, basestring): + plugin = load(plugin) + app.install(plugin) + + if config: + app.config.update(config) + + if server in server_names: + server = server_names.get(server) + if isinstance(server, basestring): + server = load(server) + if isinstance(server, type): + server = server(host=host, port=port, **kargs) + if not isinstance(server, ServerAdapter): + raise ValueError("Unknown or unsupported server: %r" % server) + + server.quiet = server.quiet or quiet + if not server.quiet: + _stderr("Bottle v%s server starting up (using %s)...\n" % + (__version__, repr(server))) + _stderr("Listening on http://%s:%d/\n" % + (server.host, server.port)) + _stderr("Hit Ctrl-C to quit.\n\n") + + if reloader: + lockfile = os.environ.get('BOTTLE_LOCKFILE') + bgcheck = FileCheckerThread(lockfile, interval) + with bgcheck: + server.run(app) + if bgcheck.status == 'reload': + sys.exit(3) + else: + server.run(app) + except KeyboardInterrupt: + pass + except (SystemExit, MemoryError): + raise + except: + if not reloader: raise + if not getattr(server, 'quiet', quiet): + print_exc() + time.sleep(interval) + sys.exit(3) + + +class FileCheckerThread(threading.Thread): + """ Interrupt main-thread as soon as a changed module file is detected, + the lockfile gets deleted or gets to old. """ + + def __init__(self, lockfile, interval): + threading.Thread.__init__(self) + self.daemon = True + self.lockfile, self.interval = lockfile, interval + #: Is one of 'reload', 'error' or 'exit' + self.status = None + + def run(self): + exists = os.path.exists + mtime = lambda p: os.stat(p).st_mtime + files = dict() + + for module in list(sys.modules.values()): + path = getattr(module, '__file__', '') + if path[-4:] in ('.pyo', '.pyc'): path = path[:-1] + if path and exists(path): files[path] = mtime(path) + + while not self.status: + if not exists(self.lockfile)\ + or mtime(self.lockfile) < time.time() - self.interval - 5: + self.status = 'error' + thread.interrupt_main() + for path, lmtime in list(files.items()): + if not exists(path) or mtime(path) > lmtime: + self.status = 'reload' + thread.interrupt_main() + break + time.sleep(self.interval) + + def __enter__(self): + self.start() + + def __exit__(self, exc_type, *_): + if not self.status: self.status = 'exit' # silent exit + self.join() + return exc_type is not None and issubclass(exc_type, KeyboardInterrupt) + +############################################################################### +# Template Adapters ############################################################ +############################################################################### + + +class TemplateError(HTTPError): + def __init__(self, message): + HTTPError.__init__(self, 500, message) + + +class BaseTemplate(object): + """ Base class and minimal API for template adapters """ + extensions = ['tpl', 'html', 'thtml', 'stpl'] + settings = {} #used in prepare() + defaults = {} #used in render() + + def __init__(self, + source=None, + name=None, + lookup=None, + encoding='utf8', **settings): + """ Create a new template. + If the source parameter (str or buffer) is missing, the name argument + is used to guess a template filename. Subclasses can assume that + self.source and/or self.filename are set. Both are strings. + The lookup, encoding and settings parameters are stored as instance + variables. + The lookup parameter stores a list containing directory paths. + The encoding parameter should be used to decode byte strings or files. + The settings parameter contains a dict for engine-specific settings. + """ + self.name = name + self.source = source.read() if hasattr(source, 'read') else source + self.filename = source.filename if hasattr(source, 'filename') else None + self.lookup = [os.path.abspath(x) for x in lookup] if lookup else [] + self.encoding = encoding + self.settings = self.settings.copy() # Copy from class variable + self.settings.update(settings) # Apply + if not self.source and self.name: + self.filename = self.search(self.name, self.lookup) + if not self.filename: + raise TemplateError('Template %s not found.' % repr(name)) + if not self.source and not self.filename: + raise TemplateError('No template specified.') + self.prepare(**self.settings) + + @classmethod + def search(cls, name, lookup=None): + """ Search name in all directories specified in lookup. + First without, then with common extensions. Return first hit. """ + if not lookup: + depr('The template lookup path list should not be empty.', + True) #0.12 + lookup = ['.'] + + if os.path.isabs(name) and os.path.isfile(name): + depr('Absolute template path names are deprecated.', True) #0.12 + return os.path.abspath(name) + + for spath in lookup: + spath = os.path.abspath(spath) + os.sep + fname = os.path.abspath(os.path.join(spath, name)) + if not fname.startswith(spath): continue + if os.path.isfile(fname): return fname + for ext in cls.extensions: + if os.path.isfile('%s.%s' % (fname, ext)): + return '%s.%s' % (fname, ext) + + @classmethod + def global_config(cls, key, *args): + """ This reads or sets the global settings stored in class.settings. """ + if args: + cls.settings = cls.settings.copy() # Make settings local to class + cls.settings[key] = args[0] + else: + return cls.settings[key] + + def prepare(self, **options): + """ Run preparations (parsing, caching, ...). + It should be possible to call this again to refresh a template or to + update settings. + """ + raise NotImplementedError + + def render(self, *args, **kwargs): + """ Render the template with the specified local variables and return + a single byte or unicode string. If it is a byte string, the encoding + must match self.encoding. This method must be thread-safe! + Local variables may be provided in dictionaries (args) + or directly, as keywords (kwargs). + """ + raise NotImplementedError + + +class MakoTemplate(BaseTemplate): + def prepare(self, **options): + from mako.template import Template + from mako.lookup import TemplateLookup + options.update({'input_encoding': self.encoding}) + options.setdefault('format_exceptions', bool(DEBUG)) + lookup = TemplateLookup(directories=self.lookup, **options) + if self.source: + self.tpl = Template(self.source, lookup=lookup, **options) + else: + self.tpl = Template(uri=self.name, + filename=self.filename, + lookup=lookup, **options) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + _defaults = self.defaults.copy() + _defaults.update(kwargs) + return self.tpl.render(**_defaults) + + +class CheetahTemplate(BaseTemplate): + def prepare(self, **options): + from Cheetah.Template import Template + self.context = threading.local() + self.context.vars = {} + options['searchList'] = [self.context.vars] + if self.source: + self.tpl = Template(source=self.source, **options) + else: + self.tpl = Template(file=self.filename, **options) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + self.context.vars.update(self.defaults) + self.context.vars.update(kwargs) + out = str(self.tpl) + self.context.vars.clear() + return out + + +class Jinja2Template(BaseTemplate): + def prepare(self, filters=None, tests=None, globals={}, **kwargs): + from jinja2 import Environment, FunctionLoader + self.env = Environment(loader=FunctionLoader(self.loader), **kwargs) + if filters: self.env.filters.update(filters) + if tests: self.env.tests.update(tests) + if globals: self.env.globals.update(globals) + if self.source: + self.tpl = self.env.from_string(self.source) + else: + self.tpl = self.env.get_template(self.filename) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + _defaults = self.defaults.copy() + _defaults.update(kwargs) + return self.tpl.render(**_defaults) + + def loader(self, name): + fname = self.search(name, self.lookup) + if not fname: return + with open(fname, "rb") as f: + return f.read().decode(self.encoding) + + +class SimpleTemplate(BaseTemplate): + def prepare(self, + escape_func=html_escape, + noescape=False, + syntax=None, **ka): + self.cache = {} + enc = self.encoding + self._str = lambda x: touni(x, enc) + self._escape = lambda x: escape_func(touni(x, enc)) + self.syntax = syntax + if noescape: + self._str, self._escape = self._escape, self._str + + @cached_property + def co(self): + return compile(self.code, self.filename or '', 'exec') + + @cached_property + def code(self): + source = self.source + if not source: + with open(self.filename, 'rb') as f: + source = f.read() + try: + source, encoding = touni(source), 'utf8' + except UnicodeError: + depr('Template encodings other than utf8 are not supported.') #0.11 + source, encoding = touni(source, 'latin1'), 'latin1' + parser = StplParser(source, encoding=encoding, syntax=self.syntax) + code = parser.translate() + self.encoding = parser.encoding + return code + + def _rebase(self, _env, _name=None, **kwargs): + _env['_rebase'] = (_name, kwargs) + + def _include(self, _env, _name=None, **kwargs): + env = _env.copy() + env.update(kwargs) + if _name not in self.cache: + self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) + return self.cache[_name].execute(env['_stdout'], env) + + def execute(self, _stdout, kwargs): + env = self.defaults.copy() + env.update(kwargs) + env.update({ + '_stdout': _stdout, + '_printlist': _stdout.extend, + 'include': functools.partial(self._include, env), + 'rebase': functools.partial(self._rebase, env), + '_rebase': None, + '_str': self._str, + '_escape': self._escape, + 'get': env.get, + 'setdefault': env.setdefault, + 'defined': env.__contains__ + }) + eval(self.co, env) + if env.get('_rebase'): + subtpl, rargs = env.pop('_rebase') + rargs['base'] = ''.join(_stdout) #copy stdout + del _stdout[:] # clear stdout + return self._include(env, subtpl, **rargs) + return env + + def render(self, *args, **kwargs): + """ Render the template using keyword arguments as local variables. """ + env = {} + stdout = [] + for dictarg in args: + env.update(dictarg) + env.update(kwargs) + self.execute(stdout, env) + return ''.join(stdout) + + +class StplSyntaxError(TemplateError): + + pass + + +class StplParser(object): + """ Parser for stpl templates. """ + _re_cache = {} #: Cache for compiled re patterns + + # This huge pile of voodoo magic splits python code into 8 different tokens. + # We use the verbose (?x) regex mode to make this more manageable + + _re_tok = _re_inl = r'''((?mx) # verbose and dot-matches-newline mode + [urbURB]* + (?: ''(?!') + |""(?!") + |'{6} + |"{6} + |'(?:[^\\']|\\.)+?' + |"(?:[^\\"]|\\.)+?" + |'{3}(?:[^\\]|\\.|\n)+?'{3} + |"{3}(?:[^\\]|\\.|\n)+?"{3} + ) + )''' + + _re_inl = _re_tok.replace(r'|\n', '') # We re-use this string pattern later + + _re_tok += r''' + # 2: Comments (until end of line, but not the newline itself) + |(\#.*) + + # 3: Open and close (4) grouping tokens + |([\[\{\(]) + |([\]\}\)]) + + # 5,6: Keywords that start or continue a python block (only start of line) + |^([\ \t]*(?:if|for|while|with|try|def|class)\b) + |^([\ \t]*(?:elif|else|except|finally)\b) + + # 7: Our special 'end' keyword (but only if it stands alone) + |((?:^|;)[\ \t]*end[\ \t]*(?=(?:%(block_close)s[\ \t]*)?\r?$|;|\#)) + + # 8: A customizable end-of-code-block template token (only end of line) + |(%(block_close)s[\ \t]*(?=\r?$)) + + # 9: And finally, a single newline. The 10th token is 'everything else' + |(\r?\n) + ''' + + # Match the start tokens of code areas in a template + _re_split = r'''(?m)^[ \t]*(\\?)((%(line_start)s)|(%(block_start)s))''' + # Match inline statements (may contain python strings) + _re_inl = r'''%%(inline_start)s((?:%s|[^'"\n]+?)*?)%%(inline_end)s''' % _re_inl + + default_syntax = '<% %> % {{ }}' + + def __init__(self, source, syntax=None, encoding='utf8'): + self.source, self.encoding = touni(source, encoding), encoding + self.set_syntax(syntax or self.default_syntax) + self.code_buffer, self.text_buffer = [], [] + self.lineno, self.offset = 1, 0 + self.indent, self.indent_mod = 0, 0 + self.paren_depth = 0 + + def get_syntax(self): + """ Tokens as a space separated string (default: <% %> % {{ }}) """ + return self._syntax + + def set_syntax(self, syntax): + self._syntax = syntax + self._tokens = syntax.split() + if not syntax in self._re_cache: + names = 'block_start block_close line_start inline_start inline_end' + etokens = map(re.escape, self._tokens) + pattern_vars = dict(zip(names.split(), etokens)) + patterns = (self._re_split, self._re_tok, self._re_inl) + patterns = [re.compile(p % pattern_vars) for p in patterns] + self._re_cache[syntax] = patterns + self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax] + + syntax = property(get_syntax, set_syntax) + + def translate(self): + if self.offset: raise RuntimeError('Parser is a one time instance.') + while True: + m = self.re_split.search(self.source, pos=self.offset) + if m: + text = self.source[self.offset:m.start()] + self.text_buffer.append(text) + self.offset = m.end() + if m.group(1): # Escape syntax + line, sep, _ = self.source[self.offset:].partition('\n') + self.text_buffer.append(self.source[m.start():m.start(1)] + + m.group(2) + line + sep) + self.offset += len(line + sep) + continue + self.flush_text() + self.offset += self.read_code(self.source[self.offset:], + multiline=bool(m.group(4))) + else: + break + self.text_buffer.append(self.source[self.offset:]) + self.flush_text() + return ''.join(self.code_buffer) + + def read_code(self, pysource, multiline): + code_line, comment = '', '' + offset = 0 + while True: + m = self.re_tok.search(pysource, pos=offset) + if not m: + code_line += pysource[offset:] + offset = len(pysource) + self.write_code(code_line.strip(), comment) + break + code_line += pysource[offset:m.start()] + offset = m.end() + _str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups() + if self.paren_depth > 0 and (_blk1 or _blk2): # a if b else c + code_line += _blk1 or _blk2 + continue + if _str: # Python string + code_line += _str + elif _com: # Python comment (up to EOL) + comment = _com + if multiline and _com.strip().endswith(self._tokens[1]): + multiline = False # Allow end-of-block in comments + elif _po: # open parenthesis + self.paren_depth += 1 + code_line += _po + elif _pc: # close parenthesis + if self.paren_depth > 0: + # we could check for matching parentheses here, but it's + # easier to leave that to python - just check counts + self.paren_depth -= 1 + code_line += _pc + elif _blk1: # Start-block keyword (if/for/while/def/try/...) + code_line, self.indent_mod = _blk1, -1 + self.indent += 1 + elif _blk2: # Continue-block keyword (else/elif/except/...) + code_line, self.indent_mod = _blk2, -1 + elif _end: # The non-standard 'end'-keyword (ends a block) + self.indent -= 1 + elif _cend: # The end-code-block template token (usually '%>') + if multiline: multiline = False + else: code_line += _cend + else: # \n + self.write_code(code_line.strip(), comment) + self.lineno += 1 + code_line, comment, self.indent_mod = '', '', 0 + if not multiline: + break + + return offset + + def flush_text(self): + text = ''.join(self.text_buffer) + del self.text_buffer[:] + if not text: return + parts, pos, nl = [], 0, '\\\n' + ' ' * self.indent + for m in self.re_inl.finditer(text): + prefix, pos = text[pos:m.start()], m.end() + if prefix: + parts.append(nl.join(map(repr, prefix.splitlines(True)))) + if prefix.endswith('\n'): parts[-1] += nl + parts.append(self.process_inline(m.group(1).strip())) + if pos < len(text): + prefix = text[pos:] + lines = prefix.splitlines(True) + if lines[-1].endswith('\\\\\n'): lines[-1] = lines[-1][:-3] + elif lines[-1].endswith('\\\\\r\n'): lines[-1] = lines[-1][:-4] + parts.append(nl.join(map(repr, lines))) + code = '_printlist((%s,))' % ', '.join(parts) + self.lineno += code.count('\n') + 1 + self.write_code(code) + + @staticmethod + def process_inline(chunk): + if chunk[0] == '!': return '_str(%s)' % chunk[1:] + return '_escape(%s)' % chunk + + def write_code(self, line, comment=''): + code = ' ' * (self.indent + self.indent_mod) + code += line.lstrip() + comment + '\n' + self.code_buffer.append(code) + + +def template(*args, **kwargs): + """ + Get a rendered template as a string iterator. + You can use a name, a filename or a template string as first parameter. + Template rendering arguments can be passed as dictionaries + or directly (as keyword arguments). + """ + tpl = args[0] if args else None + adapter = kwargs.pop('template_adapter', SimpleTemplate) + lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) + tplid = (id(lookup), tpl) + if tplid not in TEMPLATES or DEBUG: + settings = kwargs.pop('template_settings', {}) + if isinstance(tpl, adapter): + TEMPLATES[tplid] = tpl + if settings: TEMPLATES[tplid].prepare(**settings) + elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: + TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings) + else: + TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) + if not TEMPLATES[tplid]: + abort(500, 'Template (%s) not found' % tpl) + for dictarg in args[1:]: + kwargs.update(dictarg) + return TEMPLATES[tplid].render(kwargs) + + +mako_template = functools.partial(template, template_adapter=MakoTemplate) +cheetah_template = functools.partial(template, + template_adapter=CheetahTemplate) +jinja2_template = functools.partial(template, template_adapter=Jinja2Template) + + +def view(tpl_name, **defaults): + """ Decorator: renders a template for a handler. + The handler can control its behavior like that: + + - return a dict of template vars to fill out the template + - return something other than a dict and the view decorator will not + process the template, but return the handler result as is. + This includes returning a HTTPResponse(dict) to get, + for instance, JSON with autojson or other castfilters. + """ + + def decorator(func): + + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if isinstance(result, (dict, DictMixin)): + tplvars = defaults.copy() + tplvars.update(result) + return template(tpl_name, **tplvars) + elif result is None: + return template(tpl_name, defaults) + return result + + return wrapper + + return decorator + + +mako_view = functools.partial(view, template_adapter=MakoTemplate) +cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) +jinja2_view = functools.partial(view, template_adapter=Jinja2Template) + +############################################################################### +# Constants and Globals ######################################################## +############################################################################### + +TEMPLATE_PATH = ['./', './views/'] +TEMPLATES = {} +DEBUG = False +NORUN = False # If set, run() does nothing. Used by load_app() + +#: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') +HTTP_CODES = httplib.responses.copy() +HTTP_CODES[418] = "I'm a teapot" # RFC 2324 +HTTP_CODES[428] = "Precondition Required" +HTTP_CODES[429] = "Too Many Requests" +HTTP_CODES[431] = "Request Header Fields Too Large" +HTTP_CODES[511] = "Network Authentication Required" +_HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v)) + for (k, v) in HTTP_CODES.items()) + +#: The default template used for error pages. Override with @error() +ERROR_PAGE_TEMPLATE = """ +%%try: + %%from %s import DEBUG, request + + + + Error: {{e.status}} + + + +

    Error: {{e.status}}

    +

    Sorry, the requested URL {{repr(request.url)}} + caused an error:

    +
    {{e.body}}
    + %%if DEBUG and e.exception: +

    Exception:

    +
    {{repr(e.exception)}}
    + %%end + %%if DEBUG and e.traceback: +

    Traceback:

    +
    {{e.traceback}}
    + %%end + + +%%except ImportError: + ImportError: Could not generate the error page. Please add bottle to + the import path. +%%end +""" % __name__ + +#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a +#: request callback, this instance always refers to the *current* request +#: (even on a multi-threaded server). +request = LocalRequest() + +#: A thread-safe instance of :class:`LocalResponse`. It is used to change the +#: HTTP response for the *current* request. +response = LocalResponse() + +#: A thread-safe namespace. Not used by Bottle. +local = threading.local() + +# Initialize app stack (create first empty Bottle app) +# BC: 0.6.4 and needed for run() +app = default_app = AppStack() +app.push() + +#: A virtual package that redirects import statements. +#: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. +ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else + __name__ + ".ext", 'bottle_%s').module + + + +if __name__ == '__main__': + opt, args, parser = _cli_parse(sys.argv) + + def _cli_error(msg): + parser.print_help() + _stderr('\nError: %s\n' % msg) + sys.exit(1) + + if opt.version: + _stdout('Bottle %s\n' % __version__) + sys.exit(0) + if not args: + _cli_error("No application entry point specified.") + + sys.path.insert(0, '.') + sys.modules.setdefault('bottle', sys.modules['__main__']) + + host, port = (opt.bind or 'localhost'), 8080 + if ':' in host and host.rfind(']') < host.rfind(':'): + host, port = host.rsplit(':', 1) + host = host.strip('[]') + + config = ConfigDict() + + for cfile in opt.conf or []: + try: + if cfile.endswith('.json'): + with open(cfile, 'rb') as fp: + config.load_dict(json_loads(fp.read())) + else: + config.load_config(cfile) + except ConfigParserError: + _cli_error(str(_e())) + except IOError: + _cli_error("Unable to read config file %r" % cfile) + except (UnicodeError, TypeError, ValueError): + _cli_error("Unable to parse config file %r: %s" % (cfile, _e())) + + for cval in opt.param or []: + if '=' in cval: + config.update((cval.split('=', 1),)) + else: + config[cval] = True + + run(args[0], + host=host, + port=int(port), + server=opt.server, + reloader=opt.reload, + plugins=opt.plugin, + debug=opt.debug, + config=config) + +# THE END \ No newline at end of file diff --git a/_src/om2pyItem/iMatch/fabfile.py b/_src/om2pyItem/iMatch/fabfile.py new file mode 100644 index 000000000..345aefc7f --- /dev/null +++ b/_src/om2pyItem/iMatch/fabfile.py @@ -0,0 +1,83 @@ +from fabric.api import local, env, run +#import os +env.shell = "/system/bin/sh -c" +# Local path configuration (can be absolute or relative to fabfile) +# +# Remote server configuration +PY = '/data/data/com.hipipal.qpyplus/files/bin/python' +PYQ = 'root@192.168.2.101' +env.hosts= [PYQ] +env.user = "root" +PYQ_ROOT = '/storage/sdcard0/com.hipipal.qpyplus/projects' +PROJ_NAME = 'imatch' +CRT_PROJ = "%(PYQ_ROOT)s/%(PROJ_NAME)s"% locals() +SCP_UP = "scp *.py %(PYQ)s:%(CRT_PROJ)s/ "% locals() +HTML_UP = "scp *.html %(PYQ)s:%(CRT_PROJ)s/ "% locals() + +# Actions define. +#def pushproj(ports='22', name='imatch'): +def pushproj(): + '''scp all .py into Android QPython projects dir + ''' + print SCP_UP + local(SCP_UP) + +def pushhtml(): + '''scp all .html into Android QPython projects dir + ''' + print HTML_UP + local(HTML_UP) + +def qpy_run_it(script="hello.py"): + '''fab qpy_run_it:script=MY.py + ''' + run('pwd') + run('ls -la ./') + #run('export PYTHONHOME=/data/data/com.hipipal.qpyplus/files') + print '%s %s/%s'% (PY, CRT_PROJ, script) + #run('%s %s/%s'% (PY, CRT_PROJ, script)) + run('source %s/qpy_profile && %s %s/%s'% (PYQ_ROOT + , PY + , CRT_PROJ + , script + )) + #run('%s %s/%s'% (PY, CRT_PROJ, script)) + +'''main develop loop usage : +$ fab qpy:script=MY_developing.py +so fab will auto: + - scp all local .py up into mobile QPython projects fold + - and source right sys. env + - and call the 'MY_developing.py' + - so wiil see the script running in mobile desktop ;-) +''' +def qpy(script="hello.py"): + '''main develop tools, auto upload and running in Android + ''' + pushproj() + #qpy_run_it(script) + env() + +def html(script="home.html"): + """upload html file to Android + """ + pushhtml() + env() + +def uname(): + '''print Android sys. info. + ''' + run('uname -a') + +def env(): + '''print Android sys. env + ''' + print 'source %s/qpy_profile'% PYQ_ROOT + run('env') + #run('source %s/qpy_profile && env'% PYQ_ROOT) + +def genenv(script="gen_env.py"): + '''gen qpy need env into: /storage/sdcard0/com.hipipal.qpyplus/projects/qpy_profile + ''' + qpy_run_it(script) + run('ls -la %s'% CRT_PROJ) diff --git a/_src/om2pyItem/iMatch/main.py b/_src/om2pyItem/iMatch/main.py new file mode 100644 index 000000000..a912d75fe --- /dev/null +++ b/_src/om2pyItem/iMatch/main.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +#qpy:2 +#qpy:webapp:iMatch APP +#qpy:fullscreen +#qpy://127.0.0.1:8080/ +''' +Qpython webapp: iMatch +Email: zhangleisuda@gmail.com +Version 1.0 +''' +# 全局引用 +import os + +from bottle import Bottle, ServerAdapter +from bottle import route, run, debug, template, error +from bottle import get, post, request, static_file +from bottle import jinja2_template +from bottle import TEMPLATE_PATH + +### 常量定义 ### +ROOT = os.path.dirname(os.path.abspath(__file__)) +images = {1:None, 2:None, 3:None} +TEMPLATE_PATH.insert(0, ROOT+"/templates/") # template path setting + +""" +由于默认的 bottle 在处理退出时比较难出来, +所以我们引入了自定义的 MyWSGIRefServer, +这能很好实现自我关闭 +""" +### qpython web server ### +class MyWSGIRefServer(ServerAdapter): + server = None + + def run(self, handler): + from wsgiref.simple_server import make_server, WSGIRequestHandler + if self.quiet: + class QuietHandler(WSGIRequestHandler): + def log_request(*args, **kw): pass + self.options['handler_class'] = QuietHandler + self.server = make_server(self.host, self.port, handler, **self.options) + self.server.serve_forever() + + def stop(self): + #sys.stderr.close() + import threading + threading.Thread(target=self.server.shutdown).start() + #self.server.shutdown() + self.server.server_close() + print "# Qpython Imatch WebApp" + +### Build-in routers ### +def __exit(): + global server + server.stop() + +# 健康监测 +def __ping(): + return "ok" + +### imatch main function set ### +# webapp routers +app = Bottle() + +# Route static files such as images or CSS files then you can got it use +@app.route('/static/') +def serve_static(filepath): + img_path = ROOT + "/image" + return static_file(filepath, root=img_path) + +# index route +@app.route('/') +@app.route('/index') +def index(): + return jinja2_template('index.html') + +@app.route('/addimage/') # first + to add image +def add_image(id): + return jinja2_template('upload.html', id_item=id) + +@app.route('/upload/', method='POST') # upload the image +def do_upload_img(id): + category = "image" + upload = request.files.get('upload') + name, ext = os.path.splitext(upload.filename) + if ext not in ('.png', '.jpg', '.jpeg'): + return "File extension not allowed!" + + save_path = ROOT + "/{category}".format(category=category) + if not os.path.exists(save_path): + os.makedirs(save_path) + + file_path = "{path}/{file}".format(path=save_path, file=upload.filename) + if not os.path.exists(file_path): + upload.save(file_path) + + global images + images[id] = upload.filename + return jinja2_template('showimg.html', img_dict=images) + +### Qpy exit and monitor ### +app.route('/__exit', method=['GET', 'HEAD'])(__exit) +app.route('/__ping', method=['GET', 'HEAD'])(__ping) + +### run server ### +try: + server = MyWSGIRefServer(host="127.0.0.1", port="8080") + app.run(server=server, reloader=True, debug=True) +except Exception,ex: + print "Exception: %s" % repr(ex) \ No newline at end of file diff --git a/_src/om2pyItem/iMatch/requirement.txt b/_src/om2pyItem/iMatch/requirement.txt new file mode 100644 index 000000000..34a28501f --- /dev/null +++ b/_src/om2pyItem/iMatch/requirement.txt @@ -0,0 +1,3 @@ +- bottle dev +- Qpython CN 1.2.2 + - pip install Jinja2 \ No newline at end of file diff --git a/_src/om2pyItem/iMatch/templates/index.html b/_src/om2pyItem/iMatch/templates/index.html new file mode 100644 index 000000000..06945eb82 --- /dev/null +++ b/_src/om2pyItem/iMatch/templates/index.html @@ -0,0 +1,6 @@ +{% extends "layout.html" %} +{% block index %} +

    +

    +

    +

    +

    +

    +{% endblock %} \ No newline at end of file diff --git a/_src/om2pyItem/iMatch/templates/layout.html b/_src/om2pyItem/iMatch/templates/layout.html new file mode 100644 index 000000000..d97a224d0 --- /dev/null +++ b/_src/om2pyItem/iMatch/templates/layout.html @@ -0,0 +1,63 @@ + + + + + + + + + + + iMatch + + + + + + + + + +
    + +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/_src/om2pyItem/iMatch/templates/showimg.html b/_src/om2pyItem/iMatch/templates/showimg.html new file mode 100644 index 000000000..8342f625b --- /dev/null +++ b/_src/om2pyItem/iMatch/templates/showimg.html @@ -0,0 +1,10 @@ +{% extends "layout.html" %} +{% block show_img %} + {% for id in range(1, 4) %} + {% if img_dict[id] %} + picture one
    + {% else %} +

    +


    + {% endif %} + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/_src/om2pyItem/iMatch/templates/upload.html b/_src/om2pyItem/iMatch/templates/upload.html new file mode 100644 index 000000000..267800aa0 --- /dev/null +++ b/_src/om2pyItem/iMatch/templates/upload.html @@ -0,0 +1,7 @@ +{% extends "layout.html" %} +{% block upload %} +
    + upload:
    + +
    +{% endblock %} \ No newline at end of file diff --git a/_src/om2pyItem/scaffold/web/__init__.py b/_src/om2pyItem/iMatch/web/__init__.py similarity index 100% rename from _src/om2pyItem/scaffold/web/__init__.py rename to _src/om2pyItem/iMatch/web/__init__.py diff --git a/_src/om2pyItem/iMatch/web/main.py b/_src/om2pyItem/iMatch/web/main.py new file mode 100644 index 000000000..6546a8079 --- /dev/null +++ b/_src/om2pyItem/iMatch/web/main.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +""" +bottle example of upload file +refer to +0-http://bottlepy.org/docs/dev/tutorial.html#file-uploads +1- https://gist.github.com/Arthraim/994641 +2-http://stackoverflow.com/questions/15050064/ \ +how-to-upload-and-save-a-file-using-bottle-framework +""" +import os +from bottle import route, run, error, abort, redirect, response, debug +from bottle import get, post, request, static_file, template + +ROOT = os.path.dirname(os.path.abspath(__file__)) + +@route('/') +@route('/index.html') +def index(): + return 'Go to Hello World page' + +@route('/hello') +def hello(): + return '

    Hello Jeremiah

    ' + +@route('/hello/') +def hello_name(name): + page = request.GET.get('page', '1') + return '

    Hello %s
    (%s)

    ' % (name, page) + +@route('/static/') +def serve_static(filename): + return static_file(filename, root=ROOT) + +@route('/raise_error') +def raise_error(): + abort(404, "error: page not finded") + +@route('/redirect') +def redirect_to_hello(): + redirect('/hello') + +@route('/ajax') +def ajax_response(): + return {'dictionary': 'you will see ajax response rigth? Content-Type will be "application/json"'} + +@error(404) +def error404(error): + return '404 error !!!' + +@get('/upload') # or @route('/upload') +def upload_view(): + return """ +
    + Category: + Select a file: + +
    + """ + +@post('/upload') +def do_upload(): + category = request.forms.get('category') + upload = request.files.get('upload') + name, ext = os.path.splitext(upload.filename) + if ext not in ('.png', '.jpg', '.jpeg'): + return "File extension not allowed!" + + save_path = ROOT + "/{category}".format(category=category) + if not os.path.exists(save_path): + os.makedirs(save_path) + + file_path = "{path}/{file}".format(path=save_path, file=upload.filename) + upload.save(file_path) + return "File successfully saved to '{0}'.".format(save_path) + +if __name__ == '__main__': + run(host="localhost", port=8080, debug=1, reload=1) \ No newline at end of file diff --git a/_src/om2pyItem/scaffold/3party/__init__.py b/_src/om2pyItem/scaffold/3party/__init__.py deleted file mode 100644 index 139597f9c..000000000 --- a/_src/om2pyItem/scaffold/3party/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/_src/om2pyItem/scaffold/__init__.py b/_src/om2pyItem/scaffold/__init__.py deleted file mode 100644 index 139597f9c..000000000 --- a/_src/om2pyItem/scaffold/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/_src/om2pyItem/scaffold/modules/__init__.py b/_src/om2pyItem/scaffold/modules/__init__.py deleted file mode 100644 index fd97c9a80..000000000 --- a/_src/om2pyItem/scaffold/modules/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -import sys -#sys.path.append("..") - - diff --git a/_src/om2pyItem/scaffold/templates/__init__.py b/_src/om2pyItem/scaffold/templates/__init__.py deleted file mode 100644 index fd97c9a80..000000000 --- a/_src/om2pyItem/scaffold/templates/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -import sys -#sys.path.append("..") - - diff --git a/book.json b/book.json index df19e9765..6f6cca560 100644 --- a/book.json +++ b/book.json @@ -1,13 +1,8 @@ -{"title": "开智学院 编程课程 Python 入门班 私人教程模板", - "version": "15.9.18,2020", - "description": "OMOOC.py tutorial for teching", - "author": "OMOOC-support ", - - "plugins": [ - "disqus" - ], +{ + "plugins": ["disqus"], "pluginsConfig": { - "shortName": "Openmindclub" - } -} - + "disqus": { + "shortName": "LeiyuZhang" + } + } +} \ No newline at end of file diff --git a/fabfile.py b/fabfile.py new file mode 100644 index 000000000..1766ad5fc --- /dev/null +++ b/fabfile.py @@ -0,0 +1,17 @@ +# - * - coding:utf-8 -*- + +from fabric.api import local + +# fab post: your-post-name +def post(name): + local("./scripts/newpost %s" % name) + +# fab tag +def tag(): + local("./scripts/generate-tags") + +# please use: fab pu:'discribation' +def pu(discribation): + local("git ad") + local("git ci -m '%s' " % discribation) + local("git pu") \ No newline at end of file