├── .gitattributes ├── .gitignore ├── CHANGES ├── LICENSE ├── MAUNIFEST.in ├── README.rst ├── README_CN.rst ├── db ├── __init__.py ├── _db.py ├── connection.py ├── dialect.py ├── errors.py ├── mysql │ ├── __init__.py │ ├── connection.py │ └── dialect.py ├── pool.py ├── pymysql │ ├── __init__.py │ ├── connection.py │ └── dialect.py └── query │ ├── __init__.py │ ├── base.py │ ├── delete.py │ ├── expr.py │ ├── insert.py │ ├── select.py │ └── update.py ├── samples ├── app.py ├── model.py ├── orm.py └── schema.sql ├── setup.cfg ├── setup.py └── tests ├── dbt.py ├── pymysqlt.py └── test.sql /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | tmp/ 9 | *.tmp 10 | *.bak 11 | *.swp 12 | *~.nib 13 | local.properties 14 | .classpath 15 | .settings/ 16 | .loadpath 17 | 18 | # External tool builders 19 | .externalToolBuilders/ 20 | 21 | # Locally stored "Eclipse launch configurations" 22 | *.launch 23 | 24 | # CDT-specific 25 | .cproject 26 | 27 | # PDT-specific 28 | .buildpath 29 | 30 | 31 | ################# 32 | ## Visual Studio 33 | ################# 34 | 35 | ## Ignore Visual Studio temporary files, build results, and 36 | ## files generated by popular Visual Studio add-ons. 37 | 38 | # User-specific files 39 | *.suo 40 | *.user 41 | *.sln.docstates 42 | 43 | # Build results 44 | 45 | [Dd]ebug/ 46 | [Rr]elease/ 47 | x64/ 48 | build/ 49 | [Oo]bj/ 50 | 51 | # MSTest test Results 52 | [Tt]est[Rr]esult*/ 53 | [Bb]uild[Ll]og.* 54 | 55 | *_i.c 56 | *_p.c 57 | *.ilk 58 | *.meta 59 | *.obj 60 | *.pch 61 | *.pdb 62 | *.pgc 63 | *.pgd 64 | *.rsp 65 | *.sbr 66 | *.tlb 67 | *.tli 68 | *.tlh 69 | *.tmp 70 | *.tmp_proj 71 | *.log 72 | *.vspscc 73 | *.vssscc 74 | .builds 75 | *.pidb 76 | *.log 77 | *.scc 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opensdf 84 | *.sdf 85 | *.cachefile 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | 92 | # Guidance Automation Toolkit 93 | *.gpState 94 | 95 | # ReSharper is a .NET coding add-in 96 | _ReSharper*/ 97 | *.[Rr]e[Ss]harper 98 | 99 | # TeamCity is a build add-in 100 | _TeamCity* 101 | 102 | # DotCover is a Code Coverage Tool 103 | *.dotCover 104 | 105 | # NCrunch 106 | *.ncrunch* 107 | .*crunch*.local.xml 108 | 109 | # Installshield output folder 110 | [Ee]xpress/ 111 | 112 | # DocProject is a documentation generator add-in 113 | DocProject/buildhelp/ 114 | DocProject/Help/*.HxT 115 | DocProject/Help/*.HxC 116 | DocProject/Help/*.hhc 117 | DocProject/Help/*.hhk 118 | DocProject/Help/*.hhp 119 | DocProject/Help/Html2 120 | DocProject/Help/html 121 | 122 | # Click-Once directory 123 | publish/ 124 | 125 | # Publish Web Output 126 | *.Publish.xml 127 | *.pubxml 128 | 129 | # NuGet Packages Directory 130 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 131 | #packages/ 132 | 133 | # Windows Azure Build Output 134 | csx 135 | *.build.csdef 136 | 137 | # Windows Store app package directory 138 | AppPackages/ 139 | 140 | # Others 141 | sql/ 142 | *.Cache 143 | ClientBin/ 144 | [Ss]tyle[Cc]op.* 145 | ~$* 146 | *~ 147 | *.dbmdl 148 | *.[Pp]ublish.xml 149 | *.pfx 150 | *.publishsettings 151 | 152 | # RIA/Silverlight projects 153 | Generated_Code/ 154 | 155 | # Backup & report files from converting an old project file to a newer 156 | # Visual Studio version. Backup files are not needed, because we have git ;-) 157 | _UpgradeReport_Files/ 158 | Backup*/ 159 | UpgradeLog*.XML 160 | UpgradeLog*.htm 161 | 162 | # SQL Server files 163 | App_Data/*.mdf 164 | App_Data/*.ldf 165 | 166 | ############# 167 | ## Windows detritus 168 | ############# 169 | 170 | # Windows image file caches 171 | Thumbs.db 172 | ehthumbs.db 173 | 174 | # Folder config file 175 | Desktop.ini 176 | 177 | # Recycle Bin used on file shares 178 | $RECYCLE.BIN/ 179 | 180 | # Mac crap 181 | .DS_Store 182 | 183 | 184 | ############# 185 | ## Python 186 | ############# 187 | 188 | *.py[co] 189 | 190 | # Packages 191 | *.egg 192 | *.egg-info 193 | dist/ 194 | build/ 195 | eggs/ 196 | parts/ 197 | var/ 198 | sdist/ 199 | develop-eggs/ 200 | .installed.cfg 201 | 202 | # Installer logs 203 | pip-log.txt 204 | 205 | # Unit test / coverage reports 206 | .coverage 207 | .tox 208 | 209 | #Translations 210 | *.mo 211 | 212 | #Mr Developer 213 | .mr.developer.cfg 214 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | dbpy 2 | ============= 3 | 4 | Version 0.1 5 | ----------- 6 | 7 | Released on March 09 2015 8 | 9 | 10 | - silmple and flexible 11 | - graceful and useful sql query builder. 12 | - thread-safe connection pool 13 | - supports read/write master-slave mode 14 | - supports transaction 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | le GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | -------------------------------------------------------------------------------- /MAUNIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES 2 | include LICENSE 3 | include MANIFEST.in 4 | include README.rst 5 | recursive-include tests * -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | dbpy 2 | ##### 3 | 4 | 5 | 6 | dbpy is database abstration layer wrote by python. The design is inspired by `webpy db `_ and `drupal database `_ . If like the simple db abstration layer like ``tornado db`` or ``webpy db``, it is worth to try. 7 | 8 | 9 | `中文|chinese `_ 10 | 11 | changes 12 | ========== 13 | 14 | #. Add pymysql adapter 15 | 16 | 17 | 18 | Install the extension with the following command:: 19 | 20 | $ easy_install pymysql 21 | 22 | or alternatively if you have pip installed:: 23 | 24 | 25 | $ pip install pymysql 26 | 27 | Featues 28 | ================ 29 | 30 | #. silmple and flexible 31 | #. graceful and useful sql query builder. 32 | #. thread-safe connection pool 33 | #. supports read/write master-slave mode 34 | #. supports transaction 35 | 36 | The Projects use dbpy 37 | ====================== 38 | 39 | 40 | `Lilac (Distributed Scheduler Task System) `_ 41 | 42 | .. contents:: 43 | :depth: 4 44 | 45 | 46 | 47 | 48 | Install 49 | ============== 50 | 51 | Install the extension with the following command:: 52 | 53 | $ easy_install dbpy 54 | 55 | or alternatively if you have pip installed:: 56 | 57 | 58 | $ pip install dbpy 59 | 60 | 61 | or clone it form github then run the command in shell: 62 | 63 | .. code-block:: bash 64 | 65 | cd db # the path to the project 66 | python setup.py install 67 | 68 | Development 69 | =========== 70 | 71 | Fork or download it, then run: 72 | 73 | .. code-block:: bash 74 | 75 | cd db # the path to the project 76 | python setup.py develop 77 | 78 | 79 | 80 | Compatibility 81 | ============= 82 | 83 | Built and tested under Python 2.7+ 84 | 85 | 86 | DB API 87 | ======== 88 | 89 | 90 | Have a look: 91 | 92 | .. code-block:: python 93 | 94 | config = { 95 | 'passwd': 'test', 96 | 'user': 'test', 97 | 'host': 'localhost', 98 | 'db': 'test', 99 | 'max_idle' : 5*60 100 | } 101 | 102 | db.setup(config, minconn=5, maxconn=10, 103 | adapter='mysql', key='default', slave=False) 104 | 105 | db.execute('show tables') 106 | 107 | 108 | 109 | setup 110 | --------- 111 | 112 | :config: the connection basic config, the all of arguements of MySQLDB#connect is acceptable。 the ``max_idle`` is the connect timeout setting that is used to reconnection when connection is timeout, default is 10 seconds. 113 | :minconn: the minimum connections for the connection pool, default is 5. 114 | :maxconn: the maximum connections for the connection pool, default is 10. 115 | :adapter: the database driver adapter name, currently supports mysql (MySQLdb, pymysql) only. 116 | :key: the database idenfify for database, default database is "default" 117 | :slave: if set to true, the database will be register as a slave database. make sure you setup a master firstly. 118 | 119 | 120 | .. code-block:: python 121 | 122 | config = { 123 | 'passwd': 'test', 124 | 'user': 'test', 125 | 'host': 'localhost', 126 | 'db': 'test', 127 | 'max_idle' : 5*60 128 | } 129 | 130 | db.setup(config, key='test') 131 | config['host'] = 'test.slave' 132 | # set a slave, and now the master can only to write 133 | db.setup(config, key='test', slave=True) 134 | 135 | config['host'] = 'test.slave2' 136 | # add more slave for 'test' 137 | db.setup(config, key='test', slave=True) 138 | 139 | 140 | config['host'] = 'host2' 141 | config['db'] = 'social' 142 | # set another database 143 | db.setup(config, key='social', slave=True) 144 | 145 | query 146 | ------- 147 | 148 | 149 | 150 | query api is used for reading database operation, like select..., show tables, if you wanna update your database please use execute api. 151 | 152 | query(sql, args=None, many=None, as_dict=False, key='default'): 153 | 154 | :sql: the raw sql 155 | :args: the args for sql arguement to prepare execute. 156 | :many: when set to a greater zero integer, it will use fetchmany then yield return a generator, otherwise a list. 157 | :as_dict: when set to true, query api will return the database result as dict row, otherwise tuple row. 158 | :key: the idenfify of database. 159 | 160 | .. code-block:: python 161 | 162 | print db.query('SELECT 1') 163 | # > ((1L,),) 164 | 165 | # use social db 166 | print db.query('SELECT 1', key='social') 167 | # > ((1L,),) 168 | 169 | print db.query('SELECT * FROM users WHERE uid=%s and name=%s', (1, 'user_1')) 170 | # > ((1L, u'user_1'),) 171 | 172 | # Wanna return dict row 173 | print db.query('SELECT * FROM users WHERE uid=%s and name=%s', 174 | (1, 'user_1'), as_dict=True) 175 | # > ({'uid': 1L, 'name': u'user_1'},) 176 | 177 | # Use fetchmany(many) then yeild, Return generator 178 | res = db.query('SELECT * FROM users WHERE uid=%s and name=%s', 179 | (1, 'user_1'), many=5, as_dict=True) 180 | print res 181 | print res.next() 182 | # > 183 | # > {'uid': 1L, 'name': u'user_1'} 184 | 185 | 186 | execute 187 | -------- 188 | 189 | the api is used for writing database operation, like insert, update, delete.. if you wanna read query your database please use query api. 190 | 191 | execute(sql, args=None, key='default'): 192 | 193 | 194 | :sql: the raw sql 195 | :args: the args for sql arguement to prepare execute. 196 | :key: the idenfify of database. 197 | 198 | 199 | Return:: 200 | 201 | it returns last_insert_id when sql is insert statement, otherwise rowcount 202 | 203 | .. code-block:: python 204 | 205 | db.execute('DROP TABLE IF EXISTS `users`') 206 | db.execute("""CREATE TABLE `users` ( 207 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT, 208 | `name` varchar(20) NOT NULL, 209 | PRIMARY KEY (`uid`))""") 210 | 211 | # when inset mutil-values,the api will call executemany 212 | db.execute('INSERT INTO users VALUES(%s, %s)', [(10, 'execute_test'), (9, 'execute_test')]) 213 | # > 9 214 | db.execute('DELETE FROM users WHERE name=%s', ('execute_test',)) 215 | # > 2 216 | 217 | 218 | # use social db 219 | db.execute('delete from events where created_at<%s', (expired, ), key='social') 220 | # > 10 221 | 222 | select 223 | ----------- 224 | 225 | the api is used for select sql database query. 226 | 227 | select(table, key='default'): 228 | 229 | :table: the table name 230 | :key: the idenfify of database 231 | 232 | select all 233 | ~~~~~~~~~~~~~~~~ 234 | 235 | .. code-block:: python 236 | 237 | db.select('users') 238 | # > SELECT * FROM `users` 239 | 240 | specific columns 241 | ~~~~~~~~~~~~~~~~~ 242 | 243 | .. code-block:: python 244 | 245 | db.select('users').fields('uid', 'name') 246 | # > SELECT `uid`, `name` FROM `users` 247 | 248 | 249 | execute 250 | ~~~~~~~~~~~~~~~~ 251 | 252 | when you already build your sql, try execute api to fetch your database result. 253 | 254 | execute(many=None, as_dict=False): 255 | 256 | :many: when set to a greater zero integer, it will use fetchmany then yield return a generator, otherwise a list. 257 | :as_dict: when set to true, query api will return the database result as dict row, otherwise tuple row. 258 | 259 | .. code-block:: python 260 | 261 | q = db.select('users').fields('uid', 'name') 262 | res = q.execute() 263 | print res 264 | # > ((1L, u'user_1'), (2L, u'user_2'), (3L, u'user_3'), (4L, u'user_4'), (5L, None)) 265 | 266 | res = q.execute(many=2, as_dict=True) 267 | print res 268 | print res.next() 269 | # > 270 | # > {'uid': 1L, 'name': u'user_1'} 271 | 272 | 273 | Condition 274 | ~~~~~~~~~~~ 275 | 276 | It is time to try more complex select query. 277 | 278 | condition(field, value=None, operator=None): 279 | 280 | :field: the field of table 281 | :value: the value of field, defaul is None ("field is null") 282 | :operator: the where operator like BETWEEN, IN, NOT IN, EXISTS, NOT EXISTS, IS NULL, IS NOT NULL, LIKE, NOT LIKE, =, <, >, >=, <=, <> and so on. 283 | 284 | 285 | simple 286 | ^^^^^^^^^^^^^^^^ 287 | 288 | .. code-block:: python 289 | 290 | db.select('users').condition('uid', 1) # condition('uid', 1, '=') 291 | # > SELECT * FROM `users` 292 | # > WHERE `uid` = %s 293 | 294 | 295 | in 296 | ^^^^^^^^^^^^^^^^ 297 | 298 | .. code-block:: python 299 | 300 | 301 | db.select('users').condition('uid', (1, 3)) # condition('uid', [1, 3]) 一样 302 | # > SELECT * FROM `users` 303 | # > WHERE `uid` IN (%s, %s) 304 | 305 | between 306 | ^^^^^^^^^^^^^^^^ 307 | 308 | .. code-block:: python 309 | 310 | db.select('users').condition('uid', (1, 3), 'between') 311 | # > SELECT * FROM `users` 312 | # > WHERE `uid` BETWEEN %s AND %s 313 | 314 | 315 | multi condition 316 | ^^^^^^^^^^^^^^^^^^^^^^^^ 317 | 318 | .. code-block:: python 319 | 320 | db.select('users').condition('uid', 1).condition('name', 'blabla') 321 | # > SELECT * FROM `users` 322 | # > WHERE `uid` = %s AND `name` = %s 323 | 324 | or condition 325 | ^^^^^^^^^^^^^^ 326 | 327 | .. code-block:: python 328 | 329 | or_cond = db.or_().condition('uid', 1).condition('name', 'blabla') 330 | db.select('users').condition(or_cond).condition('uid', 1, '<>') 331 | # > SELECT * FROM `users` 332 | # > WHERE ( `uid` = %s OR `name` = %s ) AND `uid` <> %s 333 | 334 | 335 | 336 | order by 337 | ~~~~~~~~~ 338 | 339 | .. code-block:: python 340 | 341 | db.select('users').order_by('name') 342 | # > SELECT * FROM `users` 343 | # > ORDER BY `name` 344 | 345 | db.select('users').order_by('name', 'DESC') 346 | # > SELECT * FROM `users` 347 | # > ORDER BY `name` DESC 348 | 349 | db.select('users').order_by('name', 'DESC').order_by('uid') 350 | # > SELECT * FROM `users` 351 | # > ORDER BY `name` DESC, `uid` 352 | 353 | 354 | 355 | distinct 356 | ~~~~~~~~~ 357 | 358 | .. code-block:: python 359 | 360 | db.select('users').distinct().condition('uid', 1) 361 | # > SELECT DISTINCT * FROM `users` 362 | # > WHERE `uid` = %s 363 | 364 | db.select('users').fields('uid', 'name').distinct().condition('uid', 1) 365 | # > SELECT DISTINCT `uid`, `name` FROM `users` 366 | # > WHERE `uid` = %s 367 | 368 | 369 | group by 370 | ~~~~~~~~~ 371 | 372 | .. code-block:: python 373 | 374 | db.select('users').group_by('name', 'uid') 375 | # > SELECT * FROM `users` 376 | # > GROUP BY `name`, `uid` 377 | 378 | 379 | limit and offset 380 | ~~~~~~~~~~~~~~~~~ 381 | 382 | .. code-block:: python 383 | 384 | db.select('users').limit(2).offset(5) 385 | # > SELECT * FROM `users` 386 | # > LIMIT 2 OFFSET 5 387 | 388 | null condition 389 | ~~~~~~~~~~~~~~~ 390 | 391 | .. code-block:: python 392 | 393 | db.select('users').is_null('name').condition('uid', 5) 394 | # > SELECT * FROM `users` 395 | # > WHERE `name` IS NULL AND `uid` = %s 396 | 397 | db.select('users').is_not_null('name').condition('uid', 5) 398 | # > SELECT * FROM `users` 399 | # > WHERE `name` IS NOT NULL AND `uid` = %s 400 | 401 | db.select('users').condition('name', None) 402 | # > SELECT * FROM `users` 403 | # > WHERE `name` IS NULL 404 | 405 | 406 | complex conditions 407 | ------------------- 408 | 409 | using db.and_(), db.or_(), we can build complex where conditions: 410 | 411 | .. code-block:: python 412 | 413 | or_cond = db.or_().condition('field1', 1).condition('field2', 'blabla') 414 | and_cond = db.and_().condition('field3', 'what').condition('field4', 'then?') 415 | print db.select('table_name').condition(or_cond).condition(and_cond) 416 | 417 | # > SELECT * FROM `table_name` 418 | # > WHERE ( `field1` = %s OR `field2` = %s ) AND ( `field3` = %s AND `field4` = %s ) 419 | 420 | expr 421 | ------------ 422 | 423 | if you wanna use the aggregate functions like sum, count, please use ``erpr`` : 424 | 425 | .. code-block:: python 426 | 427 | from db import expr 428 | 429 | db.select('users').fields(expr('count(*)')) 430 | # > SELECT count(*) FROM `users` 431 | 432 | db.select('users').fields(expr('count(uid)', 'total')) 433 | # > SELECT count(uid) AS `total` FROM `users` 434 | 435 | 436 | 437 | insert 438 | ----------- 439 | 440 | The ``insert`` api is used for building insert into sql statement. 441 | 442 | insert(table, key='default'): 443 | 444 | :table: the table name 445 | :key: the idenfify of database 446 | 447 | .. code-block:: python 448 | 449 | q = db.insert('users').values((10, 'test_insert')) 450 | # > INSERT INTO `users` VALUES(%s, %s) 451 | print q._values 452 | # > [(10, 'test_insert')] 453 | 454 | 455 | q = db.insert('users').fields('name').values({'name': 'insert_1'}).values(('insert_2',)) 456 | # > INSERT INTO `users` (`name`) VALUES(%s) 457 | print q._values 458 | # > [('insert_1',), ('insert_2',)] 459 | 460 | 461 | When you use ``execute`` api to get result, it will reutrn the ``last insert id``: 462 | 463 | .. code-block:: python 464 | 465 | 466 | print q.execute() 467 | # > 2 468 | 469 | 470 | 471 | update 472 | ----------- 473 | 474 | The ``update`` api is used for building update sql statement. 475 | 476 | update(table, key='default'): 477 | 478 | :table: the table name 479 | :key: the idenfify of database 480 | 481 | 482 | mset and set: 483 | 484 | :mset: the value must be dict tpye, that sets mutil-fileds at once time. 485 | :set(column, value): set one field one time. 486 | 487 | the where conditions please see `select`_ for more information. 488 | 489 | 490 | .. code-block:: python 491 | 492 | 493 | db.update('users').mset({'name':None, 'uid' : 12}).condition('name','user_1') 494 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s 495 | 496 | q = (db.update('users').set('name', 'update_test').set('uid', 12) 497 | .condition('name', 'user_2').condition('uid', 2)) # .execute() 498 | print q.to_sql() 499 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s AND `uid` = %s 500 | 501 | 502 | 503 | When you use ``execute`` api to get result, it will reutrn the ``rowcount``: 504 | 505 | 506 | .. code-block:: python 507 | 508 | 509 | print q.execute() 510 | # > 2 511 | 512 | limit 513 | ~~~~~~~~~ 514 | 515 | 516 | 517 | You can use limit api to lim the quantity of update. 518 | 519 | 520 | .. code-block:: python 521 | 522 | db.update('users').mset({'name':None, 'uid' : 12}).condition('name','user_1').limit(5) 523 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s LIMIT 5 524 | 525 | delete 526 | ----------- 527 | 528 | 529 | The ``delete`` api is used for building DELETE FROM sql statement. 530 | 531 | delete(table, key='default'): 532 | 533 | :table: the table name 534 | :key: the idenfify of database 535 | 536 | the where conditions please see `select`_ for more information. 537 | 538 | .. code-block:: python 539 | 540 | db.delete('users').condition('name','user_1') 541 | # > DELETE FROM `users` WHERE `name` = %s 542 | 543 | When you use ``execute`` api to get result, it will reutrn the ``rowcount``: 544 | 545 | .. code-block:: python 546 | 547 | 548 | print q.execute() 549 | # > 2 550 | 551 | 552 | to_sql and str 553 | --------------------- 554 | 555 | you can use to_sql or __str__ method to the objects of ``select``, ``insert``, ``update``, ``delete`` to print the sql you build. 556 | 557 | 558 | .. code-block:: python 559 | 560 | 561 | q = (db.update('users').set('name', 'update_test').set('uid', 12) 562 | .condition('name', 'user_2').condition('uid', 2)) 563 | print q.to_sql() 564 | print q 565 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s AND `uid` = %s 566 | 567 | 568 | transaction 569 | ------------ 570 | 571 | transaction(table, key='default'): 572 | 573 | :table: the table name 574 | :key: the idenfify of database 575 | 576 | 577 | The simple transaction done all or do nothing, you cann't set savepoint. 578 | 579 | 580 | 581 | .. code-block:: python 582 | 583 | 584 | # with context 585 | with db.transaction() as t: 586 | t.delete('users').condition('uid', 1).execute() 587 | (t.update('users').mset({'name':None, 'uid' : 12}) 588 | .condition('name','user_1').execute()) 589 | 590 | 591 | # the normal way 592 | t = db.transaction() 593 | t.begin() 594 | t.delete('users').condition('uid', 1).execute() 595 | (t.update('users').mset({'name':None, 'uid' : 12}) 596 | .condition('name','user_1').execute()) 597 | 598 | #if failed will rollback 599 | t.commit() 600 | 601 | .. note:: when uses begin must be combine with commit,otherwise the connection will not return connection pool.suggets to use ``with context`` 602 | 603 | 604 | simple orm 605 | ----------- 606 | 607 | the orm demo `samples `_ 608 | 609 | .. code-block:: python 610 | 611 | import model 612 | from orm import Backend 613 | import db 614 | 615 | db.setup({ 'host': 'localhost', 'user': 'test', 'passwd': 'test', 'db': 'blog'}) 616 | 617 | 618 | user = Backend('user').find_by_username('username') 619 | if user and user.check('password'): 620 | print 'auth' 621 | 622 | user = model.User('username', 'email', 'real_name', 'password', 623 | 'bio', 'status', 'role') 624 | if Backend('user').create(user): 625 | print 'fine' 626 | 627 | user = Backend('user').find(12) 628 | user.real_name = 'blablabla....' 629 | if Backend('user').save(user): 630 | print 'user saved' 631 | 632 | if Backend('user').delete(user): 633 | print 'delete user failed' 634 | 635 | 636 | post = model.Post('title', 'slug', 'description', 'html', 'css', 'js', 637 | 'category', 'status', 'comments', 'author') 638 | if not Backend('post').create(post): 639 | print 'created failed' 640 | 641 | Future 642 | -------- 643 | 644 | 645 | Personal idea: 646 | 647 | #. add ``join`` for select api 648 | #. add a schema class for creating or changing table. 649 | #. add some api for mysql individual sql like ``replace`` or ``duplicate update`` 650 | #. improve connection pool. 651 | 652 | 653 | LICENSE 654 | ======= 655 | 656 | Copyright (C) 2014-2015 Thomas Huang 657 | 658 | This program is free software: you can redistribute it and/or modify 659 | it under the terms of the GNU General Public License as published by 660 | the Free Software Foundation, version 2 of the License. 661 | 662 | This program is distributed in the hope that it will be useful, 663 | but WITHOUT ANY WARRANTY; without even the implied warranty of 664 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 665 | GNU General Public License for more details. 666 | 667 | You should have received a copy of the GNU General Public License 668 | along with this program. If not, see . 669 | 670 | -------------------------------------------------------------------------------- /README_CN.rst: -------------------------------------------------------------------------------- 1 | dbpy 2 | ##### 3 | 4 | 5 | dbpy是一个python写的数据库CURD人性化api库。借鉴了 `webpy db `_ 和 `drupal database `_ 的设计。 如果喜欢 tornado db 或者 webpy db这类轻巧的db库,或者想发挥原生SQL优势,那么值得一试。 6 | 7 | 8 | Featues 9 | ================ 10 | 11 | #. 灵活简单 12 | #. 天马行空的SQL构建语法糖 13 | #. 线程安全的连接池 14 | #. 支持读写分离(当前限定只能是一主多副模式) 15 | #. 支持简单事务 16 | 17 | 18 | The Projects use dbpy 19 | ====================== 20 | 21 | 22 | `Lilac (Distributed Scheduler Task System) `_ 23 | 24 | .. contents:: 25 | :depth: 4 26 | 27 | 28 | 29 | Install 30 | ============== 31 | 32 | 33 | 34 | 使用easy_install安装:: 35 | 36 | $ easy_install dbpy 37 | 38 | 或者使用pip下载安装:: 39 | 40 | 41 | $ pip install dbpy 42 | 43 | 44 | 从github上fork下来,终端执行下面命令: 45 | 46 | .. code-block:: bash 47 | 48 | cd dbpy # the path to the project 49 | python setup.py install 50 | 51 | .. note:: 安装前先安装 ``MySQLdb`` (``MySQL-python``) 依赖python库 52 | 53 | 54 | Development 55 | =========== 56 | 57 | 下载后终端执行: 58 | 59 | .. code-block:: bash 60 | 61 | cd dbpy # the path to the project 62 | python setup.py develop 63 | 64 | 65 | Compatibility 66 | ============= 67 | 68 | 在 Python 2.7+ 测试开发 69 | 70 | DB API 71 | ======== 72 | 73 | 先提醒下模块使用单例模式。所以api相对比较好使。 74 | 75 | 76 | .. code-block:: python 77 | 78 | config = { 79 | 'passwd': 'test', 80 | 'user': 'test', 81 | 'host': 'localhost', 82 | 'db': 'test', 83 | 'max_idle' : 5*60 84 | } 85 | 86 | db.setup(config, minconn=5, maxconn=10, 87 | adapter='mysql', key='default', slave=False) 88 | 89 | 90 | 91 | setup 92 | --------- 93 | 94 | :config: 是数据库连接参数,可以传入MySQLDB#connect接口中所有的可选参数。 其中``max_idle`` 相对是mysql服务端 connect_timeout配置,默认10秒。 95 | :minconn: 为当前数据库连接池保持最小连接池,默认为5 96 | :maxconn: 为当前数据库连接池最大连接池,默认为10 97 | :adapter: 为适配器名,当前支持 mysql和pymsql 98 | :key: 是数据库的标识符,默认为 default 99 | :slave: 如果为true那么当前的数据库将会注册为读数据库。如果你没有做读写分离,只有一个数据库用来读写,那么setup一次就好,这样就可以读写。 100 | 101 | .. code-block:: python 102 | 103 | config = { 104 | 'passwd': 'test', 105 | 'user': 'test', 106 | 'host': 'localhost', 107 | 'db': 'test', 108 | 'max_idle' : 5*60 109 | } 110 | 111 | db.setup(config, key='test') 112 | config['host'] = 'test.slave' 113 | # 这次setup将会把key标记为仅可写,就是在后面用api时,制定到当前key的数据库会做数据分离 114 | db.setup(config, key='test', slave=True) 115 | 116 | config['host'] = 'test.slave2' 117 | # 再加入一个slave数据库 118 | db.setup(config, key='test', slave=True) 119 | 120 | 121 | config['host'] = 'host2' 122 | config['db'] = 'social' 123 | # 再加入一个数据库 124 | db.setup(config, key='social', slave=True) 125 | 126 | query 127 | ------- 128 | 129 | query用于raw sql的查询语言。如果有更新数据请用execute. 130 | 131 | query(sql, args=None, many=None, as_dict=False, key='default'): 132 | 133 | :sql: mysql的格式化raw sql 134 | :args: 可以为元组和list,是sql格式化预处理的输入 135 | :many: 如果指定为大于零的整数将会使用fetchmany语句,并返回对象将会是迭代器.否则api调用fetchall返回结果. 136 | :as_dict: 如果为 true将会返回字典行,否则返回元组行。 137 | :key: 用于指定使用那个数据库。 138 | 139 | 140 | .. code-block:: python 141 | 142 | print db.query('SELECT 1') 143 | # > ((1L,),) 144 | 145 | # use social db 146 | print db.query('SELECT 1', key='social') 147 | # > ((1L,),) 148 | 149 | print db.query('SELECT * FROM users WHERE uid=%s and name=%s', (1, 'user_1')) 150 | # > ((1L, u'user_1'),) 151 | 152 | # Wanna return dict row 153 | print db.query('SELECT * FROM users WHERE uid=%s and name=%s', 154 | (1, 'user_1'), as_dict=True) 155 | # > ({'uid': 1L, 'name': u'user_1'},) 156 | 157 | # Use fetchmany(many) then yeild, Return generator 158 | res = db.query('SELECT * FROM users WHERE uid=%s and name=%s', 159 | (1, 'user_1'), many=5, as_dict=True) 160 | print res 161 | print res.next() 162 | # > 163 | # > {'uid': 1L, 'name': u'user_1'} 164 | 165 | 166 | execute 167 | -------- 168 | 169 | execute用于raw sql的更新语言。 170 | execute(sql, args=None, key='default'): 171 | 172 | 173 | :sql: mysql的格式化raw sql 174 | :args: 可以为元组和list,是sql格式化预处理的输入.如下面例子insert语句values有多个插入时,调用 ``executemany`` 175 | :key: 用于指定使用那个数据库。 176 | 177 | 返回规范:: 178 | 179 | 对于insert 将会返回 last_insert_id, 其他更新语句返回rowcount 180 | 181 | .. code-block:: python 182 | 183 | db.execute('DROP TABLE IF EXISTS `users`') 184 | db.execute("""CREATE TABLE `users` ( 185 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT, 186 | `name` varchar(20) NOT NULL, 187 | PRIMARY KEY (`uid`))""") 188 | 189 | # insert语句插入多个value,注意这样写将会调用executemany,你懂的,就是封装了多条execute的玩意 190 | db.execute('INSERT INTO users VALUES(%s, %s)', [(10, 'execute_test'), (9, 'execute_test')]) 191 | # > 9 192 | db.execute('DELETE FROM users WHERE name=%s', ('execute_test',)) 193 | # > 2 194 | 195 | 196 | # use social db 197 | db.execute('delete from events where created_at<%s', (expired, ), key='social') 198 | # > 10 199 | 200 | select 201 | ----------- 202 | 203 | select用于构建select 查询语言。 204 | 205 | select(table, key='default'): 206 | 207 | :table: 选定表 208 | :key: 用于指定使用那个数据库。 209 | 210 | select all 211 | ~~~~~~~~~~~~~~~~ 212 | 213 | .. code-block:: python 214 | 215 | db.select('users') 216 | # > SELECT * FROM `users` 217 | 218 | specific columns 219 | ~~~~~~~~~~~~~~~~~ 220 | 221 | .. code-block:: python 222 | 223 | db.select('users').fields('uid', 'name') 224 | # > SELECT `uid`, `name` FROM `users` 225 | 226 | 227 | execute 228 | ~~~~~~~~~~~~~~~~ 229 | 230 | 在构建好查询条语句后使用execute api可以返回结果。 231 | 232 | execute(many=None, as_dict=False): 233 | 234 | :many: 如果指定为大于零的整数将会使用fetchmany语句,并返回对象将会是迭代器.否则api调用fetchall返回结果. 235 | :as_dict: 如果为 true将会返回字典行,否则返回元组行。 236 | 237 | .. code-block:: python 238 | 239 | q = db.select('users').fields('uid', 'name') 240 | res = q.execute() 241 | print res 242 | # > ((1L, u'user_1'), (2L, u'user_2'), (3L, u'user_3'), (4L, u'user_4'), (5L, None)) 243 | 244 | res = q.execute(many=2, as_dict=True) 245 | print res 246 | print res.next() 247 | # > 248 | # > {'uid': 1L, 'name': u'user_1'} 249 | 250 | 251 | Condition 252 | ~~~~~~~~~~~ 253 | 254 | 上面已经学会如何做简单的查询,那么如何组件条件查询。这里将会重点讲述condition方法如何构建各种查询条件。 255 | 256 | condition(field, value=None, operator=None): 257 | 258 | :field: 是条件限制的表字段 259 | :value: 是字段的条件值, 如果炸路额, operator都不指定就是 "field is null" 260 | :operator: 默认可能是等于操作符号, 可选的操作符号有 BETWEEN, IN, NOT IN, EXISTS, NOT EXISTS, IS NULL, IS NOT NULL, LIKE, NOT LIKE, =, <, >, >=, <=, <>等 261 | 262 | 263 | 在所有的select,update, delete查询中多个默认的condition将会是and条件组合。 264 | 265 | simple 266 | ^^^^^^^^^^^^^^^^ 267 | 268 | .. code-block:: python 269 | 270 | db.select('users').condition('uid', 1) # condition('uid', 1, '=') 271 | # > SELECT * FROM `users` 272 | # > WHERE `uid` = %s 273 | 274 | 275 | in 276 | ^^^^^^^^^^^^^^^^ 277 | 278 | .. code-block:: python 279 | 280 | 281 | db.select('users').condition('uid', (1, 3)) # condition('uid', [1, 3]) 一样 282 | # > SELECT * FROM `users` 283 | # > WHERE `uid` IN (%s, %s) 284 | 285 | between 286 | ^^^^^^^^^^^^^^^^ 287 | 288 | .. code-block:: python 289 | 290 | db.select('users').condition('uid', (1, 3), 'between') 291 | # > SELECT * FROM `users` 292 | # > WHERE `uid` BETWEEN %s AND %s 293 | 294 | 295 | multi condition 296 | ^^^^^^^^^^^^^^^^^^^^^^^^ 297 | 298 | .. code-block:: python 299 | 300 | db.select('users').condition('uid', 1).condition('name', 'blabla') 301 | # > SELECT * FROM `users` 302 | # > WHERE `uid` = %s AND `name` = %s 303 | 304 | or condition 305 | ^^^^^^^^^^^^^^ 306 | 307 | .. code-block:: python 308 | 309 | or_cond = db.or_().condition('uid', 1).condition('name', 'blabla') 310 | db.select('users').condition(or_cond).condition('uid', 1, '<>') 311 | # > SELECT * FROM `users` 312 | # > WHERE ( `uid` = %s OR `name` = %s ) AND `uid` <> %s 313 | 314 | 315 | 316 | order by 317 | ~~~~~~~~~ 318 | 319 | .. code-block:: python 320 | 321 | db.select('users').order_by('name') 322 | # > SELECT * FROM `users` 323 | # > ORDER BY `name` 324 | 325 | db.select('users').order_by('name', 'DESC') 326 | # > SELECT * FROM `users` 327 | # > ORDER BY `name` DESC 328 | 329 | db.select('users').order_by('name', 'DESC').order_by('uid') 330 | # > SELECT * FROM `users` 331 | # > ORDER BY `name` DESC, `uid` 332 | 333 | 334 | 335 | distinct 336 | ~~~~~~~~~ 337 | 338 | .. code-block:: python 339 | 340 | db.select('users').distinct().condition('uid', 1) 341 | # > SELECT DISTINCT * FROM `users` 342 | # > WHERE `uid` = %s 343 | 344 | db.select('users').fields('uid', 'name').distinct().condition('uid', 1) 345 | # > SELECT DISTINCT `uid`, `name` FROM `users` 346 | # > WHERE `uid` = %s 347 | 348 | 349 | group by 350 | ~~~~~~~~~ 351 | 352 | .. code-block:: python 353 | 354 | db.select('users').group_by('name', 'uid') 355 | # > SELECT * FROM `users` 356 | # > GROUP BY `name`, `uid` 357 | 358 | 359 | limit and offset 360 | ~~~~~~~~~~~~~~~~~ 361 | 362 | .. code-block:: python 363 | 364 | db.select('users').limit(2).offset(5) 365 | # > SELECT * FROM `users` 366 | # > LIMIT 2 OFFSET 5 367 | 368 | null condition 369 | ~~~~~~~~~~~~~~~ 370 | 371 | .. code-block:: python 372 | 373 | db.select('users').is_null('name').condition('uid', 5) 374 | # > SELECT * FROM `users` 375 | # > WHERE `name` IS NULL AND `uid` = %s 376 | 377 | db.select('users').is_not_null('name').condition('uid', 5) 378 | # > SELECT * FROM `users` 379 | # > WHERE `name` IS NOT NULL AND `uid` = %s 380 | 381 | db.select('users').condition('name', None) 382 | # > SELECT * FROM `users` 383 | # > WHERE `name` IS NULL 384 | 385 | 386 | complex conditions 387 | ------------------- 388 | 389 | 使用 db.and_(), db.or_() 可以构建and或or粘合的条件组合。 390 | 391 | .. code-block:: python 392 | 393 | or_cond = db.or_().condition('field1', 1).condition('field2', 'blabla') 394 | and_cond = db.and_().condition('field3', 'what').condition('field4', 'then?') 395 | print db.select('table_name').condition(or_cond).condition(and_cond) 396 | 397 | # > SELECT * FROM `table_name` 398 | # > WHERE ( `field1` = %s OR `field2` = %s ) AND ( `field3` = %s AND `field4` = %s ) 399 | 400 | expr 401 | ------------ 402 | 403 | 如果你需要使用 count sum之类的集聚函数,那么使用 Expr构建字段吧。 404 | 405 | .. code-block:: python 406 | 407 | from db import expr 408 | 409 | db.select('users').fields(expr('count(*)')) 410 | # > SELECT count(*) FROM `users` 411 | 412 | db.select('users').fields(expr('count(uid)', 'total')) 413 | # > SELECT count(uid) AS `total` FROM `users` 414 | 415 | 416 | 417 | insert 418 | ----------- 419 | 420 | insert用于构建insert into的sql语句。 421 | 422 | insert(table, key='default'): 423 | 424 | :table: 选定表 425 | :key: 用于指定使用那个数据库。 426 | 427 | 428 | .. code-block:: python 429 | 430 | q = db.insert('users').values((10, 'test_insert')) 431 | # > INSERT INTO `users` VALUES(%s, %s) 432 | print q._values 433 | # > [(10, 'test_insert')] 434 | 435 | 436 | q = db.insert('users').fields('name').values({'name': 'insert_1'}).values(('insert_2',)) 437 | # > INSERT INTO `users` (`name`) VALUES(%s) 438 | print q._values 439 | # > [('insert_1',), ('insert_2',)] 440 | 441 | 构建好执行execute会执行数据库插入,execute返回的是last insert id: 442 | 443 | .. code-block:: python 444 | 445 | 446 | print q.execute() 447 | # > 2 448 | 449 | 450 | 451 | update 452 | ----------- 453 | 454 | update用于构建update的sql语句 455 | 456 | update(table, key='default'): 457 | 458 | :table: 选定表 459 | :key: 用于指定使用那个数据库。 460 | 461 | update 主要可用的方法是mset和set, mset: 462 | 463 | :mset: 传入的是字典,用于一次set多个表属性 464 | :set(column, value): 只能设置一个属性,可以多次使用 465 | 466 | 构建条件codition前面已经讲述了。请参考 `select`_ 467 | 468 | 469 | .. code-block:: python 470 | 471 | 472 | db.update('users').mset({'name':None, 'uid' : 12}).condition('name','user_1') 473 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s 474 | 475 | q = (db.update('users').set('name', 'update_test').set('uid', 12) 476 | .condition('name', 'user_2').condition('uid', 2)) # .execute() 477 | print q.to_sql() 478 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s AND `uid` = %s 479 | 480 | 481 | 构建好执行execute会执行数据库插入,execute返回的是更新的 rowcount: 482 | 483 | .. code-block:: python 484 | 485 | 486 | print q.execute() 487 | # > 2 488 | 489 | limit 490 | ~~~~~~~~~ 491 | 492 | 因为你可能希望限制更新几条。那么可以使用limit 493 | 494 | 495 | .. code-block:: python 496 | 497 | db.update('users').mset({'name':None, 'uid' : 12}).condition('name','user_1').limit(5) 498 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s LIMIT 5 499 | 500 | delete 501 | ----------- 502 | 503 | 504 | delete 用于构建delete from的sql语句。 505 | 506 | delete(table, key='default'): 507 | 508 | :table: 选定表 509 | :key: 用于指定使用那个数据库。 510 | 511 | 构建条件codition前面已经讲述了。请参考 `select`_ 512 | 513 | .. code-block:: python 514 | 515 | db.delete('users').condition('name','user_1') 516 | # > DELETE FROM `users` WHERE `name` = %s 517 | 518 | 构建好执行execute会执行数据库插入,execute返回的是删除的 rowcount: 519 | 520 | .. code-block:: python 521 | 522 | 523 | print q.execute() 524 | # > 2 525 | 526 | 527 | to_sql and str 528 | --------------------- 529 | 530 | ``db.insert``, ``db.update``, ``db.delete`` 返回的对象都可以使用 to_sql 或者__str__ 来查看构建成的sql语句。 531 | 532 | 533 | .. code-block:: python 534 | 535 | 536 | q = (db.update('users').set('name', 'update_test').set('uid', 12) 537 | .condition('name', 'user_2').condition('uid', 2)) 538 | print q.to_sql() 539 | print q 540 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s AND `uid` = %s 541 | 542 | 543 | transaction 544 | ------------ 545 | 546 | transaction(table, key='default'): 547 | 548 | :table: 选定表 549 | :key: 用于指定使用那个数据库。 550 | 551 | 对于事务,这里比较简单的实现。要么全部执行,要么全部不做,没有做保存点。 552 | 553 | 554 | 555 | .. code-block:: python 556 | 557 | 558 | # with context 559 | with db.transaction() as t: 560 | t.delete('users').condition('uid', 1).execute() 561 | (t.update('users').mset({'name':None, 'uid' : 12}) 562 | .condition('name','user_1').execute()) 563 | 564 | 565 | # 普通用法 566 | t = db.transaction() 567 | t.begin() 568 | t.delete('users').condition('uid', 1).execute() 569 | (t.update('users').mset({'name':None, 'uid' : 12}) 570 | .condition('name','user_1').execute()) 571 | 572 | #这里将会提交,如果失败将会rollback 573 | t.commit() 574 | 575 | .. note:: 使用 begin一定要结合commit方法,不然可能连接不会返还连接池。建议用 ``with`` 语句。 576 | 577 | 578 | simple orm 579 | ----------- 580 | 581 | 这里将会讲述最简单的orm构建技巧, 详细参考 `samples `_ 582 | 583 | .. code-block:: python 584 | 585 | import model 586 | from orm import Backend 587 | import db 588 | 589 | db.setup({ 'host': 'localhost', 'user': 'test', 'passwd': 'test', 'db': 'blog'}) 590 | 591 | 592 | user = Backend('user').find_by_username('username') 593 | if user and user.check('password'): 594 | print 'auth' 595 | 596 | user = model.User('username', 'email', 'real_name', 'password', 597 | 'bio', 'status', 'role') 598 | if Backend('user').create(user): 599 | print 'fine' 600 | 601 | user = Backend('user').find(12) 602 | user.real_name = 'blablabla....' 603 | if Backend('user').save(user): 604 | print 'user saved' 605 | 606 | if Backend('user').delete(user): 607 | print 'delete user failed' 608 | 609 | 610 | post = model.Post('title', 'slug', 'description', 'html', 'css', 'js', 611 | 'category', 'status', 'comments', 'author') 612 | if not Backend('post').create(post): 613 | print 'created failed' 614 | 615 | Future 616 | -------- 617 | 618 | 当前只支持mysql适配驱动,因为个人并不熟悉其他关联数据库,dbpy的设计比较灵活,所以如果有高手可以尝试写写其他数据库适配,仿照 `db/mysql目录 `_ 如果写pgsql的适配应该不会多余800行代码。 619 | 620 | 621 | 对于构建orm框架方面,从个人来讲,更喜欢原生SQL,也不打算再造一个orm轮子。从设计和实现来说,dbpy是为了更好的发挥原生SQL优势和简单灵活。 622 | 623 | 个人一些想法: 624 | 625 | #. 为select加入join构建方法糖。 626 | #. 尝试完成schema类,用于创建表,修改表结构等。 627 | #. 加入一些mysql特有的sql方法糖,比如replace, on dup更新等。 628 | #. 优化改进连接池,比如加入固定数量连接的连接池。 629 | 630 | 631 | LICENSE 632 | ======= 633 | 634 | Copyright (C) 2014-2015 Thomas Huang 635 | 636 | This program is free software: you can redistribute it and/or modify 637 | it under the terms of the GNU General Public License as published by 638 | the Free Software Foundation, version 2 of the License. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU General Public License for more details. 644 | 645 | You should have received a copy of the GNU General Public License 646 | along with this program. If not, see . 647 | 648 | -------------------------------------------------------------------------------- /db/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | __version__ = '0.1.3' 17 | VERSION = tuple(map(int, __version__.split('.'))) 18 | 19 | 20 | __all__ = [ 21 | 'and_', 22 | 'or_', 23 | 'expr', 24 | 'query', 25 | 'execute', 26 | 'transaction', 27 | 'setup', 28 | 'select', 29 | 'insert', 30 | 'update', 31 | 'delete', 32 | 'database', 33 | 'DBError' 34 | ] 35 | 36 | from db.query.select import QueryCondition 37 | from db.query.expr import Expr as expr 38 | from db._db import DB 39 | from db.errors import DBError 40 | from random import choice 41 | 42 | 43 | def and_(): 44 | return QueryCondition('AND') 45 | 46 | 47 | def or_(): 48 | return QueryCondition('OR') 49 | 50 | 51 | __db = {} 52 | 53 | 54 | def setup(config, minconn=5, maxconn=10, adapter='mysql', key='default', slave=False): 55 | """Setup database 56 | 57 | :param config dict: is the db adapter config 58 | :param key string: the key to identify dabtabase 59 | :param adapter string: the dabtabase adapter current support mysql only 60 | :param minconn int: the min connection for connection pool 61 | :param maxconn int: the max connection for connection pool 62 | :param slave boolean: If True the database can be read only. 63 | 64 | 65 | """ 66 | global __db 67 | 68 | if '.' in key: 69 | raise TypeError('The DB Key: "%s" Can\'t Contain dot' % (key)) 70 | 71 | if slave == False and key in __db: 72 | raise DBError('The Key: "%s" was set' % (key)) 73 | 74 | database = DB(config, minconn, maxconn, key, adapter) 75 | 76 | master_key = key 77 | slave_key = key + '.slave' 78 | 79 | if not slave: 80 | __db[master_key] = database 81 | if slave_key not in __db: 82 | __db[slave_key] = [database] 83 | else: 84 | if key in __db: 85 | databases = __db[slave_key] 86 | if len(databases) == 1 and __db[master_key] == databases[0]: 87 | __db[slave_key] = [database] 88 | else: 89 | __db[slave_key].append(database) 90 | else: 91 | __db[slave_key] = [database] 92 | 93 | 94 | def query(sql, args=None, many=None, as_dict=False, key='default'): 95 | """The connection raw sql query, when select table, show table 96 | to fetch records, it is compatible the dbi execute method:: 97 | 98 | 99 | :param sql string: the sql stamtement like 'select * from %s' 100 | :param args list: Wen set None, will use dbi execute(sql), else 101 | dbi execute(sql, args), the args keep the original rules, it shuld be tuple or list of list 102 | :param many int: when set, the query method will return genarate an iterate 103 | :param as_dict bool: when is True, the type of row will be dict, otherwise is tuple 104 | :param key: a key for your dabtabase you wanna use 105 | """ 106 | database = choice(__db[key + '.slave']) 107 | return database.query(sql, args, many, as_dict) 108 | 109 | 110 | def execute(sql, args=None, key='default'): 111 | """It is used for update, delete records. 112 | 113 | :param sql string: the sql stamtement like 'select * from %s' 114 | :param args list: Wen set None, will use dbi execute(sql), else 115 | dbi execute(sql, args), the args keep the original rules, it shuld be tuple or list of list 116 | :param key: a key for your dabtabase you wanna use 117 | 118 | eg:: 119 | 120 | execute('insert into users values(%s, %s)', [(1L, 'blablabla'), (2L, 'animer')]) 121 | execute('delete from users') 122 | """ 123 | database = __db[key] 124 | return database.execute(sql, args) 125 | 126 | 127 | def transaction(key='default'): 128 | """transaction wrapper 129 | 130 | :param key: a key for your dabtabase you wanna use 131 | """ 132 | database = __db[key] 133 | return database.transaction() 134 | 135 | 136 | def select(table, key='default'): 137 | """Select dialect 138 | 139 | 140 | :param key: a key for your dabtabase you wanna use 141 | """ 142 | database = choice(__db[key + '.slave']) 143 | return database.select(table) 144 | 145 | 146 | def insert(table, key='default'): 147 | """insert dialect 148 | 149 | :param key: a key for your dabtabase you wanna use 150 | """ 151 | database = __db[key] 152 | return database.insert(table) 153 | 154 | 155 | def update(table, key='default'): 156 | """update dialect 157 | 158 | :param key: a key for your dabtabase you wanna use 159 | """ 160 | database = __db[key] 161 | return database.update(table) 162 | 163 | 164 | def delete(table, key='default'): 165 | """delete dialect 166 | 167 | :param key: a key for your dabtabase you wanna use 168 | """ 169 | database = __db[key] 170 | return database.delete(table) 171 | 172 | 173 | def database(key='default', slave=False): 174 | """datbase dialect 175 | 176 | :param key: a key for your dabtabase you wanna use 177 | :param slave boolean: If True the database can be read only, Defaults False. 178 | """ 179 | if slave: 180 | key += '.slave' 181 | return choice(__db[key]) 182 | return __db.get(key) 183 | -------------------------------------------------------------------------------- /db/_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import sys 17 | import logging 18 | from db.errors import DBError 19 | from db.pool import ConnectionPool 20 | 21 | LOGGER = logging.getLogger('db') 22 | 23 | 24 | class DB(object): 25 | 26 | adapters = {} 27 | dialects = {} 28 | 29 | def __init__(self, config, minconn=5, maxconn=10, key='defalut', adapter='mysql'): 30 | """ Setup DB:: 31 | 32 | param config dict: is the db adapter config 33 | param key string: the key to identify dabtabase 34 | param adapter string: the dabtabase adapter current support mysql only 35 | param minconn int: the min connection for connection pool 36 | param maxconn int: the max connection for connection pool 37 | 38 | """ 39 | adapter = adapter or 'mysql' 40 | self.key = key 41 | self.adapter = adapter 42 | self.pool = ConnectionPool(minconn, maxconn, self.connection_class(adapter), config) 43 | self.dialect = self.dialect_class(adapter)(self) 44 | 45 | def select(self, table): 46 | """Select sql executor 47 | 48 | 49 | :param table: table name 50 | :type table: str 51 | :returns: select query instance 52 | """ 53 | return self.dialect.select(table) 54 | 55 | def insert(self, table): 56 | """insert sql executor 57 | 58 | 59 | :param table: table name 60 | :type table: str 61 | :returns: insert query instance 62 | """ 63 | return self.dialect.insert(table) 64 | 65 | def update(self, table): 66 | """update sql executor 67 | 68 | 69 | :param table: table name 70 | :type table: str 71 | :returns: update query instance 72 | """ 73 | return self.dialect.update(table) 74 | 75 | def delete(self, table): 76 | """delete sql executor 77 | 78 | 79 | :param table: table name 80 | :type table: str 81 | :returns: delete query instance 82 | """ 83 | return self.dialect.delete(table) 84 | 85 | def query(self, sql, args=None, many=None, as_dict=False): 86 | """The connection raw sql query, when select table, show table 87 | to fetch records, it is compatible the dbi execute method. 88 | 89 | 90 | :param sql string: the sql stamtement like 'select * from %s' 91 | :param args list: Wen set None, will use dbi execute(sql), else 92 | dbi execute(sql, args), the args keep the original rules, it shuld be tuple or list of list 93 | :param many int: when set, the query method will return genarate an iterate 94 | :param as_dict bool: when is true, the type of row will be dict, otherwise is tuple 95 | """ 96 | con = self.pool.pop() 97 | c = None 98 | try: 99 | c = con.cursor(as_dict) 100 | LOGGER.debug("Query sql: " + sql + " args:" + str(args)) 101 | c.execute(sql, args) 102 | if many and many > 0: 103 | return self._yield(con, c, many) 104 | else: 105 | return c.fetchall() 106 | 107 | except Exception as e: 108 | LOGGER.error("Error Qeury on %s", str(e)) 109 | raise DBError(e.args[0], e.args[1]) 110 | finally: 111 | many or (c and c.close()) 112 | many or (con and self.pool.push(con)) 113 | 114 | def _yield(self, con, cursor, many): 115 | try: 116 | result = cursor.fetchmany(many) 117 | while result: 118 | for row in result: 119 | yield row 120 | result = cursor.fetchmany(many) 121 | finally: 122 | cursor and cursor.close() 123 | con and self.pool.push(con) 124 | 125 | def execute(self, sql, args=None): 126 | """It is used for update, delete records. 127 | 128 | :param sql string: the sql stamtement like 'select * from %s' 129 | :param args list: Wen set None, will use dbi execute(sql), else 130 | dbi execute(sql, args), the args keep the original rules, it shuld be tuple or list of list 131 | 132 | eg:: 133 | 134 | execute('insert into users values(%s, %s)', [(1L, 'blablabla'), (2L, 'animer')]) 135 | execute('delete from users') 136 | """ 137 | con = self.pool.pop() 138 | c = None 139 | try: 140 | c = con.cursor() 141 | LOGGER.debug("Execute sql: " + sql + " args:" + str(args)) 142 | if type(args) is tuple: 143 | c.execute(sql, args) 144 | elif type(args) is list: 145 | if len(args) > 1 and type(args[0]) in (list, tuple): 146 | c.executemany(sql, args) 147 | else: 148 | c.execute(sql, args) 149 | elif args is None: 150 | c.execute(sql) 151 | if sql.lstrip()[:6].upper() == 'INSERT': 152 | return c.lastrowid 153 | return c.rowcount 154 | except Exception as e: 155 | LOGGER.error("Error Execute on %s", str(e)) 156 | raise DBError(str(e)) 157 | finally: 158 | c and c.close() 159 | con and self.pool.push(con) 160 | 161 | def transaction(self): 162 | return Transaction(self) 163 | 164 | def connection_class(self, adapter): 165 | """Get connection class by adapter""" 166 | if self.adapters.get(adapter): 167 | return self.adapters[adapter] 168 | try: 169 | class_prefix = getattr( 170 | __import__('db.' + adapter, globals(), locals(), 171 | ['__class_prefix__']), '__class_prefix__') 172 | driver = self._import_class('db.' + adapter + '.connection.' + 173 | class_prefix + 'Connection') 174 | except ImportError: 175 | raise DBError("Must install adapter `%s` or doesn't support" % 176 | (adapter)) 177 | 178 | self.adapters[adapter] = driver 179 | return driver 180 | 181 | def dialect_class(self, adapter): 182 | """Get dialect sql class by adapter""" 183 | if self.dialects.get(adapter): 184 | return self.dialects[adapter] 185 | try: 186 | class_prefix = getattr( 187 | __import__('db.' + adapter, globals(), locals(), 188 | ['__class_prefix__']), '__class_prefix__') 189 | driver = self._import_class('db.' + adapter + '.dialect.' + 190 | class_prefix + 'Dialect') 191 | except ImportError: 192 | raise DBError("Must install adapter `%s` or doesn't support" % 193 | (adapter)) 194 | 195 | self.dialects[adapter] = driver 196 | return driver 197 | 198 | def _import_class(self, module2cls): 199 | """Import class by module dot split string""" 200 | d = module2cls.rfind(".") 201 | classname = module2cls[d + 1: len(module2cls)] 202 | m = __import__(module2cls[0:d], globals(), locals(), [classname]) 203 | return getattr(m, classname) 204 | 205 | 206 | class lazy_attr(object): 207 | 208 | def __init__(self, wrapped): 209 | self.wrapped = wrapped 210 | try: 211 | self.__doc__ = wrapped.__doc__ 212 | except: # pragma: no cover 213 | pass 214 | 215 | def __get__(self, inst, objtype=None): 216 | if inst is None: 217 | return self 218 | val = self.wrapped(inst) 219 | setattr(inst, self.wrapped.__name__, val) 220 | return val 221 | 222 | 223 | class Transaction(object): 224 | """Database sql Transaction""" 225 | 226 | def __init__(self, db): 227 | self._db = db 228 | self._con = None 229 | 230 | @lazy_attr 231 | def dialect(self): 232 | return self._db.dialects.get(self._db.adapter)(self) 233 | 234 | def __enter__(self): 235 | self._con = self._db.pool.pop() 236 | self._con.ensure_connect() 237 | self._con.autocommit(False) 238 | return self 239 | 240 | def __exit__(self, exc_type, exc_value, traceback): 241 | try: 242 | self._con.commit() 243 | self._con.autocommit(True) 244 | except Exception as e: 245 | try: 246 | self._con.rollback() 247 | except Exception as e_: 248 | LOGGER.error('When transaction happend error: %s', e_) 249 | raise e 250 | finally: 251 | self._db.pool.push(self._con) 252 | self._con = None 253 | self._db = None 254 | 255 | def begin(self): 256 | """Begins transaction""" 257 | self._con = self._db.pool.pop() 258 | self._con.ensure_connect() 259 | self._con.autocommit(False) 260 | 261 | def commit(self): 262 | """Commits transaction""" 263 | try: 264 | self._con.commit() 265 | self._con.autocommit(True) 266 | except Exception as e: 267 | try: 268 | self._con.rollback() 269 | except Exception as e_: 270 | LOGGER.error('When transaction happend error: %s', e_) 271 | raise e 272 | finally: 273 | self._db.pool.push(self._con) 274 | self._con = None 275 | self._db = None 276 | 277 | def execute(self, sql, args): 278 | """Execute sql 279 | 280 | :param sql string: the sql stamtement like 'select * from %s' 281 | :param args list: Wen set None, will use dbi execute(sql), else 282 | db execute(sql, args), the args keep the original rules, it shuld be tuple or list of list 283 | """ 284 | c = None 285 | try: 286 | c = self._con.cursor() 287 | LOGGER.debug("execute sql: " + sql + " args:" + str(args)) 288 | if type(args) is tuple: 289 | c.execute(sql, args) 290 | elif type(args) is list: 291 | if len(args) > 1 and type(args[0]) in (list, tuple): 292 | c.executemany(sql, args) 293 | else: 294 | c.execute(sql, args) 295 | elif args is None: 296 | c.execute(sql) 297 | if sql.lstrip()[:6].upper() == 'INSERT': 298 | return c.lastrowid 299 | return c.rowcount 300 | finally: 301 | c and c.close() 302 | 303 | def insert(self, table): 304 | """Insert sql diaect""" 305 | return self.dialect.insert(table) 306 | 307 | def update(self, table): 308 | """update sql diaect""" 309 | return self.dialect.update(table) 310 | 311 | def delete(self, table): 312 | """delete sql diaect""" 313 | return self.dialect.delete(table) 314 | -------------------------------------------------------------------------------- /db/connection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | class Connection(object): 18 | """Base Database Connection class 19 | 20 | :param db_options: db optional configuration, defaults to None 21 | :type db_options: dict, optional 22 | """ 23 | 24 | def __init__(self, db_options=None): 25 | db_options = db_options or {} 26 | 27 | #: database optional configuration, defaults to None 28 | self._db_options = self.default_options() 29 | self._db_options.update(db_options) 30 | 31 | #: database real connection 32 | self._connect = None 33 | self.initialize() 34 | 35 | def initialize(self): 36 | """Initialize customize configuration in subclass""" 37 | pass 38 | 39 | def default_options(self): 40 | """Defalut options for intailize sql connection""" 41 | return {} 42 | 43 | def connect(self): 44 | """connects database""" 45 | raise NotImplementedError('Must implement connect in Subclass') 46 | 47 | def close(self): 48 | """Close connect""" 49 | if self._connect is not None: 50 | self._connect.close() 51 | self._connect = None 52 | 53 | def ensure_connect(self): 54 | """Ensure the connetion is useable""" 55 | raise NotImplementedError('Must implement ensure_connect in Subclass') 56 | 57 | def cursor(self, as_dict=False): 58 | """Gets the cursor by type , if ``as_dict is ture, make a dict sql connection cursor""" 59 | self.ensure_connect() 60 | ctype = self.real_ctype(as_dict) 61 | return self._connect.cursor(ctype) 62 | 63 | def real_ctype(self, as_dict): 64 | """The real sql cursor type""" 65 | raise NotImplementedError('Must implement real_ctype in Subclass') 66 | 67 | def driver(self): 68 | """Get database driver""" 69 | return None 70 | 71 | def commit(self): 72 | """Commit batch execute""" 73 | self._connect.commit() 74 | 75 | def rollback(self): 76 | """Rollback database process""" 77 | self._connect.rollback() 78 | 79 | def autocommit(self, enable=True): 80 | """Sets commit to auto if True""" 81 | self._connect.autocommit(enable) 82 | -------------------------------------------------------------------------------- /db/dialect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from db.query.select import SelectQuery 17 | from db.query.base import Query 18 | from db.query.expr import Expr 19 | from db.query.update import UpdateQuery 20 | from db.query.insert import InsertQuery 21 | from db.query.delete import DeleteQuery 22 | 23 | class Dialect(object): 24 | 25 | def __init__(self, db=None): 26 | self._identifier = '"' 27 | self.db = db 28 | self.initialize() 29 | 30 | def initialize(self): 31 | pass 32 | 33 | def select(self, table): 34 | return SelectQuery(table, self, self.db) 35 | 36 | def update(self, table): 37 | return UpdateQuery(table, self, self.db) 38 | 39 | def insert(self, table): 40 | return InsertQuery(table, self, self.db) 41 | 42 | def delete(self, table): 43 | return DeleteQuery(table, self, self.db) 44 | 45 | def escape(self): 46 | pass 47 | 48 | def quote(self, value): 49 | if not value: 50 | return 'NULL' 51 | elif value == True: 52 | return "'1'" 53 | elif value == False: 54 | return "'0'" 55 | elif isinstance(value, Query): 56 | if isinstance(value, SelectQuery): 57 | return '(' + value.compile() + ')' 58 | elif isinstance(value, Expr): 59 | return value.compile() 60 | else: 61 | return self.quote(value) 62 | elif isinstance(value, list): 63 | return '(' + ', '.join([self.quote(_) for _ in vlaue]) + ')' 64 | else: 65 | return str(value) 66 | return self.escape(value) 67 | 68 | def quote_column(self, column): 69 | alias = None 70 | escaped_identifier = self._identifier + self._identifier 71 | if isinstance(column, list): 72 | column, alias = column 73 | alias = alias.replace(self._identifier, escaped_identifier) 74 | 75 | if isinstance(column, Query): 76 | column = '(' + column.compile() + ')' 77 | elif isinstance(column, Expr): 78 | column = column.compile(self) 79 | else: 80 | column = str(column) 81 | column = column.replace(self._identifier, escaped_identifier) 82 | if column == '*': 83 | return column 84 | elif column.find('.') != -1: 85 | parts = column.split('.') 86 | _parts = [] 87 | for part in parts: 88 | if part != '*': 89 | _parts.append(self._identifier + part + self._identifier) 90 | else: 91 | _parts.append(part) 92 | column = '.'.join(_parts) 93 | elif 'as' not in column.lower(): 94 | column = self._identifier + column + self._identifier 95 | 96 | if alias: 97 | column += ' AS ' + self._identifier + alias + self._identifier 98 | return column 99 | 100 | def quote_table(self, table): 101 | alias = None 102 | escaped_identifier = self._identifier + self._identifier 103 | if isinstance(table, list): 104 | table, alias = table 105 | alias = alias.replace(self._identifier, escaped_identifier) 106 | if isinstance(table, Query): 107 | table = '(' + table.compile() + ')' 108 | 109 | elif isinstance(table, Expr): 110 | table = table.compile() 111 | else: 112 | table = table.replace(self._identifier, escaped_identifier) 113 | if table.find('.') != -1: 114 | parts = table.split('.') 115 | parts = [self._identifier + part + self._identifier for part in parts] 116 | table = '.'.join(parts) 117 | else: 118 | table = self._identifier + table + self._identifier 119 | 120 | if alias: 121 | table += ' AS ' + self._identifier + alias + self._identifier 122 | 123 | return table 124 | 125 | def quote_identifier(self, value): 126 | escaped_identifier = self._identifier + self._identifier 127 | 128 | if isinstance(value, list): 129 | value, alias = value 130 | alias = alias.replace(self._identifier, escaped_identifier) 131 | if isinstance(value, Query): 132 | value = '(' + value.compile() + ')' 133 | 134 | elif isinstance(value, Expr): 135 | value = value.compile() 136 | else: 137 | value = value.replace(self._identifier, escaped_identifier) 138 | if value.find('.') != -1: 139 | parts = value.split('.') 140 | parts = [self._identifier + part + self._identifier for part in parts] 141 | value = '.'.join(parts) 142 | else: 143 | value = self._identifier + value + self._identifier 144 | if alias: 145 | value += ' AS ' + self._identifier + alias + self._identifier 146 | 147 | return value 148 | 149 | def map_condition_operator(self, operator): 150 | return None -------------------------------------------------------------------------------- /db/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | class DBError(Exception): pass 17 | 18 | class NotInstallDriverError(DBError): pass 19 | 20 | class ConnectError(DBError): pass 21 | -------------------------------------------------------------------------------- /db/mysql/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | __class_prefix__ = 'MySQL' -------------------------------------------------------------------------------- /db/mysql/connection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from db.connection import Connection 17 | from db.errors import ( 18 | NotInstallDriverError, 19 | ) 20 | 21 | try: 22 | import MySQLdb 23 | from MySQLdb.cursors import DictCursor, Cursor 24 | except ImportError: 25 | raise NotInstallDriverError("Must install MySQLdb module fistly") 26 | 27 | 28 | import time 29 | import logging 30 | 31 | LOGGER = logging.getLogger('db.mysql') 32 | 33 | 34 | class MySQLConnection(Connection): 35 | 36 | def initialize(self): 37 | self._last_used = time.time() 38 | self._max_idle = self._db_options.pop('max_idle', 10) 39 | 40 | def default_options(self): 41 | return { 42 | 'port': 3306, 43 | 'host': 'localhost', 44 | 'user': 'test', 45 | 'passwd': 'test', 46 | 'db': 'test', 47 | 'use_unicode': True, 48 | 'charset': 'utf8' 49 | } 50 | 51 | def connect(self): 52 | self.close() 53 | self._connect = MySQLdb.connect(**self._db_options) 54 | self._connect.autocommit(True) 55 | 56 | def ensure_connect(self): 57 | if not self._connect or self._max_idle < (time.time() - self._last_used): 58 | try: 59 | self._connect.ping() 60 | except: 61 | self.connect() 62 | self._last_used = time.time() 63 | 64 | def real_ctype(self, as_dict=False): 65 | if as_dict: 66 | return DictCursor 67 | return Cursor 68 | 69 | def driver(self): 70 | return 'mysql' 71 | -------------------------------------------------------------------------------- /db/mysql/dialect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from db.dialect import Dialect 17 | 18 | class MySQLDialect(Dialect): 19 | 20 | def initialize(self): 21 | self._identifier = '`' -------------------------------------------------------------------------------- /db/pool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import threading 17 | import logging 18 | from db.errors import DBError 19 | import time 20 | 21 | 22 | LOGGER = logging.getLogger('db.pool') 23 | 24 | class BaseConnectionPool(object): 25 | 26 | def __init__(self, minconn, maxconn, connection_cls, db_options={}): 27 | self.db_options = db_options 28 | self.maxconn = maxconn 29 | self.minconn = minconn if self.maxconn > minconn else int(self.maxconn * 0.2) 30 | if not connection_cls: 31 | raise ValueError('Must be Connection subclass') 32 | self.connection_cls = connection_cls 33 | 34 | def new_connect(self): 35 | return self.connection_cls(self.db_options) 36 | 37 | def push(self, con): 38 | pass 39 | 40 | def pop(self): 41 | pass 42 | 43 | def release(self): 44 | pass 45 | 46 | 47 | class ConnectionPool(BaseConnectionPool): 48 | 49 | def __init__(self, minconn=3, maxconn=10,connection_cls=None, db_options={}): 50 | self._created_conns = 0 51 | BaseConnectionPool.__init__(self, minconn, maxconn, connection_cls, db_options) 52 | self._lock = threading.Lock() 53 | self._available_conns = [] 54 | self._in_use_conns = [] 55 | for i in range(self.minconn): 56 | self._available_conns.append(self.new_connect()) 57 | 58 | def pop(self): 59 | con = None 60 | first_tried = time.time() 61 | while True: 62 | self._lock.acquire() 63 | try: 64 | con = self._available_conns.pop(0) 65 | self._in_use_conns.append(con) 66 | break 67 | except IndexError: 68 | 69 | if self._created_conns < self.maxconn: 70 | 71 | self._created_conns += 1 72 | con = self.new_connect() 73 | self._in_use_conns.append(con) 74 | break 75 | finally: 76 | self._lock.release() 77 | 78 | if not con and 3 <= (time.time() - first_tried): 79 | raise DBPoolError("tried 3 seconds, can't load connection, maybe too many threads") 80 | 81 | return con 82 | 83 | def push(self, con): 84 | self._lock.acquire() 85 | if con in self._in_use_conns: 86 | self._in_use_conns.remove(con) 87 | self._available_conns.append(con) 88 | self._lock.release() 89 | 90 | def release(self): 91 | with self._lock: 92 | for conn in self._available_conns: 93 | conn.close() 94 | for conn in self._in_use_conns: 95 | conn.close() 96 | self._available_conns[:] = [] 97 | self._in_use_conns[:] = [] 98 | self._created_conns = 0 99 | 100 | 101 | class DBPoolError(DBError): pass 102 | -------------------------------------------------------------------------------- /db/pymysql/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | __class_prefix__ = 'PyMySQL' -------------------------------------------------------------------------------- /db/pymysql/connection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from db.connection import Connection 17 | from db.errors import ( 18 | NotInstallDriverError, 19 | ) 20 | 21 | try: 22 | import pymysql 23 | pymysql.install_as_MySQLdb() 24 | from pymysql.cursors import DictCursor, Cursor 25 | except ImportError: 26 | raise NotInstallDriverError("Must install pymysql module fistly") 27 | 28 | 29 | 30 | import time 31 | import logging 32 | 33 | LOGGER = logging.getLogger('db.pymysql') 34 | 35 | 36 | class PyMySQLConnection(Connection): 37 | 38 | def initialize(self): 39 | self._last_used = time.time() 40 | self._max_idle = self._db_options.pop('max_idle', 10) 41 | 42 | def default_options(self): 43 | return { 44 | 'port': 3306, 45 | 'host': 'localhost', 46 | 'user': 'test', 47 | 'passwd': 'test', 48 | 'db': 'test', 49 | 'use_unicode': True, 50 | 'charset': 'utf8' 51 | } 52 | 53 | def connect(self): 54 | self.close() 55 | self._connect = pymysql.connect(**self._db_options) 56 | self._connect.autocommit(True) 57 | 58 | def ensure_connect(self): 59 | if not self._connect or self._max_idle < (time.time() - self._last_used): 60 | try: 61 | self._connect.ping() 62 | except: 63 | self.connect() 64 | self._last_used = time.time() 65 | 66 | def real_ctype(self, as_dict=False): 67 | if as_dict: 68 | return DictCursor 69 | return Cursor 70 | 71 | def driver(self): 72 | return 'pymysql' 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /db/pymysql/dialect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from db.dialect import Dialect 17 | 18 | class PyMySQLDialect(Dialect): 19 | 20 | def initialize(self): 21 | self._identifier = '`' -------------------------------------------------------------------------------- /db/query/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import logging 17 | 18 | LOGGER = logging.getLogger('db.query') -------------------------------------------------------------------------------- /db/query/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | class Query(object): 18 | """Base Sql Query class""" 19 | 20 | def __init__(self, dialect): 21 | """The sql dialect for building sql""" 22 | self.dialect = dialect 23 | 24 | def close(self): 25 | """Reset the dialect to none""" 26 | self.dialect = None 27 | 28 | def __del__(self): 29 | self.close() 30 | 31 | def to_sql(self): 32 | """Generates the sql statement""" 33 | return self.compile() 34 | 35 | def compile(self): 36 | """Comile the sql statement""" 37 | raise NotImplementedError("Implements ``compile`` in subclass...") 38 | 39 | __str__ = to_sql 40 | 41 | def execute(self): 42 | """excute database sql operator""" 43 | raise NotImplementedError("Implements ``execute`` in subclass...") 44 | -------------------------------------------------------------------------------- /db/query/delete.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from db.query.select import WhereQuery 17 | 18 | 19 | class DeleteQuery(WhereQuery): 20 | """Delete operator query builder""" 21 | 22 | def __init__(self, table, dialect, db): 23 | """Constructor 24 | 25 | :param table: table name 26 | :type table: str 27 | :param dialect: the sql dialect instance 28 | :param db: the database connection instance 29 | """ 30 | if table: 31 | self._table = table 32 | self._db = db 33 | WhereQuery.__init__(self, dialect) 34 | 35 | def table(self, table): 36 | """Sets table name""" 37 | self._table = table 38 | return self 39 | 40 | def compile(self): 41 | """Compiles the delete sql statement""" 42 | sql = '' 43 | sql += 'DELETE FROM ' + self.dialect.quote_table(self._table) 44 | if self._where: 45 | sql += ' WHERE ' + self.compile_condition(self._where) 46 | if self._order_by: 47 | sql += ' ' + self.compile_order_by(self._order_by) 48 | 49 | if self._limit: 50 | sql += ' LIMIT ' + self._limit 51 | return sql 52 | 53 | def clear(self): 54 | """Clear and reset to orignal state""" 55 | WhereQuery.clear(self) 56 | self._table = None 57 | self._parameters = [] 58 | self._sql = None 59 | 60 | def execute(self): 61 | """Execute the sql for delete operator""" 62 | return self._db.execute(self.to_sql(), self.bind) 63 | -------------------------------------------------------------------------------- /db/query/expr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | class Expr(object): 18 | """Sql expresion builder""" 19 | 20 | def __init__(self, expression, alias=None): 21 | #: sql expresion 22 | self.expression = expression 23 | #: expresssion filed name 24 | self.alias = alias 25 | 26 | def compile(self, db): 27 | """Building the sql expression 28 | 29 | :param db: the database instance 30 | """ 31 | sql = self.expression 32 | if self.alias: 33 | sql += (' AS ' + db.quote_column(self.alias)) 34 | return sql 35 | -------------------------------------------------------------------------------- /db/query/insert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from db.query.base import Query 17 | 18 | 19 | class InsertQuery(Query): 20 | 21 | def __init__(self, table, dialect, db, columns=[]): 22 | self._columns = columns or [] 23 | self._values = [] 24 | self._table = table 25 | self._db = db 26 | Query.__init__(self, dialect) 27 | 28 | def table(self, table): 29 | self._table = table 30 | return self 31 | 32 | def fields(self, *columns): 33 | self._columns.extend(columns) 34 | return self 35 | 36 | def values(self, values): 37 | """The values for insert , 38 | it can be a dict row or list tuple row. 39 | """ 40 | if isinstance(values, dict): 41 | l = [] 42 | for column in self._columns: 43 | l.append(values[column]) 44 | self._values.append(tuple(l)) 45 | else: 46 | self._values.append(values) 47 | return self 48 | 49 | def compile(self): 50 | sql = 'INSERT INTO ' + self.dialect.quote_table(self._table) 51 | if self._columns: 52 | sql += ' (' + ', '.join([self.dialect.quote_column(_) for _ in self._columns]) + ')' 53 | sql += ' VALUES(' + ', '.join(['%s' for _ in range(len(self._values[0]))]) + ')' 54 | return sql 55 | 56 | def execute(self): 57 | bind = self._values[0] if len(self._values) == 1 else self._values 58 | return self._db.execute(self.to_sql(), bind) 59 | 60 | def clear(self): 61 | self._columns = [] 62 | self._values = [] 63 | return self 64 | -------------------------------------------------------------------------------- /db/query/select.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from db.query.base import Query 17 | from db.errors import DBError 18 | from db.query import LOGGER 19 | 20 | 21 | class QueryCondition(object): 22 | 23 | def __init__(self, glue): 24 | self.glue = glue 25 | self.conditions = [] 26 | self.changed = True 27 | 28 | self.bind = [] 29 | 30 | def __nonzero__(self): 31 | return bool(len(self.conditions)) 32 | __bool__ = __nonzero__ 33 | 34 | def where(self, fields, snippet): 35 | self.conditions.append(dict( 36 | field=fields, 37 | value=snippet, 38 | op=None 39 | )) 40 | self.changed = True 41 | return self 42 | 43 | def __len__(self): 44 | return len(self.conditions) 45 | 46 | def condition(self, field, value=None, op=None): 47 | if not op: 48 | if isinstance(value, (list, tuple)): 49 | op = 'IN' 50 | elif value is None: 51 | op = 'IS NULL' 52 | else: 53 | op = '=' 54 | self.conditions.append(dict( 55 | field=field, 56 | value=value, 57 | op=op 58 | )) 59 | self.changed = True 60 | 61 | return self 62 | 63 | def is_null(self, field): 64 | return self.condition(field) 65 | 66 | def is_not_null(self, field): 67 | return self.condition(field, None, 'IS NOT NULL') 68 | 69 | def exists(self, select): 70 | return self.condition('', select, 'EXISTS') 71 | 72 | def not_exists(self, select): 73 | return self.condition('', select, 'NOT EXISTS') 74 | 75 | def compile(self, db, query_placeholder='%s'): 76 | if self.changed: 77 | condition_fragments = [] 78 | #arguments = [] 79 | conditions = [_.copy() for _ in self.conditions] 80 | glue = self.glue 81 | self.glue = None 82 | 83 | for condition in conditions: 84 | is_select = False 85 | if not condition.get('op'): 86 | condition_fragments.append(' (' + condition['field'] + ') ') 87 | else: 88 | field = condition['field'] 89 | if isinstance(field, QueryCondition): 90 | field.compile(db) 91 | if len(condition) == 1: 92 | condition_fragments.append(field.to_sql()) 93 | else: 94 | condition_fragments.append( 95 | ' (' + field.to_sql() + ') ') 96 | self.bind.extend(field.bind) 97 | else: 98 | operator_defaults = dict( 99 | prefix='', 100 | postfix='', 101 | delimiter='', 102 | op=condition['op'], 103 | use_value=True) 104 | op = db.map_condition_operator(condition['op']) 105 | if not op: 106 | op = self.map_condition_operator( 107 | condition['op']) 108 | operator_defaults.update(op) 109 | op = operator_defaults 110 | placeholders = [] 111 | if isinstance(condition['value'], SelectQuery): 112 | is_select = True 113 | condition['value'].compile( 114 | db, query_placeholder) 115 | placeholders.append(condition['value'].to_sql()) 116 | self.bind.extend(condition.bind) 117 | op['use_value'] = False 118 | elif not op.get('delimiter'): 119 | condition['value'] = [condition['value']] 120 | 121 | if op.get('use_value'): 122 | for value in condition['value']: 123 | if isinstance(value, list): 124 | placeholders.append( 125 | ', '.join(['%s'] * len(value))) 126 | self.bind.extend(value) 127 | else: 128 | self.bind.append(value) 129 | placeholders.append('%s') 130 | 131 | if is_select: 132 | LOGGER.deubg('select :' + db.quote_column(condition['field'])) 133 | condition_fragments.append(' (' + 134 | db.quote_column(condition['field']) + ' ' + 135 | op['op'] + ' ' + op['prefix'] + 136 | op['delimiter'].join(placeholders) + op['postfix'] + ') ') 137 | else: 138 | condition_fragments.append(' ' + 139 | db.quote_column(condition['field']) + ' ' + 140 | op['op'] + ' ' + op['prefix'] + 141 | op['delimiter'].join(placeholders) + op['postfix'] + ' ') 142 | 143 | self.changed = False 144 | self.string_version = glue.join(condition_fragments) 145 | 146 | def to_sql(self): 147 | if self.changed: 148 | return '' 149 | return self.string_version 150 | __str__ = to_sql 151 | 152 | def map_condition_operator(self, op): 153 | specials = { 154 | 'BETWEEN': {'delimiter': ' AND '}, 155 | 'IN': {'delimiter': ', ', 'prefix': ' (', 'postfix': ') '}, 156 | 'NOT IN': {'delimiter': ', ', 'prefix': ' (', 'postfix': ')'}, 157 | 'EXISTS': {'prefix': ' (', 'postfix': ') '}, 158 | 'NOT EXISTS': {'prefix': ' (', 'postfix': ') '}, 159 | 'IS NULL': {'use_value': False}, 160 | 'IS NOT NULL': {'use_value': False}, 161 | 'LIKE': {}, 162 | 'NOT LIKE': {}, 163 | '=': {}, 164 | '<': {}, 165 | '>': {}, 166 | '>=': {}, 167 | '<=': {} 168 | } 169 | if specials.get(op): 170 | result = specials.get(op) 171 | else: 172 | op = op.upper() 173 | result = specials.get(op) if specials.get(op) else {} 174 | 175 | result['op'] = op 176 | return result 177 | 178 | 179 | class WhereQuery(Query): 180 | 181 | def __init__(self, dialect): 182 | self._where = QueryCondition('AND') 183 | self._order_by = [] 184 | self._limit = None 185 | 186 | self.bind = [] 187 | 188 | Query.__init__(self, dialect) 189 | 190 | def where(self, snippet, args): 191 | self._where.where(snippet, args) 192 | return self 193 | 194 | def condition(self, field, value=None, operator=None): 195 | self._where.condition(field, value, operator) 196 | return self 197 | 198 | def conditions(self): 199 | return self._where.conditions() 200 | 201 | def arguments(self): 202 | return self._where.arguments() 203 | 204 | def is_null(self, field): 205 | self._where.is_null(field) 206 | return self 207 | 208 | def is_not_null(self, field): 209 | self._where.is_not_null(field) 210 | return self 211 | 212 | def exists(self, query): 213 | self._where.exists(query) 214 | return self 215 | 216 | def not_exists(self, query): 217 | self._where.not_exists(query) 218 | return self 219 | 220 | def order_by(self, column, direction=''): 221 | self._order_by.append((column, direction)) 222 | return self 223 | 224 | def limit(self, limt): 225 | self._limit = limt 226 | return self 227 | 228 | def compile_condition(self, condition): 229 | condition.compile(self.dialect, self) 230 | sql = condition.to_sql() 231 | self.bind.extend(condition.bind) 232 | return sql 233 | 234 | def compile_set(self, values): 235 | sets = [] 236 | for column, value in values: 237 | column = self.dialect.quote_column(column) 238 | self.bind.append(value) 239 | sets.append(column + ' = ' + '%s') 240 | 241 | return ', '.join(sets) 242 | 243 | def compile_group_by(self, columns): 244 | group = [] 245 | for column in columns: 246 | column = self.dialect.quote_identifier(column) if isinstance(column, list) else \ 247 | self.dialect.quote_column(column) 248 | group.append(column) 249 | 250 | return 'GROUP BY ' + ', '.join(group) 251 | 252 | def compile_order_by(self, columns, direction=''): 253 | sorts = [] 254 | for column, direction in columns: 255 | column = self.dialect.quote_identifier(column) if isinstance(column, list) else \ 256 | self.dialect.quote_column(column) 257 | if direction: 258 | direction = ' ' + direction.upper() 259 | sorts.append(column + direction) 260 | return 'ORDER BY ' + ', '.join(sorts) 261 | 262 | 263 | def clear(self): 264 | self._where = QueryCondition('AND') 265 | self._order_by = [] 266 | self._limit = None 267 | self.bind = [] 268 | 269 | 270 | 271 | class SelectQuery(WhereQuery): 272 | 273 | def __init__(self, table, dialect, db, columns=None): 274 | 275 | self._select = [] 276 | self._distinct = False 277 | 278 | self._from = [table] 279 | self._group_by = [] 280 | self._offset = [] 281 | 282 | self._select = columns or [] 283 | self._db = db 284 | WhereQuery.__init__(self, dialect) 285 | 286 | def execute(self, many=None, as_dict=False): 287 | return self._db.query(self.to_sql(), self.bind, many=many, as_dict=as_dict) 288 | 289 | def distinct(self, distinct=True): 290 | self._distinct = distinct 291 | return self 292 | 293 | def select(self, *columns): 294 | self._select.extend(columns) 295 | return self 296 | 297 | fields = select 298 | 299 | def fram(self, tables): 300 | self._from.extend(tables) 301 | return self 302 | 303 | def group_by(self, *columns): 304 | self._group_by.extend(columns) 305 | return self 306 | 307 | def offset(self, offset): 308 | self._offset = offset 309 | return self 310 | 311 | def compile(self): 312 | quote_column = self.dialect.quote_column 313 | quote_table = self.dialect.quote_table 314 | 315 | sql = 'SELECT ' 316 | if self._distinct: 317 | sql += 'DISTINCT ' 318 | if not self._select: 319 | sql += '*' 320 | else: 321 | sql += ', '.join([quote_column(_) for _ in self._select]) 322 | 323 | if self._from: 324 | sql += ' FROM ' + ', '.join([quote_table(_) for _ in self._from]) 325 | 326 | if self._where: 327 | sql += '\nWHERE ' + self.compile_condition(self._where) 328 | 329 | if self._group_by: 330 | sql += '\n' + self.compile_group_by(self._group_by) 331 | 332 | if self._order_by: 333 | sql += '\n' + self.compile_order_by(self._order_by) 334 | if self._limit: 335 | sql += '\nLIMIT ' + str(self._limit) 336 | 337 | if self._offset: 338 | sql += ' OFFSET ' + str(self._offset) 339 | return sql 340 | 341 | def clear(self): 342 | self._select = [] 343 | self._distinct = False 344 | self._from = [] 345 | self._join = [] 346 | self._group_by = [] 347 | self._offset = [] 348 | self._where = QueryCondition('AND') 349 | self._last_join = None 350 | self._limit = None 351 | self._offset = None -------------------------------------------------------------------------------- /db/query/update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from db.query.select import WhereQuery 17 | 18 | 19 | class UpdateQuery(WhereQuery): 20 | 21 | def __init__(self, table, dialect, db): 22 | 23 | self._table = table 24 | self._db = db 25 | self._set = [] 26 | WhereQuery.__init__(self, dialect) 27 | 28 | def table(self, table): 29 | self._table = table 30 | return self 31 | 32 | def execute(self): 33 | return self._db.execute(self.to_sql(), self.bind) 34 | 35 | def mset(self, kwargs): 36 | for c in kwargs.iteritems(): 37 | self._set.append(c) 38 | 39 | return self 40 | 41 | def set(self, column, value): 42 | self._set.append((column, value)) 43 | return self 44 | 45 | 46 | def compile(self): 47 | sql = 'UPDATE ' + self.dialect.quote_table(self._table) 48 | 49 | sql += ' SET ' + self.compile_set(self._set) 50 | if self._where: 51 | sql += ' WHERE ' + self.compile_condition(self._where) 52 | if self._order_by: 53 | sql += self.compile_order_by(self._where) 54 | 55 | if self._limit: 56 | sql += ' LIMIT ' + str(self._limit) 57 | 58 | return sql 59 | 60 | def clear(self): 61 | WhereQuery.clear(self) 62 | self._table = None 63 | self._set = [] 64 | self._limit = None 65 | self.bind = [] -------------------------------------------------------------------------------- /samples/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | import model 18 | from orm import Backend 19 | import db 20 | 21 | db.setup({'host': 'localhost', 'user': 'test', 'passwd': 'test', 'db': 'blog'}) 22 | 23 | 24 | user = Backend('user').find_by_username('username') 25 | if user and user.check('password'): 26 | print('auth') 27 | 28 | user = model.User('username', 'email', 'real_name', 'password', 'bio', 'status', 'role') 29 | if Backend('user').create(user): 30 | print('fine') 31 | 32 | user = Backend('user').find(12) 33 | user.real_name = 'blablabla....' 34 | if Backend('user').save(user): 35 | print('user saved') 36 | 37 | if Backend('user').delete(user): 38 | print('delete user failed') 39 | 40 | 41 | post = model.Post('title', 'slug', 'description', 'html', 'css', 'js', 'category', 'status', 'comments', 'author') 42 | if not Backend('post').create(post): 43 | print('created failed') 44 | -------------------------------------------------------------------------------- /samples/model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from hashlib import sha224 17 | from datetime import datetime 18 | 19 | 20 | class User(object): 21 | 22 | def __init__(self, username, email, real_name, password, bio, status, role='user', uid=None): 23 | """If the user load from database, if will intialize the uid and secure password. 24 | Otherwise will hash encrypt the real password 25 | 26 | arg role enum: the string in ('user', 'editor', 'administrator') 27 | arg status enum: the string in ('actived', 'inactive') 28 | arg password fix legnth string: the use sha224 password hash 29 | """ 30 | 31 | self.username = username 32 | self.email = email 33 | self.real_name = real_name 34 | self.bio = bio 35 | self.status = status 36 | self.role = role 37 | 38 | if uid: 39 | self.uid = uid 40 | self.password = password 41 | else: 42 | self.password = self.secure_password(password) 43 | 44 | def check(self, password): 45 | """Check the password""" 46 | return self.password == self.secure_password(password) 47 | 48 | def secure_password(self, password): 49 | """Encrypt password to sha224 hash""" 50 | return sha224(password).hexdigest() 51 | 52 | def as_json(self): 53 | data = self.__dict__.copy() 54 | del data['password'] 55 | return data 56 | 57 | 58 | class Post(object): 59 | 60 | def __init__(self, title, slug, description, html, css, js, category, status, 61 | comments, author=None, created=datetime.now(), pid=None): 62 | self.title = title 63 | self.slug = slug 64 | self.description = description 65 | self.created = created 66 | self.html = html 67 | self.css = css 68 | self.js = js 69 | self.category = category 70 | self.status = status 71 | self.comments = comments 72 | self.author = author 73 | self.created = created 74 | self.pid = pid 75 | 76 | def as_json(self): 77 | data = self.__dict__.copy() 78 | return data 79 | -------------------------------------------------------------------------------- /samples/orm.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import logging 17 | import db 18 | 19 | from model import User, Post 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | class BaseMapper(object): 25 | 26 | def load(self, data, o): 27 | return o(*data) 28 | 29 | 30 | class PrimaryTrait(object): 31 | 32 | primary_id = 'id' 33 | 34 | def find(self, id): 35 | q = db.select(self.table).condition(self.primary_id, id) 36 | data = q.query() 37 | if data: 38 | return self.load(data[0], self.model) 39 | 40 | 41 | class UserMapper(BaseMapper, PrimaryTrait): 42 | 43 | model = User 44 | table = 'users' 45 | 46 | def find(self, uid): 47 | """Find and load the user from database by uid(user id)""" 48 | data = (db.select(self.table).select('username', 'email', 'real_name', 49 | 'password', 'bio', 'status', 'role', 'uid'). 50 | condition('uid', uid).execute() 51 | ) 52 | if data: 53 | logger.info('data %s', data) 54 | return self.load(data[0], self.model) 55 | 56 | def find_by_username(self, username): 57 | """Return user by username if find in database otherwise None""" 58 | data = (db.select(self.table).select('username', 'email', 'real_name', 59 | 'password', 'bio', 'status', 'role', 'uid'). 60 | condition('username', username).execute() 61 | ) 62 | if data: 63 | return self.load(data[0], self.model) 64 | 65 | def create(self, user): 66 | return db.execute("INSERT INTO users(username, email, real_name, password, bio, status, role) \ 67 | VALUES(%s, %s, %s, %s, %s, %s, %s)", 68 | (user.username, user.email, user.real_name, user.password, user.bio, user.status, user.role)) 69 | 70 | def search(self, **kw): 71 | """Find the users match the condition in kw""" 72 | q = db.select(self.table).condition('status', 'active') 73 | for k, v in kw: 74 | q.condition(k, v) 75 | data = q.execute() 76 | users = [] 77 | for user in data: 78 | users.append(self.load(user, self.model)) 79 | return users 80 | 81 | def count(self): 82 | return db.query('SELECT COUNT(*) FROM ' + self.table)[0][0] 83 | 84 | def paginate(self, page=1, perpage=10): 85 | count = self.count() 86 | q = db.select(self.table).select('username', 'email', 'real_name', 87 | 'password', 'bio', 'status', 'role', 'uid') 88 | results = q.limit(perpage).offset((page - 1) * perpage).order_by('real_name', 'desc').execute() 89 | return [self.load(user, self.model) for user in results] 90 | 91 | def save(self, user): 92 | q = db.update(self.table) 93 | data = dict((_, getattr(user, _)) for _ in ('username', 'email', 'real_name', 94 | 'password', 'bio', 'status', 'role')) 95 | q.mset(data) 96 | return q.condition('uid', user.uid).execute() 97 | 98 | def delete(self, user): 99 | return db.delete(self.table).condition('uid', user.uid).execute() 100 | 101 | 102 | class PostMapper(BaseMapper): 103 | 104 | table = 'posts' 105 | model = Post 106 | 107 | def find(self, pid): 108 | data = db.select(self.table).fields('title', 'slug', 'description', 'html', 'css', 'js', 109 | 'category', 'status', 'comments', 'author', 'created', 'pid').condition('pid', pid).execute() 110 | if data: 111 | return self.load(data[0], self.model) 112 | 113 | def count(self): 114 | return db.select(self.table).fields(db.expr('COUNT(*)')).execute()[0][0] 115 | 116 | def create(self, post): 117 | row = [] 118 | for _ in ('title', 'slug', 'description', 'created', 'html', 'css', 'js', 119 | 'category', 'status', 'comments', 'author'): 120 | row.append(getattr(post, _)) 121 | return db.insert(self.table).columns('title', 'slug', 'description', 'created', 'html', 'css', 'js', 122 | 'category', 'status', 'comments', 'author').values(row).execute() 123 | 124 | def paginate(self, page=1, perpage=10, category=None): 125 | """Paginate the posts""" 126 | q = db.select(self.table).fields('title', 'slug', 'description', 'html', 'css', 'js', 127 | 'category', 'status', 'comments', 'author', 'created', 'pid') 128 | if category: 129 | q.condition('category', category) 130 | results = (q.limit(perpage).offset((page - 1) * perpage) 131 | .order_by('created', 'DESC').execute()) 132 | return [self.load(data, self.model) for data in results] 133 | 134 | def save(self, page): 135 | q = db.update(self.table) 136 | data = dict((_, getattr(page, _)) for _ in ('title', 'slug', 'description', 'html', 'css', 'js', 137 | 'category', 'status', 'comments')) 138 | q.mset(data) 139 | return q.condition('pid', page.pid).execute() 140 | 141 | def delete(self, page_id): 142 | return db.delete(self.table).condition('pid', page_id).execute() 143 | 144 | def category_count(self, category_id): 145 | return db.select(self.table).fields(db.expr('count(*)', 146 | 'total')).condition('category', category_id).condition('status', 'published').execute()[0][0] 147 | 148 | 149 | __backends = {} 150 | __backends['post'] = PostMapper() 151 | __backends['user'] = UserMapper() 152 | 153 | 154 | def Backend(name): 155 | return __backends.get(name) 156 | -------------------------------------------------------------------------------- /samples/schema.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP TABLE IF EXISTS `users`; 3 | CREATE TABLE `users` ( 4 | `uid` MEDIUMINT(8) unsigned NOT NULL AUTO_INCREMENT, 5 | `username` varchar(140) NOT NULL, 6 | `real_name` varchar(140) NOT NULL, 7 | `email` VARCHAR(140) NOT NULL, 8 | `password` CHAR(56) NOT NULL, 9 | `bio` text NOT NULL, 10 | `status` enum('inactive','active') NOT NULL, 11 | `role` enum('administrator','editor','user') NOT NULL, 12 | 13 | PRIMARY KEY (`uid`), 14 | UNIQUE KEY (`email`) 15 | 16 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 17 | 18 | 19 | DROP TABLE IF EXISTS `posts`; 20 | CREATE TABLE `posts` ( 21 | `pid` int(6) NOT NULL AUTO_INCREMENT, 22 | `title` varchar(150) NOT NULL, 23 | `slug` varchar(150) NOT NULL, 24 | `description` text NOT NULL, 25 | `html` text NOT NULL, 26 | `css` text NOT NULL, 27 | `js` text NOT NULL, 28 | `created` datetime NOT NULL, 29 | `author` int(6) NOT NULL, 30 | `category` int(6) NOT NULL, 31 | `status` enum('draft','published','archived') NOT NULL, 32 | `comments` tinyint(1) NOT NULL, 33 | 34 | PRIMARY KEY (`pid`), 35 | KEY `status` (`status`), 36 | KEY `slug` (`slug`) 37 | ) ENGINE=InnoDB CHARSET=utf8; -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal=1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | import sys 5 | from db import __version__ 6 | from codecs import open 7 | 8 | with open('README.rst', 'r', 'utf-8') as f: 9 | readme = f.read() 10 | 11 | setup( 12 | name='dbpy', 13 | version=__version__, 14 | author="Thomas Huang", 15 | author_email='lyanghwy@gmail.com', 16 | description="database abstraction layer for pythoneer", 17 | long_description=readme, 18 | license="GPL", 19 | keywords="database abstraction layer for pythoneer(orm, database)", 20 | url='https://github.com/whiteclover/dbpy', 21 | packages=find_packages(exclude=['samples', 'tests*']), 22 | zip_safe=False, 23 | include_package_data=True, 24 | install_requires=['setuptools'], 25 | test_suite='unittest', 26 | classifiers=[ 27 | 'Development Status :: 3 - Alpha', 28 | 'License :: OSI Approved :: GNU Affero General Public License v3', 29 | 'Natural Language :: English', 30 | 'Programming Language :: Python :: 3', 31 | 'Programming Language :: Python :: 3.4', 32 | 'Programming Language :: Python :: 3.5', 33 | 'Programming Language :: Python :: 3.6', 34 | 'Programming Language :: Python :: Implementation :: CPython', 35 | 'Programming Language :: Python :: Implementation :: PyPy', 36 | 'Operating System :: OS Independent', 37 | 'Topic :: Database' 38 | ] 39 | ) 40 | -------------------------------------------------------------------------------- /tests/dbt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import unittest 17 | import db 18 | import logging 19 | 20 | 21 | 22 | global config 23 | 24 | config = { 25 | 'passwd': 'test', 26 | 'user': 'test', 27 | 'host': 'localhost', 28 | 'db': 'test' 29 | } 30 | 31 | 32 | def _create(): 33 | db.execute('DROP TABLE IF EXISTS `users`') 34 | db.execute("""CREATE TABLE `users` ( 35 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT, 36 | `name` varchar(20), 37 | PRIMARY KEY (`uid`))""") 38 | 39 | class TestDBBase(unittest.TestCase): 40 | 41 | def setUp(self): 42 | setattr(db, '__db', {}) 43 | 44 | def test_dup_key(self): 45 | db.setup(config) 46 | f = lambda: db.setup(config) 47 | self.assertRaises(db.DBError, f) 48 | 49 | def test_invalid_key(self): 50 | f = lambda: db.setup(config, key='dd.xx') 51 | 52 | self.assertRaises(TypeError, f) 53 | 54 | 55 | def test_database(self): 56 | db.setup(config) 57 | self.assertEqual(db.database(), db.database('default', slave=True)) 58 | conns = getattr(db, '__db', []) 59 | self.assertEqual(len(conns['default.slave']), 1) 60 | 61 | db.setup(config, slave=True) 62 | self.assertNotEqual(db.database(), db.database('default', slave=True)) 63 | conns = getattr(db, '__db', []) 64 | self.assertEqual(len(conns['default.slave']), 1) 65 | 66 | db.setup(config, slave=True) 67 | conns = getattr(db, '__db', []) 68 | self.assertEqual(len(conns['default.slave']), 2) 69 | 70 | class TestBase(unittest.TestCase): 71 | 72 | def setUp(self): 73 | setattr(db, '__db', {}) 74 | db.setup(config) 75 | db.execute('DROP TABLE IF EXISTS `users`') 76 | db.execute("""CREATE TABLE `users` ( 77 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT, 78 | `name` varchar(20) NOT NULL, 79 | PRIMARY KEY (`uid`))""") 80 | 81 | def test_query(self): 82 | self.assertEqual(1, db.query('SELECT 1')[0][0]) 83 | self.assertEqual(0, len(db.query('SELECT * FROM users'))) 84 | 85 | def test_execute(self): 86 | res = db.execute('INSERT INTO users VALUES(%s, %s)', [(10, 'execute_test'), (9, 'execute_test')]) 87 | self.assertTrue(res) 88 | res = db.execute('DELETE FROM users WHERE name=%s', ('execute_test',)) 89 | self.assertEqual(res, 2) 90 | 91 | def test_pool(self): 92 | import threading 93 | 94 | def q(n): 95 | for i in range(10): 96 | res = db.query('select count(*) from users') 97 | self.assertEqual(0, res[0][0]) 98 | n = 50 99 | ts = [] 100 | for i in range(n): 101 | t = threading.Thread(target=q, args=(i,)) 102 | ts.append(t) 103 | for t in ts: 104 | t.start() 105 | for t in ts: 106 | t.join() 107 | 108 | 109 | 110 | class TestMultilDB(unittest.TestCase): 111 | 112 | def setUp(self): 113 | setattr(db, '__db', {}) 114 | db.setup(config, key='test') 115 | db.setup(config, key='test', slave=True) 116 | db.execute('DROP TABLE IF EXISTS `users`', key='test') 117 | db.execute("""CREATE TABLE `users` ( 118 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT, 119 | `name` varchar(20) NOT NULL, 120 | PRIMARY KEY (`uid`))""", key='test') 121 | rows = [] 122 | for _ in range(1, 10): 123 | rows.append('(%d , "name_%d")' % (_, _)) 124 | db.execute('INSERT INTO users VALUES ' + ', '.join(rows), key='test') 125 | 126 | def tearDown(self): 127 | db.execute('DELETE FROM users', key='test') 128 | 129 | 130 | def test_excute(self): 131 | res = db.execute('insert into users values(%s, %s)', [(10L, 'thomas'), (11L, 'animer')], key='test') 132 | res = db.query('SELECT count(*) FROM users WHERE uid>=10', key='test') 133 | self.assertEqual(2, res[0][0]) 134 | 135 | def test_query(self): 136 | res = db.query('select name from users limit 5', key='test') 137 | self.assertEqual(len(res), 5) 138 | res = db.query('select name from users limit %s', (100,), many=20, key='test') 139 | rows = [] 140 | for r in res: 141 | rows.append(r) 142 | self.assertTrue(10, len(rows)) 143 | 144 | class TestSelectQuery(unittest.TestCase): 145 | 146 | def setUp(self): 147 | setattr(db, '__db', {}) 148 | db.setup(config) 149 | _create() 150 | users = [] 151 | for i in range(1, 5): 152 | users.append((i, 'user_' + str(i))) 153 | users.append((5, None)) 154 | db.execute('INSERT INTO users VALUES(%s, %s)', users) 155 | self.select = db.select('users') 156 | 157 | def tearDown(self): 158 | db.execute('delete from users') 159 | 160 | 161 | def test_select_all(self): 162 | self.assertEquals(len(self.select 163 | .execute()), 5) 164 | 165 | 166 | def test_select_as_dict(self): 167 | res = self.select.condition('uid', 1).execute(as_dict=True) 168 | self.assertEqual(len(res), 1) 169 | self.assertEqual(type(res[0]), dict) 170 | self.assertEqual(res[0]['uid'], 1) 171 | 172 | def test_select_many(self): 173 | res = (self.select.fields('*') 174 | .execute(many=2)) 175 | rows = [] 176 | for row in res: 177 | rows.append(row) 178 | 179 | self.assertEquals(len(rows), 5) 180 | 181 | def test_select_condition(self): 182 | res = (self.select 183 | .condition('name', 'user_1') 184 | .condition('uid', 1) 185 | .execute()) 186 | 187 | self.assertEquals(res[0][1], 'user_1') 188 | 189 | def test_select_or_condition(self): 190 | from db import or_ 191 | or_con = or_() 192 | or_con.condition('name', 'user_1') 193 | or_con.condition('name', 'user_2') 194 | res = (self.select 195 | .condition(or_con) 196 | .execute()) 197 | 198 | self.assertEquals(res[0][1], 'user_1') 199 | 200 | def test_select_like(self): 201 | res = (self.select 202 | .condition('name', 'user_%', 'like') 203 | .execute()) 204 | self.assertEquals(len(res), 4) 205 | 206 | def test_select_in(self): 207 | res = (self.select.fields('*') 208 | .condition('name', ['user_1', 'user_2']) 209 | .execute()) 210 | self.assertEquals(res[0][1], 'user_1') 211 | self.assertEquals(res[1][1], 'user_2') 212 | 213 | def test_select_group_by(self): 214 | self.assertEquals(len(self.select 215 | .group_by('name', 'uid') 216 | .execute()), 5) 217 | 218 | def test_select_order_by_ASC(self): 219 | 220 | self.assertEquals(len(self.select 221 | .order_by('name') 222 | .execute()), 5) 223 | 224 | def test_select_order_by_DESC(self): 225 | 226 | self.assertEquals(len(self.select 227 | .order_by('name', 'DESC') 228 | .execute()), 5) 229 | 230 | def test_select_limit(self): 231 | self.assertEquals(len(self.select.limit(2).execute()), 2) 232 | 233 | def test_table_dot_condition(self): 234 | res = self.select.condition('users.uid', 5).execute() 235 | self.assertEqual(res[0][0], 5) 236 | 237 | def test_is_null(self): 238 | res = self.select.is_null('name').condition('uid', 5).execute() 239 | self.assertEqual(res[0][0], 5) 240 | 241 | def test_is_not_null(self): 242 | self.assertEqual(len(self.select.is_not_null('uid').execute()), 5) 243 | 244 | def test_expr(self): 245 | from db import expr 246 | res = self.select.fields(expr('count(*)')).execute() 247 | self.assertEqual(res[0][0], 5) 248 | res = db.select('users').fields(expr('count(uid)', 'total')).execute() 249 | self.assertEqual(res[0][0], 5) 250 | 251 | 252 | class TestUpdateQuery(unittest.TestCase): 253 | 254 | def setUp(self): 255 | setattr(db, '__db', {}) 256 | db.setup(config) 257 | _create() 258 | users = [] 259 | for i in range(1, 6): 260 | users.append((i, 'user_' + str(i))) 261 | db.execute('delete from users') 262 | db.execute('INSERT INTO users VALUES(%s, %s)', users) 263 | self.update = db.update('users') 264 | 265 | def tearDown(self): 266 | db.execute('delete from users') 267 | 268 | def test_update_on_name(self): 269 | res = (self.update. 270 | mset({'name':'update_test'}) 271 | .condition('name','user_1') 272 | .execute()) 273 | self.assertEquals(res, 1) 274 | 275 | 276 | def test_update_on_name_and_uid(self): 277 | res = (self.update. 278 | set('name', 'update_test') 279 | .condition('name', 'user_2') 280 | .condition('uid', 2) 281 | .execute()) 282 | self.assertEquals(res, 1) 283 | 284 | def test_update_not_exists(self): 285 | res = (self.update. 286 | mset({'name':'update', 'uid': 10}) 287 | .condition('name', 'not_exists') 288 | .execute()) 289 | self.assertEquals(res, 0) 290 | 291 | class TestInsertQuery(unittest.TestCase): 292 | 293 | def setUp(self): 294 | setattr(db, '__db', {}) 295 | db.setup(config) 296 | _create() 297 | users = [] 298 | for i in range(1, 6): 299 | users.append((i, 'user_' + str(i))) 300 | db.execute('delete from users') 301 | db.execute('INSERT INTO users VALUES(%s, %s)', users) 302 | self.insert = db.insert('users') 303 | self.select = db.select('users') 304 | 305 | def tearDown(self): 306 | db.execute('delete from users') 307 | 308 | def test_insert(self): 309 | res = self.insert.values((10, 'test_insert')).execute() 310 | res = self.select.condition('name', 'test_insert').execute() 311 | self.assertEqual(res[0][1], 'test_insert') 312 | 313 | def test_insert_dict_values(self): 314 | self.insert.fields('name').values({'name': 'insert_1'}).values(('insert_2',)).execute() 315 | res = self.select.condition('name', ['insert_1', 'insert_2']).execute() 316 | self.assertEqual(len(res), 2) 317 | 318 | class TestDeleteQuery(unittest.TestCase): 319 | 320 | def setUp(self): 321 | setattr(db, '__db', {}) 322 | db.setup(config) 323 | _create() 324 | users = [] 325 | for i in range(1, 6): 326 | users.append((i, 'user_' + str(i))) 327 | db.execute('INSERT INTO users VALUES(%s, %s)', users) 328 | self.delete = db.delete('users') 329 | 330 | def tearDown(self): 331 | db.execute('delete from users') 332 | 333 | def test_delete_by_uid(self): 334 | res = self.delete.condition('uid', 1).execute() 335 | self.assertEqual(res, 1) 336 | 337 | def test_delete_by_condtions(self): 338 | res = self.delete.condition('uid', 2).condition('name', 'user_2').execute() 339 | self.assertEqual(res, 1) 340 | 341 | def test_delete_or_condtions(self): 342 | from db import or_ 343 | or_con = or_().condition('name', 'user_1').condition('name', 'user_2') 344 | res = self.delete.condition(or_con).execute() 345 | self.assertEqual(res, 2) 346 | 347 | 348 | class TestTransaction(unittest.TestCase): 349 | 350 | def setUp(self): 351 | setattr(db, '__db', {}) 352 | db.setup(config) 353 | _create() 354 | users = [] 355 | for i in range(1, 6): 356 | users.append((i, 'user_' + str(i))) 357 | db.execute('INSERT INTO users VALUES(%s, %s)', users) 358 | 359 | 360 | def tearDown(self): 361 | db.execute('delete from users') 362 | 363 | def test_with(self): 364 | with db.transaction() as t: 365 | t.delete('users').condition('uid', 1).execute() 366 | res = db.select('users').condition('uid', 1).execute() 367 | self.assertEqual(len(res), 1) 368 | res = db.select('users').condition('uid', 1).execute() 369 | self.assertEqual(len(res), 0) 370 | 371 | def test_begin_commit(self): 372 | t = db.transaction() 373 | t.begin() 374 | t.delete('users').condition('uid', 1).execute() 375 | res = db.select('users').condition('uid', 1).execute() 376 | self.assertEqual(len(res), 1) 377 | t.commit() 378 | res = db.select('users').condition('uid', 1).execute() 379 | self.assertEqual(len(res), 0) 380 | 381 | if __name__ == '__main__': 382 | debug = True 383 | level = logging.DEBUG if debug else logging.INFO 384 | logging.basicConfig(level=level, 385 | format='%(asctime)s %(levelname)-8s %(message)s', 386 | datefmt='%Y-%m-%d %H:%M:%S', filemode='a+') 387 | unittest.main(verbosity=2 if debug else 0) -------------------------------------------------------------------------------- /tests/pymysqlt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2014-2015 Thomas Huang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, version 2 of the License. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import unittest 17 | import db 18 | import logging 19 | 20 | 21 | 22 | global config 23 | 24 | config = { 25 | 'passwd': 'test', 26 | 'user': 'test', 27 | 'host': 'localhost', 28 | 'db': 'test' 29 | } 30 | 31 | 32 | def _create(): 33 | db.execute('DROP TABLE IF EXISTS `users`') 34 | db.execute("""CREATE TABLE `users` ( 35 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT, 36 | `name` varchar(20), 37 | PRIMARY KEY (`uid`))""") 38 | 39 | class TestDBBase(unittest.TestCase): 40 | 41 | def setUp(self): 42 | setattr(db, '__db', {}) 43 | 44 | def test_dup_key(self): 45 | db.setup(config,adapter='pymysql') 46 | f = lambda: db.setup(config,adapter='pymysql') 47 | self.assertRaises(db.DBError, f) 48 | 49 | def test_invalid_key(self): 50 | f = lambda: db.setup(config, key='dd.xx') 51 | 52 | self.assertRaises(TypeError, f) 53 | 54 | 55 | def test_database(self): 56 | db.setup(config,adapter='pymysql') 57 | self.assertEqual(db.database(), db.database('default', slave=True)) 58 | conns = getattr(db, '__db', []) 59 | self.assertEqual(len(conns['default.slave']), 1) 60 | 61 | db.setup(config, slave=True) 62 | self.assertNotEqual(db.database(), db.database('default', slave=True)) 63 | conns = getattr(db, '__db', []) 64 | self.assertEqual(len(conns['default.slave']), 1) 65 | 66 | db.setup(config, slave=True) 67 | conns = getattr(db, '__db', []) 68 | self.assertEqual(len(conns['default.slave']), 2) 69 | 70 | class TestBase(unittest.TestCase): 71 | 72 | def setUp(self): 73 | setattr(db, '__db', {}) 74 | db.setup(config,adapter='pymysql') 75 | db.execute('DROP TABLE IF EXISTS `users`') 76 | db.execute("""CREATE TABLE `users` ( 77 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT, 78 | `name` varchar(20) NOT NULL, 79 | PRIMARY KEY (`uid`))""") 80 | 81 | def test_query(self): 82 | self.assertEqual(1, db.query('SELECT 1')[0][0]) 83 | self.assertEqual(0, len(db.query('SELECT * FROM users'))) 84 | 85 | def test_execute(self): 86 | res = db.execute('INSERT INTO users VALUES(%s, %s)', [(10, 'execute_test'), (9, 'execute_test')]) 87 | self.assertTrue(res) 88 | res = db.execute('DELETE FROM users WHERE name=%s', ('execute_test',)) 89 | self.assertEqual(res, 2) 90 | 91 | def test_pool(self): 92 | import threading 93 | 94 | def q(n): 95 | for i in range(10): 96 | res = db.query('select count(*) from users') 97 | self.assertEqual(0, res[0][0]) 98 | n = 50 99 | ts = [] 100 | for i in range(n): 101 | t = threading.Thread(target=q, args=(i,)) 102 | ts.append(t) 103 | for t in ts: 104 | t.start() 105 | for t in ts: 106 | t.join() 107 | 108 | 109 | 110 | class TestMultilDB(unittest.TestCase): 111 | 112 | def setUp(self): 113 | setattr(db, '__db', {}) 114 | db.setup(config, key='test') 115 | db.setup(config, key='test', slave=True) 116 | db.execute('DROP TABLE IF EXISTS `users`', key='test') 117 | db.execute("""CREATE TABLE `users` ( 118 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT, 119 | `name` varchar(20) NOT NULL, 120 | PRIMARY KEY (`uid`))""", key='test') 121 | rows = [] 122 | for _ in range(1, 10): 123 | rows.append('(%d , "name_%d")' % (_, _)) 124 | db.execute('INSERT INTO users VALUES ' + ', '.join(rows), key='test') 125 | 126 | def tearDown(self): 127 | db.execute('DELETE FROM users', key='test') 128 | 129 | 130 | def test_excute(self): 131 | res = db.execute('insert into users values(%s, %s)', [(10L, 'thomas'), (11L, 'animer')], key='test') 132 | res = db.query('SELECT count(*) FROM users WHERE uid>=10', key='test') 133 | self.assertEqual(2, res[0][0]) 134 | 135 | def test_query(self): 136 | res = db.query('select name from users limit 5', key='test') 137 | self.assertEqual(len(res), 5) 138 | res = db.query('select name from users limit %s', (100,), many=20, key='test') 139 | rows = [] 140 | for r in res: 141 | rows.append(r) 142 | self.assertTrue(10, len(rows)) 143 | 144 | class TestSelectQuery(unittest.TestCase): 145 | 146 | def setUp(self): 147 | setattr(db, '__db', {}) 148 | db.setup(config,adapter='pymysql') 149 | _create() 150 | users = [] 151 | for i in range(1, 5): 152 | users.append((i, 'user_' + str(i))) 153 | users.append((5, None)) 154 | db.execute('INSERT INTO users VALUES(%s, %s)', users) 155 | self.select = db.select('users') 156 | 157 | def tearDown(self): 158 | db.execute('delete from users') 159 | 160 | 161 | def test_select_all(self): 162 | self.assertEquals(len(self.select 163 | .execute()), 5) 164 | 165 | 166 | def test_select_as_dict(self): 167 | res = self.select.condition('uid', 1).execute(as_dict=True) 168 | self.assertEqual(len(res), 1) 169 | self.assertEqual(type(res[0]), dict) 170 | self.assertEqual(res[0]['uid'], 1) 171 | 172 | def test_select_many(self): 173 | res = (self.select.fields('*') 174 | .execute(many=2)) 175 | rows = [] 176 | for row in res: 177 | rows.append(row) 178 | 179 | self.assertEquals(len(rows), 5) 180 | 181 | def test_select_condition(self): 182 | res = (self.select 183 | .condition('name', 'user_1') 184 | .condition('uid', 1) 185 | .execute()) 186 | 187 | self.assertEquals(res[0][1], 'user_1') 188 | 189 | def test_select_or_condition(self): 190 | from db import or_ 191 | or_con = or_() 192 | or_con.condition('name', 'user_1') 193 | or_con.condition('name', 'user_2') 194 | res = (self.select 195 | .condition(or_con) 196 | .execute()) 197 | 198 | self.assertEquals(res[0][1], 'user_1') 199 | 200 | def test_select_like(self): 201 | res = (self.select 202 | .condition('name', 'user_%', 'like') 203 | .execute()) 204 | self.assertEquals(len(res), 4) 205 | 206 | def test_select_in(self): 207 | res = (self.select.fields('*') 208 | .condition('name', ['user_1', 'user_2']) 209 | .execute()) 210 | self.assertEquals(res[0][1], 'user_1') 211 | self.assertEquals(res[1][1], 'user_2') 212 | 213 | def test_select_group_by(self): 214 | self.assertEquals(len(self.select 215 | .group_by('name', 'uid') 216 | .execute()), 5) 217 | 218 | def test_select_order_by_ASC(self): 219 | 220 | self.assertEquals(len(self.select 221 | .order_by('name') 222 | .execute()), 5) 223 | 224 | def test_select_order_by_DESC(self): 225 | 226 | self.assertEquals(len(self.select 227 | .order_by('name', 'DESC') 228 | .execute()), 5) 229 | 230 | def test_select_limit(self): 231 | self.assertEquals(len(self.select.limit(2).execute()), 2) 232 | 233 | def test_table_dot_condition(self): 234 | res = self.select.condition('users.uid', 5).execute() 235 | self.assertEqual(res[0][0], 5) 236 | 237 | def test_is_null(self): 238 | res = self.select.is_null('name').condition('uid', 5).execute() 239 | self.assertEqual(res[0][0], 5) 240 | 241 | def test_is_not_null(self): 242 | self.assertEqual(len(self.select.is_not_null('uid').execute()), 5) 243 | 244 | def test_expr(self): 245 | from db import expr 246 | res = self.select.fields(expr('count(*)')).execute() 247 | self.assertEqual(res[0][0], 5) 248 | res = db.select('users').fields(expr('count(uid)', 'total')).execute() 249 | self.assertEqual(res[0][0], 5) 250 | 251 | 252 | class TestUpdateQuery(unittest.TestCase): 253 | 254 | def setUp(self): 255 | setattr(db, '__db', {}) 256 | db.setup(config,adapter='pymysql') 257 | _create() 258 | users = [] 259 | for i in range(1, 6): 260 | users.append((i, 'user_' + str(i))) 261 | db.execute('delete from users') 262 | db.execute('INSERT INTO users VALUES(%s, %s)', users) 263 | self.update = db.update('users') 264 | 265 | def tearDown(self): 266 | db.execute('delete from users') 267 | 268 | def test_update_on_name(self): 269 | res = (self.update. 270 | mset({'name':'update_test'}) 271 | .condition('name','user_1') 272 | .execute()) 273 | self.assertEquals(res, 1) 274 | 275 | 276 | def test_update_on_name_and_uid(self): 277 | res = (self.update. 278 | set('name', 'update_test') 279 | .condition('name', 'user_2') 280 | .condition('uid', 2) 281 | .execute()) 282 | self.assertEquals(res, 1) 283 | 284 | def test_update_not_exists(self): 285 | res = (self.update. 286 | mset({'name':'update', 'uid': 10}) 287 | .condition('name', 'not_exists') 288 | .execute()) 289 | self.assertEquals(res, 0) 290 | 291 | class TestInsertQuery(unittest.TestCase): 292 | 293 | def setUp(self): 294 | setattr(db, '__db', {}) 295 | db.setup(config,adapter='pymysql') 296 | _create() 297 | users = [] 298 | for i in range(1, 6): 299 | users.append((i, 'user_' + str(i))) 300 | db.execute('delete from users') 301 | db.execute('INSERT INTO users VALUES(%s, %s)', users) 302 | self.insert = db.insert('users') 303 | self.select = db.select('users') 304 | 305 | def tearDown(self): 306 | db.execute('delete from users') 307 | 308 | def test_insert(self): 309 | res = self.insert.values((10, 'test_insert')).execute() 310 | res = self.select.condition('name', 'test_insert').execute() 311 | self.assertEqual(res[0][1], 'test_insert') 312 | 313 | def test_insert_dict_values(self): 314 | self.insert.fields('name').values({'name': 'insert_1'}).values(('insert_2',)).execute() 315 | res = self.select.condition('name', ['insert_1', 'insert_2']).execute() 316 | self.assertEqual(len(res), 2) 317 | 318 | class TestDeleteQuery(unittest.TestCase): 319 | 320 | def setUp(self): 321 | setattr(db, '__db', {}) 322 | db.setup(config,adapter='pymysql') 323 | _create() 324 | users = [] 325 | for i in range(1, 6): 326 | users.append((i, 'user_' + str(i))) 327 | db.execute('INSERT INTO users VALUES(%s, %s)', users) 328 | self.delete = db.delete('users') 329 | 330 | def tearDown(self): 331 | db.execute('delete from users') 332 | 333 | def test_delete_by_uid(self): 334 | res = self.delete.condition('uid', 1).execute() 335 | self.assertEqual(res, 1) 336 | 337 | def test_delete_by_condtions(self): 338 | res = self.delete.condition('uid', 2).condition('name', 'user_2').execute() 339 | self.assertEqual(res, 1) 340 | 341 | def test_delete_or_condtions(self): 342 | from db import or_ 343 | or_con = or_().condition('name', 'user_1').condition('name', 'user_2') 344 | res = self.delete.condition(or_con).execute() 345 | self.assertEqual(res, 2) 346 | 347 | 348 | class TestTransaction(unittest.TestCase): 349 | 350 | def setUp(self): 351 | setattr(db, '__db', {}) 352 | db.setup(config,adapter='pymysql') 353 | _create() 354 | users = [] 355 | for i in range(1, 6): 356 | users.append((i, 'user_' + str(i))) 357 | db.execute('INSERT INTO users VALUES(%s, %s)', users) 358 | 359 | 360 | def tearDown(self): 361 | db.execute('delete from users') 362 | 363 | def test_with(self): 364 | with db.transaction() as t: 365 | t.delete('users').condition('uid', 1).execute() 366 | res = db.select('users').condition('uid', 1).execute() 367 | self.assertEqual(len(res), 1) 368 | res = db.select('users').condition('uid', 1).execute() 369 | self.assertEqual(len(res), 0) 370 | 371 | def test_begin_commit(self): 372 | t = db.transaction() 373 | t.begin() 374 | t.delete('users').condition('uid', 1).execute() 375 | res = db.select('users').condition('uid', 1).execute() 376 | self.assertEqual(len(res), 1) 377 | t.commit() 378 | res = db.select('users').condition('uid', 1).execute() 379 | self.assertEqual(len(res), 0) 380 | 381 | if __name__ == '__main__': 382 | debug = True 383 | level = logging.DEBUG if debug else logging.INFO 384 | logging.basicConfig(level=level, 385 | format='%(asctime)s %(levelname)-8s %(message)s', 386 | datefmt='%Y-%m-%d %H:%M:%S', filemode='a+') 387 | unittest.main(verbosity=2 if debug else 0) -------------------------------------------------------------------------------- /tests/test.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE TABLE `users` ( 3 | `uid` int(11) unsigned NOT NULL AUTO_INCREMENT, 4 | `name` varchar(60) DEFAULT NULL, 5 | PRIMARY KEY (`uid`) 6 | ); --------------------------------------------------------------------------------