├── .github └── workflows │ └── tests.yaml ├── .gitignore ├── .readthedocs.yaml ├── .travis.yml ├── .travis_deps.sh ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── TODO.rst ├── bench.py ├── docs ├── Makefile ├── _static │ └── peewee-white.png ├── _themes │ └── flask │ │ ├── layout.html │ │ ├── relations.html │ │ ├── static │ │ ├── flasky.css_t │ │ └── small_flask.css │ │ └── theme.conf ├── conf.py ├── crdb.png ├── index.rst ├── make.bat ├── mariadb.png ├── mysql.png ├── peewee-logo.png ├── peewee-white.png ├── peewee │ ├── api.rst │ ├── changes.rst │ ├── contributing.rst │ ├── crdb.rst │ ├── database.rst │ ├── example.rst │ ├── hacks.rst │ ├── installation.rst │ ├── interactive.rst │ ├── models.rst │ ├── playhouse.rst │ ├── query_builder.rst │ ├── query_examples.rst │ ├── query_operators.rst │ ├── querying.rst │ ├── quickstart.rst │ ├── relationships.rst │ ├── schema-horizontal.png │ ├── schema.jpg │ ├── sqlite_ext.rst │ └── tweepee.jpg ├── peewee3-logo.png ├── postgresql.png ├── requirements.txt └── sqlite.png ├── examples ├── adjacency_list.py ├── analytics │ ├── app.py │ ├── reports.py │ ├── requirements.txt │ └── run_example.py ├── anomaly_detection.py ├── blog │ ├── app.py │ ├── requirements.txt │ ├── static │ │ ├── css │ │ │ ├── blog.min.css │ │ │ └── hilite.css │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ └── glyphicons-halflings-regular.woff │ │ ├── js │ │ │ ├── bootstrap.min.js │ │ │ └── jquery-1.11.0.min.js │ │ └── robots.txt │ └── templates │ │ ├── base.html │ │ ├── create.html │ │ ├── detail.html │ │ ├── edit.html │ │ ├── includes │ │ └── pagination.html │ │ ├── index.html │ │ ├── login.html │ │ └── logout.html ├── diary.py ├── graph.py ├── hexastore.py ├── reddit_ranking.py ├── sqlite_fts_compression.py └── twitter │ ├── app.py │ ├── requirements.txt │ ├── run_example.py │ ├── static │ └── style.css │ └── templates │ ├── create.html │ ├── homepage.html │ ├── includes │ ├── message.html │ └── pagination.html │ ├── join.html │ ├── layout.html │ ├── login.html │ ├── private_messages.html │ ├── public_messages.html │ ├── user_detail.html │ ├── user_followers.html │ ├── user_following.html │ └── user_list.html ├── peewee.py ├── playhouse ├── README.md ├── __init__.py ├── _pysqlite │ ├── cache.h │ ├── connection.h │ └── module.h ├── _sqlite_ext.pyx ├── _sqlite_udf.pyx ├── apsw_ext.py ├── cockroachdb.py ├── dataset.py ├── db_url.py ├── fields.py ├── flask_utils.py ├── hybrid.py ├── kv.py ├── migrate.py ├── mysql_ext.py ├── pool.py ├── postgres_ext.py ├── psycopg3_ext.py ├── reflection.py ├── shortcuts.py ├── signals.py ├── sqlcipher_ext.py ├── sqlite_changelog.py ├── sqlite_ext.py ├── sqlite_udf.py ├── sqliteq.py └── test_utils.py ├── pwiz.py ├── pyproject.toml ├── runtests.py ├── setup.py └── tests ├── __init__.py ├── __main__.py ├── apsw_ext.py ├── base.py ├── base_models.py ├── cockroachdb.py ├── cysqlite.py ├── dataset.py ├── db_tests.py ├── db_url.py ├── expressions.py ├── extra_fields.py ├── fields.py ├── hybrid.py ├── keys.py ├── kv.py ├── libs ├── __init__.py └── mock.py ├── manytomany.py ├── migrations.py ├── model_save.py ├── model_sql.py ├── models.py ├── mysql_ext.py ├── pool.py ├── postgres.py ├── postgres_helpers.py ├── prefetch_tests.py ├── psycopg3_ext.py ├── pwiz_integration.py ├── queries.py ├── reflection.py ├── regressions.py ├── results.py ├── returning.py ├── schema.py ├── shortcuts.py ├── signals.py ├── sql.py ├── sqlcipher_ext.py ├── sqlite.py ├── sqlite_changelog.py ├── sqlite_helpers.py ├── sqlite_udf.py ├── sqliteq.py ├── test_utils.py └── transactions.py /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push] 3 | jobs: 4 | tests: 5 | name: ${{ matrix.peewee-backend }} - ${{ matrix.python-version }} 6 | runs-on: ubuntu-latest 7 | timeout-minutes: 15 8 | services: 9 | mysql: 10 | image: mariadb:latest 11 | env: 12 | MYSQL_ROOT_PASSWORD: peewee 13 | MYSQL_DATABASE: peewee_test 14 | ports: 15 | - 3306:3306 16 | postgres: 17 | image: postgres 18 | env: 19 | POSTGRES_USER: postgres 20 | POSTGRES_PASSWORD: peewee 21 | POSTGRES_DB: peewee_test 22 | ports: 23 | - 5432:5432 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | python-version: [3.8, "3.11", "3.13"] 28 | peewee-backend: 29 | - "sqlite" 30 | - "postgresql" 31 | - "mysql" 32 | include: 33 | - python-version: "3.9" 34 | peewee-backend: sqlite 35 | - python-version: "3.11" 36 | peewee-backend: cockroachdb 37 | - python-version: "3.11" 38 | peewee-backend: psycopg3 39 | - python-version: "3.13" 40 | peewee-backend: psycopg3 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: actions/setup-python@v5 44 | with: 45 | python-version: ${{ matrix.python-version }} 46 | - name: deps 47 | env: 48 | PGUSER: postgres 49 | PGHOST: 127.0.0.1 50 | PGPASSWORD: peewee 51 | run: | 52 | sudo apt-get install libsqlite3-dev 53 | pip install setuptools psycopg2-binary cython pymysql 'apsw' mysql-connector sqlcipher3-binary 'psycopg[binary]' pysqlite3 54 | python setup.py build_ext -i 55 | psql peewee_test -c 'CREATE EXTENSION hstore;' 56 | - name: crdb 57 | if: ${{ matrix.peewee-backend == 'cockroachdb' }} 58 | run: | 59 | wget -qO- https://binaries.cockroachdb.com/cockroach-v22.2.6.linux-amd64.tgz | tar xz 60 | ./cockroach-v22.2.6.linux-amd64/cockroach start-single-node --insecure --background 61 | ./cockroach-v22.2.6.linux-amd64/cockroach sql --insecure -e 'create database peewee_test;' 62 | - name: runtests ${{ matrix.peewee-backend }} - ${{ matrix.python-version }} 63 | env: 64 | PEEWEE_TEST_BACKEND: ${{ matrix.peewee-backend }} 65 | PGUSER: postgres 66 | PGHOST: 127.0.0.1 67 | PGPASSWORD: peewee 68 | run: python runtests.py --mysql-user=root --mysql-password=peewee -s -v2 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build 3 | prof/ 4 | docs/_build/ 5 | playhouse/*.c 6 | playhouse/*.h 7 | playhouse/*.so 8 | playhouse/tests/peewee_test.db 9 | .idea/ 10 | MANIFEST 11 | peewee_test.db 12 | closure.so 13 | lsm.so 14 | regexp.so 15 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | python: 3 | install: 4 | - requirements: docs/requirements.txt 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.11" 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.4 5 | - 3.5 6 | - 3.6 7 | env: 8 | - PEEWEE_TEST_BACKEND=sqlite 9 | - PEEWEE_TEST_BACKEND=postgresql 10 | - PEEWEE_TEST_BACKEND=mysql 11 | matrix: 12 | include: 13 | - python: 3.7 14 | dist: xenial 15 | env: PEEWEE_TEST_BACKEND=sqlite 16 | - python: 3.7 17 | dist: xenial 18 | env: PEEWEE_TEST_BACKEND=postgresql 19 | - python: 3.7 20 | dist: xenial 21 | env: PEEWEE_TEST_BACKEND=mysql 22 | - python: 3.8 23 | dist: xenial 24 | - python: 3.7 25 | dist: xenial 26 | env: 27 | - PEEWEE_TEST_BUILD_SQLITE=1 28 | - PEEWEE_CLOSURE_EXTENSION=/usr/local/lib/closure.so 29 | - LSM_EXTENSION=/usr/local/lib/lsm.so 30 | before_install: 31 | - sudo apt-get install -y tcl-dev 32 | - ./.travis_deps.sh 33 | - sudo ldconfig 34 | script: "python runtests.py -v2" 35 | - python: 3.7 36 | dist: xenial 37 | env: 38 | - PEEWEE_TEST_BACKEND=cockroachdb 39 | before_install: 40 | - wget -qO- https://binaries.cockroachdb.com/cockroach-v20.1.1.linux-amd64.tgz | tar xvz 41 | - ./cockroach-v20.1.1.linux-amd64/cockroach start --insecure --background 42 | - ./cockroach-v20.1.1.linux-amd64/cockroach sql --insecure -e 'create database peewee_test;' 43 | allow_failures: 44 | addons: 45 | postgresql: "9.6" 46 | mariadb: "10.3" 47 | services: 48 | - postgresql 49 | - mariadb 50 | install: "pip install psycopg2-binary Cython pymysql apsw mysql-connector" 51 | before_script: 52 | - python setup.py build_ext -i 53 | - psql -c 'drop database if exists peewee_test;' -U postgres 54 | - psql -c 'create database peewee_test;' -U postgres 55 | - psql peewee_test -c 'create extension hstore;' -U postgres 56 | - mysql -e 'drop user if exists travis@localhost;' 57 | - mysql -e 'create user travis@localhost;' 58 | - mysql -e 'drop database if exists peewee_test;' 59 | - mysql -e 'create database peewee_test;' 60 | - mysql -e 'grant all on *.* to travis@localhost;' || true 61 | script: "python runtests.py" 62 | -------------------------------------------------------------------------------- /.travis_deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | setup_sqlite_deps() { 4 | wget https://www.sqlite.org/src/tarball/sqlite.tar.gz 5 | tar xzf sqlite.tar.gz 6 | cd sqlite/ 7 | export CFLAGS="-DSQLITE_ENABLE_FTS3 \ 8 | -DSQLITE_ENABLE_FTS3_PARENTHESIS \ 9 | -DSQLITE_ENABLE_FTS4 \ 10 | -DSQLITE_ENABLE_FTS5 \ 11 | -DSQLITE_ENABLE_JSON1 \ 12 | -DSQLITE_ENABLE_LOAD_EXTENSION \ 13 | -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT \ 14 | -DSQLITE_TEMP_STORE=3 \ 15 | -DSQLITE_USE_URI \ 16 | -O2 \ 17 | -fPIC" 18 | export PREFIX="/usr/local" 19 | LIBS="-lm" ./configure \ 20 | --disable-tcl \ 21 | --enable-shared \ 22 | --enable-tempstore=always \ 23 | --prefix="$PREFIX" 24 | make && sudo make install 25 | 26 | cd ext/misc/ 27 | 28 | # Build the transitive closure extension and copy shared library. 29 | gcc -fPIC -O2 -lsqlite3 -shared closure.c -o closure.so 30 | sudo cp closure.so /usr/local/lib 31 | 32 | # Build the lsm1 extension and copy shared library. 33 | cd ../lsm1 34 | export CFLAGS="-fPIC -O2" 35 | TCCX="gcc -fPIC -O2" make lsm.so 36 | sudo cp lsm.so /usr/local/lib 37 | } 38 | 39 | if [ -n "$PEEWEE_TEST_BUILD_SQLITE" ]; then 40 | setup_sqlite_deps 41 | fi 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Charles Leifer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md 2 | include LICENSE 3 | include README.rst 4 | include TODO.rst 5 | include pyproject.toml 6 | include runtests.py 7 | include tests.py 8 | include playhouse/*.pyx 9 | include playhouse/*.c 10 | include playhouse/pskel 11 | include playhouse/README.md 12 | include playhouse/tests/README 13 | recursive-include examples * 14 | recursive-include docs * 15 | recursive-include playhouse/_pysqlite * 16 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://media.charlesleifer.com/blog/photos/peewee3-logo.png 2 | 3 | peewee 4 | ====== 5 | 6 | Peewee is a simple and small ORM. It has few (but expressive) concepts, making it easy to learn and intuitive to use. 7 | 8 | * a small, expressive ORM 9 | * python 2.7+ and 3.4+ 10 | * supports sqlite, mysql, mariadb, postgresql 11 | * tons of `extensions `_ 12 | 13 | New to peewee? These may help: 14 | 15 | * `Quickstart `_ 16 | * `Example twitter app `_ 17 | * `Using peewee interactively `_ 18 | * `Models and fields `_ 19 | * `Querying `_ 20 | * `Relationships and joins `_ 21 | 22 | Examples 23 | -------- 24 | 25 | Defining models is similar to Django or SQLAlchemy: 26 | 27 | .. code-block:: python 28 | 29 | from peewee import * 30 | import datetime 31 | 32 | 33 | db = SqliteDatabase('my_database.db') 34 | 35 | class BaseModel(Model): 36 | class Meta: 37 | database = db 38 | 39 | class User(BaseModel): 40 | username = CharField(unique=True) 41 | 42 | class Tweet(BaseModel): 43 | user = ForeignKeyField(User, backref='tweets') 44 | message = TextField() 45 | created_date = DateTimeField(default=datetime.datetime.now) 46 | is_published = BooleanField(default=True) 47 | 48 | Connect to the database and create tables: 49 | 50 | .. code-block:: python 51 | 52 | db.connect() 53 | db.create_tables([User, Tweet]) 54 | 55 | Create a few rows: 56 | 57 | .. code-block:: python 58 | 59 | charlie = User.create(username='charlie') 60 | huey = User(username='huey') 61 | huey.save() 62 | 63 | # No need to set `is_published` or `created_date` since they 64 | # will just use the default values we specified. 65 | Tweet.create(user=charlie, message='My first tweet') 66 | 67 | Queries are expressive and composable: 68 | 69 | .. code-block:: python 70 | 71 | # A simple query selecting a user. 72 | User.get(User.username == 'charlie') 73 | 74 | # Get tweets created by one of several users. 75 | usernames = ['charlie', 'huey', 'mickey'] 76 | users = User.select().where(User.username.in_(usernames)) 77 | tweets = Tweet.select().where(Tweet.user.in_(users)) 78 | 79 | # We could accomplish the same using a JOIN: 80 | tweets = (Tweet 81 | .select() 82 | .join(User) 83 | .where(User.username.in_(usernames))) 84 | 85 | # How many tweets were published today? 86 | tweets_today = (Tweet 87 | .select() 88 | .where( 89 | (Tweet.created_date >= datetime.date.today()) & 90 | (Tweet.is_published == True)) 91 | .count()) 92 | 93 | # Paginate the user table and show me page 3 (users 41-60). 94 | User.select().order_by(User.username).paginate(3, 20) 95 | 96 | # Order users by the number of tweets they've created: 97 | tweet_ct = fn.Count(Tweet.id) 98 | users = (User 99 | .select(User, tweet_ct.alias('ct')) 100 | .join(Tweet, JOIN.LEFT_OUTER) 101 | .group_by(User) 102 | .order_by(tweet_ct.desc())) 103 | 104 | # Do an atomic update (for illustrative purposes only, imagine a simple 105 | # table for tracking a "count" associated with each URL). We don't want to 106 | # naively get the save in two separate steps since this is prone to race 107 | # conditions. 108 | Counter.update(count=Counter.count + 1).where(Counter.url == request.url) 109 | 110 | Check out the `example twitter app `_. 111 | 112 | Learning more 113 | ------------- 114 | 115 | Check the `documentation `_ for more examples. 116 | 117 | Specific question? Come hang out in the #peewee channel on irc.libera.chat, or post to the mailing list, http://groups.google.com/group/peewee-orm . If you would like to report a bug, `create a new issue `_ on GitHub. 118 | 119 | Still want more info? 120 | --------------------- 121 | 122 | .. image:: https://media.charlesleifer.com/blog/photos/wat.jpg 123 | 124 | I've written a number of blog posts about building applications and web-services with peewee (and usually Flask). If you'd like to see some real-life applications that use peewee, the following resources may be useful: 125 | 126 | * `Building a note-taking app with Flask and Peewee `_ as well as `Part 2 `_ and `Part 3 `_. 127 | * `Analytics web service built with Flask and Peewee `_. 128 | * `Personalized news digest (with a boolean query parser!) `_. 129 | * `Structuring Flask apps with Peewee `_. 130 | * `Creating a lastpass clone with Flask and Peewee `_. 131 | * `Creating a bookmarking web-service that takes screenshots of your bookmarks `_. 132 | * `Building a pastebin, wiki and a bookmarking service using Flask and Peewee `_. 133 | * `Encrypted databases with Python and SQLCipher `_. 134 | * `Dear Diary: An Encrypted, Command-Line Diary with Peewee `_. 135 | * `Query Tree Structures in SQLite using Peewee and the Transitive Closure Extension `_. 136 | -------------------------------------------------------------------------------- /TODO.rst: -------------------------------------------------------------------------------- 1 | todo 2 | ==== 3 | -------------------------------------------------------------------------------- /bench.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | 3 | 4 | db = SqliteDatabase(':memory:') 5 | #db = PostgresqlDatabase('peewee_test', host='127.0.0.1', port=26257, user='root') 6 | #db = PostgresqlDatabase('peewee_test', host='127.0.0.1', user='postgres') 7 | 8 | class Base(Model): 9 | class Meta: 10 | database = db 11 | 12 | class Register(Base): 13 | value = IntegerField() 14 | 15 | class Collection(Base): 16 | name = TextField() 17 | 18 | class Item(Base): 19 | collection = ForeignKeyField(Collection, backref='items') 20 | name = TextField() 21 | 22 | import functools 23 | import time 24 | 25 | def timed(fn): 26 | @functools.wraps(fn) 27 | def inner(*args, **kwargs): 28 | times = [] 29 | N = 10 30 | for i in range(N): 31 | start = time.perf_counter() 32 | fn(i, *args, **kwargs) 33 | times.append(time.perf_counter() - start) 34 | print('%0.3f ... %s' % (round(sum(times) / N, 3), fn.__name__)) 35 | return inner 36 | 37 | def populate_register(s, n): 38 | for i in range(s, n): 39 | Register.create(value=i) 40 | 41 | def populate_collections(n, n_i): 42 | for i in range(n): 43 | c = Collection.create(name=str(i)) 44 | for j in range(n_i): 45 | Item.create(collection=c, name=str(j)) 46 | 47 | @timed 48 | def insert(i): 49 | with db.atomic(): 50 | populate_register((i * 1000), (i + 1) * 1000) 51 | 52 | @timed 53 | def batch_insert(i): 54 | it = range(i * 1000, (i + 1) * 1000) 55 | for i in db.batch_commit(it, 100): 56 | Register.insert(value=i).execute() 57 | 58 | @timed 59 | def bulk_insert(i): 60 | with db.atomic(): 61 | for i in range(i * 1000, (i + 1) * 1000, 100): 62 | data = [(j,) for j in range(i, i + 100)] 63 | Register.insert_many(data, fields=[Register.value]).execute() 64 | 65 | @timed 66 | def bulk_create(i): 67 | with db.atomic(): 68 | data = [Register(value=i) for i in range(i * 1000, (i + 1) * 1000)] 69 | Register.bulk_create(data, batch_size=100) 70 | 71 | @timed 72 | def select(i): 73 | query = Register.select() 74 | for row in query: 75 | pass 76 | 77 | @timed 78 | def select_related_dbapi_raw(i): 79 | query = Item.select(Item, Collection).join(Collection) 80 | cursor = db.execute(query) 81 | for row in cursor: 82 | pass 83 | 84 | @timed 85 | def insert_related(i): 86 | with db.atomic(): 87 | populate_collections(30, 60) 88 | 89 | @timed 90 | def select_related(i): 91 | query = Item.select(Item, Collection).join(Collection) 92 | for item in query: 93 | pass 94 | 95 | @timed 96 | def select_related_left(i): 97 | query = Collection.select(Collection, Item).join(Item, JOIN.LEFT_OUTER) 98 | for collection in query: 99 | pass 100 | 101 | @timed 102 | def select_related_dicts(i): 103 | query = Item.select(Item, Collection).join(Collection).dicts() 104 | for row in query: 105 | pass 106 | 107 | @timed 108 | def select_related_objects(i): 109 | query = Item.select(Item, Collection).join(Collection).objects() 110 | for item in query: 111 | pass 112 | 113 | @timed 114 | def select_prefetch(i): 115 | query = prefetch(Collection.select(), Item) 116 | for c in query: 117 | for i in c.items: 118 | pass 119 | 120 | @timed 121 | def select_prefetch_join(i): 122 | query = prefetch(Collection.select(), Item, 123 | prefetch_type=PREFETCH_TYPE.JOIN) 124 | for c in query: 125 | for i in c.items: 126 | pass 127 | 128 | 129 | if __name__ == '__main__': 130 | db.create_tables([Register, Collection, Item]) 131 | insert() 132 | insert_related() 133 | Register.delete().execute() 134 | batch_insert() 135 | assert Register.select().count() == 10000 136 | Register.delete().execute() 137 | bulk_insert() 138 | assert Register.select().count() == 10000 139 | Register.delete().execute() 140 | bulk_create() 141 | assert Register.select().count() == 10000 142 | select() 143 | select_related() 144 | select_related_left() 145 | select_related_objects() 146 | select_related_dicts() 147 | select_related_dbapi_raw() 148 | select_prefetch() 149 | select_prefetch_join() 150 | db.drop_tables([Register, Collection, Item]) 151 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/peewee.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/peewee.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/peewee" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/peewee" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/_static/peewee-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/docs/_static/peewee-white.png -------------------------------------------------------------------------------- /docs/_themes/flask/layout.html: -------------------------------------------------------------------------------- 1 | {%- extends "basic/layout.html" %} 2 | {%- block extrahead %} 3 | {{ super() }} 4 | {% if theme_touch_icon %} 5 | 6 | {% endif %} 7 | 9 | {% endblock %} 10 | {%- block relbar2 %}{% endblock %} 11 | {% block header %} 12 | {{ super() }} 13 | {% if pagename == 'index' %} 14 |
15 | {% endif %} 16 | {% endblock %} 17 | {%- block footer %} 18 | 22 | {% if pagename == 'index' %} 23 |
24 | {% endif %} 25 | {%- endblock %} 26 | -------------------------------------------------------------------------------- /docs/_themes/flask/relations.html: -------------------------------------------------------------------------------- 1 |

Related Topics

2 | 20 | -------------------------------------------------------------------------------- /docs/_themes/flask/static/small_flask.css: -------------------------------------------------------------------------------- 1 | /* 2 | * small_flask.css_t 3 | * ~~~~~~~~~~~~~~~~~ 4 | * 5 | * :copyright: Copyright 2010 by Armin Ronacher. 6 | * :license: Flask Design License, see LICENSE for details. 7 | */ 8 | 9 | body { 10 | margin: 0; 11 | padding: 20px 30px; 12 | } 13 | 14 | div.documentwrapper { 15 | float: none; 16 | background: white; 17 | } 18 | 19 | div.sphinxsidebar { 20 | display: block; 21 | float: none; 22 | width: 102.5%; 23 | margin: 50px -30px -20px -30px; 24 | padding: 10px 20px; 25 | background: #333; 26 | color: white; 27 | } 28 | 29 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 30 | div.sphinxsidebar h3 a { 31 | color: white; 32 | } 33 | 34 | div.sphinxsidebar a { 35 | color: #aaa; 36 | } 37 | 38 | div.sphinxsidebar p.logo { 39 | display: none; 40 | } 41 | 42 | div.document { 43 | width: 100%; 44 | margin: 0; 45 | } 46 | 47 | div.related { 48 | display: block; 49 | margin: 0; 50 | padding: 10px 0 20px 0; 51 | } 52 | 53 | div.related ul, 54 | div.related ul li { 55 | margin: 0; 56 | padding: 0; 57 | } 58 | 59 | div.footer { 60 | display: none; 61 | } 62 | 63 | div.bodywrapper { 64 | margin: 0; 65 | } 66 | 67 | div.body { 68 | min-height: 0; 69 | padding: 0; 70 | } 71 | -------------------------------------------------------------------------------- /docs/_themes/flask/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | 6 | [options] 7 | index_logo = '' 8 | index_logo_height = 120px 9 | touch_icon = 10 | -------------------------------------------------------------------------------- /docs/crdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/docs/crdb.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. peewee documentation master file, created by 2 | sphinx-quickstart on Thu Nov 25 21:20:29 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | peewee 7 | ====== 8 | 9 | .. image:: peewee3-logo.png 10 | 11 | Peewee is a simple and small ORM. It has few (but expressive) concepts, making 12 | it easy to learn and intuitive to use. 13 | 14 | * a small, expressive ORM 15 | * python 2.7+ and 3.4+ 16 | * supports sqlite, mysql, mariadb, postgresql. 17 | * :ref:`tons of extensions ` 18 | 19 | .. image:: postgresql.png 20 | :target: peewee/database.html#using-postgresql 21 | :alt: postgresql 22 | 23 | .. image:: mysql.png 24 | :target: peewee/database.html#using-mysql 25 | :alt: mysql 26 | 27 | .. image:: mariadb.png 28 | :target: peewee/database.html#using-mariadb 29 | :alt: mariadb 30 | 31 | .. image:: sqlite.png 32 | :target: peewee/database.html#using-sqlite 33 | :alt: sqlite 34 | 35 | Peewee's source code hosted on `GitHub `_. 36 | 37 | New to peewee? These may help: 38 | 39 | * :ref:`Quickstart ` 40 | * :ref:`Example twitter app ` 41 | * :ref:`Using peewee interactively ` 42 | * :ref:`Models and fields ` 43 | * :ref:`Querying ` 44 | * :ref:`Relationships and joins ` 45 | 46 | Contents: 47 | --------- 48 | 49 | .. toctree:: 50 | :maxdepth: 2 51 | :glob: 52 | 53 | peewee/installation 54 | peewee/quickstart 55 | peewee/example 56 | peewee/interactive 57 | peewee/contributing 58 | peewee/database 59 | peewee/models 60 | peewee/querying 61 | peewee/query_operators 62 | peewee/relationships 63 | peewee/api 64 | peewee/sqlite_ext 65 | peewee/playhouse 66 | peewee/query_examples 67 | peewee/query_builder 68 | peewee/hacks 69 | peewee/changes 70 | 71 | Note 72 | ---- 73 | 74 | If you find any bugs, odd behavior, or have an idea for a new feature please don't hesitate to `open an issue `_ on GitHub or `contact me `_. 75 | 76 | Indices and tables 77 | ================== 78 | 79 | * :ref:`genindex` 80 | * :ref:`modindex` 81 | * :ref:`search` 82 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 47 | goto end 48 | ) 49 | 50 | if "%1" == "dirhtml" ( 51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 52 | echo. 53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 54 | goto end 55 | ) 56 | 57 | if "%1" == "singlehtml" ( 58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 59 | echo. 60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 61 | goto end 62 | ) 63 | 64 | if "%1" == "pickle" ( 65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 66 | echo. 67 | echo.Build finished; now you can process the pickle files. 68 | goto end 69 | ) 70 | 71 | if "%1" == "json" ( 72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 73 | echo. 74 | echo.Build finished; now you can process the JSON files. 75 | goto end 76 | ) 77 | 78 | if "%1" == "htmlhelp" ( 79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 80 | echo. 81 | echo.Build finished; now you can run HTML Help Workshop with the ^ 82 | .hhp project file in %BUILDDIR%/htmlhelp. 83 | goto end 84 | ) 85 | 86 | if "%1" == "qthelp" ( 87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 88 | echo. 89 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 90 | .qhcp project file in %BUILDDIR%/qthelp, like this: 91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\peewee.qhcp 92 | echo.To view the help file: 93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\peewee.ghc 94 | goto end 95 | ) 96 | 97 | if "%1" == "devhelp" ( 98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 99 | echo. 100 | echo.Build finished. 101 | goto end 102 | ) 103 | 104 | if "%1" == "epub" ( 105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 106 | echo. 107 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 108 | goto end 109 | ) 110 | 111 | if "%1" == "latex" ( 112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 113 | echo. 114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 115 | goto end 116 | ) 117 | 118 | if "%1" == "text" ( 119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 120 | echo. 121 | echo.Build finished. The text files are in %BUILDDIR%/text. 122 | goto end 123 | ) 124 | 125 | if "%1" == "man" ( 126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 127 | echo. 128 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 129 | goto end 130 | ) 131 | 132 | if "%1" == "changes" ( 133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 134 | echo. 135 | echo.The overview file is in %BUILDDIR%/changes. 136 | goto end 137 | ) 138 | 139 | if "%1" == "linkcheck" ( 140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 141 | echo. 142 | echo.Link check complete; look for any errors in the above output ^ 143 | or in %BUILDDIR%/linkcheck/output.txt. 144 | goto end 145 | ) 146 | 147 | if "%1" == "doctest" ( 148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 149 | echo. 150 | echo.Testing of doctests in the sources finished, look at the ^ 151 | results in %BUILDDIR%/doctest/output.txt. 152 | goto end 153 | ) 154 | 155 | :end 156 | -------------------------------------------------------------------------------- /docs/mariadb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/docs/mariadb.png -------------------------------------------------------------------------------- /docs/mysql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/docs/mysql.png -------------------------------------------------------------------------------- /docs/peewee-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/docs/peewee-logo.png -------------------------------------------------------------------------------- /docs/peewee-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/docs/peewee-white.png -------------------------------------------------------------------------------- /docs/peewee/contributing.rst: -------------------------------------------------------------------------------- 1 | .. _contributing: 2 | 3 | Contributing 4 | ============ 5 | 6 | In order to continually improve, Peewee needs the help of developers like you. 7 | Whether it's contributing patches, submitting bug reports, or just asking and 8 | answering questions, you are helping to make Peewee a better library. 9 | 10 | In this document I'll describe some of the ways you can help. 11 | 12 | Patches 13 | ------- 14 | 15 | Do you have an idea for a new feature, or is there a clunky API you'd like to 16 | improve? Before coding it up and submitting a pull-request, `open a new issue 17 | `_ on GitHub describing your 18 | proposed changes. This doesn't have to be anything formal, just a description 19 | of what you'd like to do and why. 20 | 21 | When you're ready, you can submit a pull-request with your changes. Successful 22 | patches will have the following: 23 | 24 | * Unit tests. 25 | * Documentation, both prose form and general :ref:`API documentation `. 26 | * Code that conforms stylistically with the rest of the Peewee codebase. 27 | 28 | Bugs 29 | ---- 30 | 31 | If you've found a bug, please check to see if it has `already been reported `_, 32 | and if not `create an issue on GitHub `_. 33 | The more information you include, the more quickly the bug will get fixed, so 34 | please try to include the following: 35 | 36 | * Traceback and the error message (please `format your code `_!) 37 | * Relevant portions of your code or code to reproduce the error 38 | * Peewee version: ``python -c "from peewee import __version__; print(__version__)"`` 39 | * Which database you're using 40 | 41 | If you have found a bug in the code and submit a failing test-case, then hats-off to you, you are a hero! 42 | 43 | Questions 44 | --------- 45 | 46 | If you have questions about how to do something with peewee, then I recommend 47 | either: 48 | 49 | * Ask on StackOverflow. I check SO just about every day for new peewee 50 | questions and try to answer them. This has the benefit also of preserving the 51 | question and answer for other people to find. 52 | * Ask on the mailing list, https://groups.google.com/group/peewee-orm 53 | -------------------------------------------------------------------------------- /docs/peewee/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installing and Testing 4 | ====================== 5 | 6 | Most users will want to simply install the latest version, hosted on PyPI: 7 | 8 | .. code-block:: console 9 | 10 | pip install peewee 11 | 12 | Peewee comes with a couple C extensions that will be built if Cython is 13 | available. 14 | 15 | * Sqlite extensions, which includes Cython implementations of the SQLite date 16 | manipulation functions, the REGEXP operator, and full-text search result 17 | ranking algorithms. 18 | 19 | 20 | Installing with git 21 | ------------------- 22 | 23 | The project is hosted at https://github.com/coleifer/peewee and can be installed 24 | using git: 25 | 26 | .. code-block:: console 27 | 28 | git clone https://github.com/coleifer/peewee.git 29 | cd peewee 30 | python setup.py install 31 | 32 | .. note:: 33 | On some systems you may need to use ``sudo python setup.py install`` to 34 | install peewee system-wide. 35 | 36 | If you would like to build the SQLite extension in a git checkout, you can run: 37 | 38 | .. code-block:: console 39 | 40 | # Build the C extension and place shared libraries alongside other modules. 41 | python setup.py build_ext -i 42 | 43 | 44 | Running tests 45 | ------------- 46 | 47 | You can test your installation by running the test suite. 48 | 49 | .. code-block:: console 50 | 51 | python runtests.py 52 | 53 | You can test specific features or specific database drivers using the 54 | ``runtests.py`` script. To view the available test runner options, use: 55 | 56 | .. code-block:: console 57 | 58 | python runtests.py --help 59 | 60 | .. note:: 61 | To run tests against Postgres or MySQL you need to create a database named 62 | "peewee_test". To test the Postgres extension module, you will also want to 63 | install the HStore extension in the postgres test database: 64 | 65 | .. code-block:: sql 66 | 67 | -- install the hstore extension on the peewee_test postgres db. 68 | CREATE EXTENSION hstore; 69 | 70 | 71 | Optional dependencies 72 | --------------------- 73 | 74 | .. note:: 75 | To use Peewee, you typically won't need anything outside the standard 76 | library, since most Python distributions are compiled with SQLite support. 77 | You can test by running ``import sqlite3`` in the Python console. If you 78 | wish to use another database, there are many DB-API 2.0-compatible drivers 79 | out there, such as ``pymysql`` or ``psycopg2`` for MySQL and Postgres 80 | respectively. 81 | 82 | * `Cython `_: used to expose additional functionality when 83 | using SQLite and to implement things like search result ranking in a 84 | performant manner. Since the generated C files are included with the package 85 | distribution, Cython is no longer required to use the C extensions. 86 | * `apsw `_: an optional 3rd-party SQLite 87 | binding offering greater performance and comprehensive support for SQLite's C 88 | APIs. Use with :py:class:`APSWDatabase`. 89 | * `gevent `_ is an optional dependency for 90 | :py:class:`SqliteQueueDatabase` (though it works with ``threading`` just 91 | fine). 92 | * `BerkeleyDB `_ can 93 | be compiled with a SQLite frontend, which works with Peewee. Compiling can be 94 | tricky so `here are instructions `_. 95 | * Lastly, if you use the *Flask* framework, there are helper extension modules 96 | available. 97 | 98 | 99 | Note on the SQLite extensions 100 | ----------------------------- 101 | 102 | Peewee includes two SQLite-specific C extensions which provide additional 103 | functionality and improved performance for SQLite database users. Peewee will 104 | attempt to determine ahead-of-time if SQLite3 is installed, and only build the 105 | SQLite extensions if the SQLite shared-library is available on your system. 106 | 107 | If, however, you receive errors like the following when attempting to install 108 | Peewee, you can explicitly disable the compilation of the SQLite C extensions 109 | by settings the ``NO_SQLITE`` environment variable. 110 | 111 | .. code-block:: console 112 | 113 | fatal error: sqlite3.h: No such file or directory 114 | 115 | Here is how to install Peewee with the SQLite extensions explicitly disabled: 116 | 117 | .. code-block:: console 118 | 119 | $ NO_SQLITE=1 python setup.py install 120 | -------------------------------------------------------------------------------- /docs/peewee/interactive.rst: -------------------------------------------------------------------------------- 1 | .. _interactive: 2 | 3 | Using Peewee Interactively 4 | ========================== 5 | 6 | Peewee contains helpers for working interactively from a Python interpreter or 7 | something like a Jupyter notebook. For this example, we'll assume that we have 8 | a pre-existing Sqlite database with the following simple schema: 9 | 10 | .. code-block:: sql 11 | 12 | CREATE TABLE IF NOT EXISTS "event" ( 13 | "id" INTEGER NOT NULL PRIMARY KEY, 14 | "key" TEXT NOT NULL, 15 | "timestamp" DATETIME NOT NULL, 16 | "metadata" TEXT NOT NULL); 17 | 18 | To experiment with querying this database from an interactive interpreter 19 | session, we would start our interpreter and import the following helpers: 20 | 21 | * ``peewee.SqliteDatabase`` - to reference the "events.db" 22 | * ``playhouse.reflection.generate_models`` - to generate models from an 23 | existing database. 24 | * ``playhouse.reflection.print_model`` - to view the model definition. 25 | * ``playhouse.reflection.print_table_sql`` - to view the table SQL. 26 | 27 | Our terminal session might look like this: 28 | 29 | .. code-block:: pycon 30 | 31 | >>> from peewee import SqliteDatabase 32 | >>> from playhouse.reflection import generate_models, print_model, print_table_sql 33 | >>> 34 | 35 | The :py:func:`generate_models` function will introspect the database and 36 | generate model classes for all the tables that are found. This is a handy way 37 | to get started and can save a lot of typing. The function returns a dictionary 38 | keyed by the table name, with the generated model as the corresponding value: 39 | 40 | .. code-block:: pycon 41 | 42 | >>> db = SqliteDatabase('events.db') 43 | >>> models = generate_models(db) 44 | >>> list(models.items()) 45 | [('events', )] 46 | 47 | >>> globals().update(models) # Inject models into global namespace. 48 | >>> event 49 | 50 | 51 | To take a look at the model definition, which lists the model's fields and 52 | data-type, we can use the :py:func:`print_model` function: 53 | 54 | .. code-block:: pycon 55 | 56 | >>> print_model(event) 57 | event 58 | id AUTO 59 | key TEXT 60 | timestamp DATETIME 61 | metadata TEXT 62 | 63 | We can also generate a SQL ``CREATE TABLE`` for the introspected model, if you 64 | find that easier to read. This should match the actual table definition in the 65 | introspected database: 66 | 67 | .. code-block:: pycon 68 | 69 | >>> print_table_sql(event) 70 | CREATE TABLE IF NOT EXISTS "event" ( 71 | "id" INTEGER NOT NULL PRIMARY KEY, 72 | "key" TEXT NOT NULL, 73 | "timestamp" DATETIME NOT NULL, 74 | "metadata" TEXT NOT NULL) 75 | 76 | Now that we are familiar with the structure of the table we're working with, we 77 | can run some queries on the generated ``event`` model: 78 | 79 | .. code-block:: pycon 80 | 81 | >>> for e in event.select().order_by(event.timestamp).limit(5): 82 | ... print(e.key, e.timestamp) 83 | ... 84 | e00 2019-01-01 00:01:00 85 | e01 2019-01-01 00:02:00 86 | e02 2019-01-01 00:03:00 87 | e03 2019-01-01 00:04:00 88 | e04 2019-01-01 00:05:00 89 | 90 | >>> event.select(fn.MIN(event.timestamp), fn.MAX(event.timestamp)).scalar(as_tuple=True) 91 | (datetime.datetime(2019, 1, 1, 0, 1), datetime.datetime(2019, 1, 1, 1, 0)) 92 | 93 | >>> event.select().count() # Or, len(event) 94 | 60 95 | 96 | For more information about these APIs and other similar reflection utilities, 97 | see the :ref:`reflection` section of the :ref:`playhouse extensions ` 98 | document. 99 | 100 | To generate an actual Python module containing model definitions for an 101 | existing database, you can use the command-line :ref:`pwiz ` tool. Here 102 | is a quick example: 103 | 104 | .. code-block:: console 105 | 106 | $ pwiz -e sqlite events.db > events.py 107 | 108 | The ``events.py`` file will now be an import-able module containing a database 109 | instance (referencing the ``events.db``) along with model definitions for any 110 | tables found in the database. ``pwiz`` does some additional nice things like 111 | introspecting indexes and adding proper flags for ``NULL``/``NOT NULL`` 112 | constraints, etc. 113 | 114 | The APIs discussed in this section: 115 | 116 | * :py:func:`generate_models` 117 | * :py:func:`print_model` 118 | * :py:func:`print_table_sql` 119 | 120 | More low-level APIs are also available on the :py:class:`Database` instance: 121 | 122 | * :py:meth:`Database.get_tables` 123 | * :py:meth:`Database.get_indexes` 124 | * :py:meth:`Database.get_columns` (for a given table) 125 | * :py:meth:`Database.get_primary_keys` (for a given table) 126 | * :py:meth:`Database.get_foreign_keys` (for a given table) 127 | -------------------------------------------------------------------------------- /docs/peewee/schema-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/docs/peewee/schema-horizontal.png -------------------------------------------------------------------------------- /docs/peewee/schema.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/docs/peewee/schema.jpg -------------------------------------------------------------------------------- /docs/peewee/tweepee.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/docs/peewee/tweepee.jpg -------------------------------------------------------------------------------- /docs/peewee3-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/docs/peewee3-logo.png -------------------------------------------------------------------------------- /docs/postgresql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/docs/postgresql.png -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | docutils<0.18 2 | sphinx-rtd-theme 3 | -------------------------------------------------------------------------------- /docs/sqlite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/docs/sqlite.png -------------------------------------------------------------------------------- /examples/adjacency_list.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | 3 | 4 | db = SqliteDatabase(':memory:') 5 | 6 | class Node(Model): 7 | name = TextField() 8 | parent = ForeignKeyField('self', backref='children', null=True) 9 | 10 | class Meta: 11 | database = db 12 | 13 | def __str__(self): 14 | return self.name 15 | 16 | def dump(self, _indent=0): 17 | return (' ' * _indent + self.name + '\n' + 18 | ''.join(child.dump(_indent + 1) for child in self.children)) 19 | 20 | db.create_tables([Node]) 21 | 22 | tree = ('root', ( 23 | ('n1', ( 24 | ('c11', ()), 25 | ('c12', ()))), 26 | ('n2', ( 27 | ('c21', ()), 28 | ('c22', ( 29 | ('g221', ()), 30 | ('g222', ()))), 31 | ('c23', ()), 32 | ('c24', ( 33 | ('g241', ()), 34 | ('g242', ()), 35 | ('g243', ()))))))) 36 | stack = [(None, tree)] 37 | while stack: 38 | parent, (name, children) = stack.pop() 39 | node = Node.create(name=name, parent=parent) 40 | for child_tree in children: 41 | stack.insert(0, (node, child_tree)) 42 | 43 | # Now that we have created the stack, let's eagerly load 4 levels of children. 44 | # To show that it works, we'll turn on the query debugger so you can see which 45 | # queries are executed. 46 | import logging; logger = logging.getLogger('peewee') 47 | logger.addHandler(logging.StreamHandler()) 48 | logger.setLevel(logging.DEBUG) 49 | 50 | C = Node.alias('c') 51 | G = Node.alias('g') 52 | GG = Node.alias('gg') 53 | GGG = Node.alias('ggg') 54 | 55 | roots = Node.select().where(Node.parent.is_null()) 56 | pf = prefetch(roots, C, (G, C), (GG, G), (GGG, GG)) 57 | for root in pf: 58 | print(root.dump()) 59 | -------------------------------------------------------------------------------- /examples/analytics/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example "Analytics" app. To start using this on your site, do the following: 3 | 4 | * Create a postgresql database: 5 | 6 | createdb analytics 7 | 8 | * Create an account for each domain you intend to collect analytics for, e.g. 9 | 10 | Account.create(domain='charlesleifer.com') 11 | 12 | * Update configuration values marked "TODO", e.g. DOMAIN. 13 | 14 | * Run this app using the WSGI server of your choice. 15 | 16 | * Using the appropriate account id, add a ` 20 | 21 | Take a look at `reports.py` for some interesting queries you can perform 22 | on your pageview data. 23 | """ 24 | import datetime 25 | import os 26 | from urllib.parse import parse_qsl, urlparse 27 | import binascii 28 | 29 | from flask import Flask, Response, abort, g, request 30 | from peewee import * 31 | from playhouse.postgres_ext import BinaryJSONField, PostgresqlExtDatabase 32 | 33 | # Analytics settings. 34 | # 1px gif. 35 | BEACON = binascii.unhexlify( 36 | '47494638396101000100800000dbdfef00000021f90401000000002c00000000010001000002024401003b') 37 | DATABASE_NAME = 'analytics' 38 | DOMAIN = 'http://analytics.yourdomain.com' # TODO: change me. 39 | JAVASCRIPT = """(function(id){ 40 | var d=document,i=new Image,e=encodeURIComponent; 41 | i.src='%s/a.gif?id='+id+'&url='+e(d.location.href)+'&ref='+e(d.referrer)+'&t='+e(d.title); 42 | })(%s)""".replace('\n', '') 43 | 44 | # Flask settings. 45 | DEBUG = bool(os.environ.get('DEBUG')) 46 | SECRET_KEY = 'secret - change me' # TODO: change me. 47 | 48 | app = Flask(__name__) 49 | app.config.from_object(__name__) 50 | 51 | database = PostgresqlExtDatabase(DATABASE_NAME, user='postgres') 52 | 53 | 54 | class BaseModel(Model): 55 | class Meta: 56 | database = database 57 | 58 | 59 | class Account(BaseModel): 60 | domain = CharField() 61 | 62 | def verify_url(self, url): 63 | netloc = urlparse(url).netloc 64 | url_domain = '.'.join(netloc.split('.')[-2:]) # Ignore subdomains. 65 | return self.domain == url_domain 66 | 67 | 68 | class PageView(BaseModel): 69 | account = ForeignKeyField(Account, backref='pageviews') 70 | url = TextField() 71 | timestamp = DateTimeField(default=datetime.datetime.now) 72 | title = TextField(default='') 73 | ip = CharField(default='') 74 | referrer = TextField(default='') 75 | headers = BinaryJSONField() 76 | params = BinaryJSONField() 77 | 78 | @classmethod 79 | def create_from_request(cls, account, request): 80 | parsed = urlparse(request.args['url']) 81 | params = dict(parse_qsl(parsed.query)) 82 | 83 | return PageView.create( 84 | account=account, 85 | url=parsed.path, 86 | title=request.args.get('t') or '', 87 | ip=request.headers.get('x-forwarded-for', request.remote_addr), 88 | referrer=request.args.get('ref') or '', 89 | headers=dict(request.headers), 90 | params=params) 91 | 92 | 93 | @app.route('/a.gif') 94 | def analyze(): 95 | # Make sure an account id and url were specified. 96 | if not request.args.get('id') or not request.args.get('url'): 97 | abort(404) 98 | 99 | # Ensure the account id is valid. 100 | try: 101 | account = Account.get(Account.id == request.args['id']) 102 | except Account.DoesNotExist: 103 | abort(404) 104 | 105 | # Ensure the account id matches the domain of the URL we wish to record. 106 | if not account.verify_url(request.args['url']): 107 | abort(403) 108 | 109 | # Store the page-view data in the database. 110 | PageView.create_from_request(account, request) 111 | 112 | # Return a 1px gif. 113 | response = Response(app.config['BEACON'], mimetype='image/gif') 114 | response.headers['Cache-Control'] = 'private, no-cache' 115 | return response 116 | 117 | 118 | @app.route('/a.js') 119 | def script(): 120 | account_id = request.args.get('id') 121 | if account_id: 122 | return Response( 123 | app.config['JAVASCRIPT'] % (app.config['DOMAIN'], account_id), 124 | mimetype='text/javascript') 125 | return Response('', mimetype='text/javascript') 126 | 127 | 128 | @app.errorhandler(404) 129 | def not_found(e): 130 | return Response('

Not found.

') 131 | 132 | # Request handlers -- these two hooks are provided by flask and we will use them 133 | # to create and tear down a database connection on each request. 134 | 135 | 136 | @app.before_request 137 | def before_request(): 138 | g.db = database 139 | g.db.connection() 140 | 141 | 142 | @app.after_request 143 | def after_request(response): 144 | g.db.close() 145 | return response 146 | 147 | 148 | if __name__ == '__main__': 149 | database.create_tables([Account, PageView], safe=True) 150 | app.run(debug=True) 151 | -------------------------------------------------------------------------------- /examples/analytics/reports.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | 3 | from app import Account, PageView 4 | 5 | 6 | DEFAULT_ACCOUNT_ID = 1 7 | 8 | class Report(object): 9 | def __init__(self, account_id=DEFAULT_ACCOUNT_ID): 10 | self.account = Account.get(Account.id == account_id) 11 | self.date_range = None 12 | 13 | def get_query(self): 14 | query = PageView.select().where(PageView.account == self.account) 15 | if self.date_range: 16 | query = query.where(PageView.timestamp.between(*self.date_range)) 17 | return query 18 | 19 | def top_pages_by_time_period(self, interval='day'): 20 | """ 21 | Get a breakdown of top pages per interval, i.e. 22 | 23 | day url count 24 | 2014-01-01 /blog/ 11 25 | 2014-01-02 /blog/ 14 26 | 2014-01-03 /blog/ 9 27 | """ 28 | date_trunc = fn.date_trunc(interval, PageView.timestamp) 29 | return (self.get_query() 30 | .select( 31 | PageView.url, 32 | date_trunc.alias(interval), 33 | fn.Count(PageView.id).alias('count')) 34 | .group_by(PageView.url, date_trunc) 35 | .order_by( 36 | SQL(interval), 37 | SQL('count').desc(), 38 | PageView.url)) 39 | 40 | def cookies(self): 41 | """ 42 | Retrieve the cookies header from all the users who visited. 43 | """ 44 | return (self.get_query() 45 | .select(PageView.ip, PageView.headers['Cookie']) 46 | .where(PageView.headers['Cookie'].is_null(False)) 47 | .tuples()) 48 | 49 | def user_agents(self): 50 | """ 51 | Retrieve user-agents, sorted by most common to least common. 52 | """ 53 | return (self.get_query() 54 | .select( 55 | PageView.headers['User-Agent'], 56 | fn.Count(PageView.id)) 57 | .group_by(PageView.headers['User-Agent']) 58 | .order_by(fn.Count(PageView.id).desc()) 59 | .tuples()) 60 | 61 | def languages(self): 62 | """ 63 | Retrieve languages, sorted by most common to least common. The 64 | Accept-Languages header sometimes looks weird, i.e. 65 | "en-US,en;q=0.8,is;q=0.6,da;q=0.4" We will split on the first semi- 66 | colon. 67 | """ 68 | language = PageView.headers['Accept-Language'] 69 | first_language = fn.SubStr( 70 | language, # String to slice. 71 | 1, # Left index. 72 | fn.StrPos(language, ';')) 73 | return (self.get_query() 74 | .select(first_language, fn.Count(PageView.id)) 75 | .group_by(first_language) 76 | .order_by(fn.Count(PageView.id).desc()) 77 | .tuples()) 78 | 79 | def trail(self): 80 | """ 81 | Get all visitors by IP and then list the pages they visited in order. 82 | """ 83 | inner = (self.get_query() 84 | .select(PageView.ip, PageView.url) 85 | .order_by(PageView.timestamp)) 86 | return (PageView 87 | .select( 88 | PageView.ip, 89 | fn.array_agg(PageView.url).alias('urls')) 90 | .from_(inner.alias('t1')) 91 | .group_by(PageView.ip)) 92 | 93 | def _referrer_clause(self, domain_only=True): 94 | if domain_only: 95 | return fn.SubString(Clause( 96 | PageView.referrer, SQL('FROM'), '.*://([^/]*)')) 97 | return PageView.referrer 98 | 99 | def top_referrers(self, domain_only=True): 100 | """ 101 | What domains send us the most traffic? 102 | """ 103 | referrer = self._referrer_clause(domain_only) 104 | return (self.get_query() 105 | .select(referrer, fn.Count(PageView.id)) 106 | .group_by(referrer) 107 | .order_by(fn.Count(PageView.id).desc()) 108 | .tuples()) 109 | 110 | def referrers_for_url(self, domain_only=True): 111 | referrer = self._referrer_clause(domain_only) 112 | return (self.get_query() 113 | .select(PageView.url, referrer, fn.Count(PageView.id)) 114 | .group_by(PageView.url, referrer) 115 | .order_by(PageView.url, fn.Count(PageView.id).desc()) 116 | .tuples()) 117 | 118 | def referrers_to_url(self, domain_only=True): 119 | referrer = self._referrer_clause(domain_only) 120 | return (self.get_query() 121 | .select(referrer, PageView.url, fn.Count(PageView.id)) 122 | .group_by(referrer, PageView.url) 123 | .order_by(referrer, fn.Count(PageView.id).desc()) 124 | .tuples()) 125 | -------------------------------------------------------------------------------- /examples/analytics/requirements.txt: -------------------------------------------------------------------------------- 1 | peewee 2 | flask 3 | psycopg2 4 | -------------------------------------------------------------------------------- /examples/analytics/run_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | sys.path.insert(0, '../..') 5 | 6 | from app import app 7 | app.run(debug=True) 8 | -------------------------------------------------------------------------------- /examples/anomaly_detection.py: -------------------------------------------------------------------------------- 1 | import math 2 | from peewee import * 3 | 4 | 5 | db = SqliteDatabase(':memory:') 6 | 7 | class Reg(Model): 8 | key = TextField() 9 | value = IntegerField() 10 | 11 | class Meta: 12 | database = db 13 | 14 | 15 | db.create_tables([Reg]) 16 | 17 | # Create a user-defined aggregate function suitable for computing the standard 18 | # deviation of a series. 19 | @db.aggregate('stddev') 20 | class StdDev(object): 21 | def __init__(self): 22 | self.n = 0 23 | self.values = [] 24 | 25 | def step(self, value): 26 | self.n += 1 27 | self.values.append(value) 28 | 29 | def finalize(self): 30 | if self.n < 2: 31 | return 0 32 | mean = sum(self.values) / self.n 33 | sqsum = sum((i - mean) ** 2 for i in self.values) 34 | return math.sqrt(sqsum / (self.n - 1)) 35 | 36 | 37 | values = [2, 3, 5, 2, 3, 12, 5, 3, 4, 1, 2, 1, -9, 3, 3, 5] 38 | 39 | Reg.create_table() 40 | Reg.insert_many([{'key': 'k%02d' % i, 'value': v} 41 | for i, v in enumerate(values)]).execute() 42 | 43 | # We'll calculate the mean and the standard deviation of the series in a common 44 | # table expression, which will then be used by our query to find rows whose 45 | # zscore exceeds a certain threshold. 46 | cte = (Reg 47 | .select(fn.avg(Reg.value), fn.stddev(Reg.value)) 48 | .cte('stats', columns=('series_mean', 'series_stddev'))) 49 | 50 | # The zscore is defined as the (value - mean) / stddev. 51 | zscore = (Reg.value - cte.c.series_mean) / cte.c.series_stddev 52 | 53 | # Find rows which fall outside of 2 standard deviations. 54 | threshold = 2 55 | query = (Reg 56 | .select(Reg.key, Reg.value, zscore.alias('zscore')) 57 | .from_(Reg, cte) 58 | .where((zscore >= threshold) | (zscore <= -threshold)) 59 | .with_cte(cte)) 60 | 61 | for row in query: 62 | print(row.key, row.value, round(row.zscore, 2)) 63 | 64 | db.close() 65 | -------------------------------------------------------------------------------- /examples/blog/requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | beautifulsoup4 3 | micawber 4 | pygments 5 | markdown 6 | peewee 7 | -------------------------------------------------------------------------------- /examples/blog/static/css/hilite.css: -------------------------------------------------------------------------------- 1 | .highlight { 2 | background: #040400; 3 | color: #FFFFFF; 4 | } 5 | 6 | .highlight span.selection { color: #323232; } 7 | .highlight span.gp { color: #9595FF; } 8 | .highlight span.vi { color: #9595FF; } 9 | .highlight span.kn { color: #00C0D1; } 10 | .highlight span.cp { color: #AEE674; } 11 | .highlight span.caret { color: #FFFFFF; } 12 | .highlight span.no { color: #AEE674; } 13 | .highlight span.s2 { color: #BBFB8D; } 14 | .highlight span.nb { color: #A7FDB2; } 15 | .highlight span.nc { color: #C2ABFF; } 16 | .highlight span.nd { color: #AEE674; } 17 | .highlight span.s { color: #BBFB8D; } 18 | .highlight span.nf { color: #AEE674; } 19 | .highlight span.nx { color: #AEE674; } 20 | .highlight span.kp { color: #00C0D1; } 21 | .highlight span.nt { color: #C2ABFF; } 22 | .highlight span.s1 { color: #BBFB8D; } 23 | .highlight span.bg { color: #040400; } 24 | .highlight span.kt { color: #00C0D1; } 25 | .highlight span.support_function { color: #81B864; } 26 | .highlight span.ow { color: #EBE1B4; } 27 | .highlight span.mf { color: #A1FF24; } 28 | .highlight span.bp { color: #9595FF; } 29 | .highlight span.fg { color: #FFFFFF; } 30 | .highlight span.c1 { color: #3379FF; } 31 | .highlight span.kc { color: #9595FF; } 32 | .highlight span.c { color: #3379FF; } 33 | .highlight span.sx { color: #BBFB8D; } 34 | .highlight span.kd { color: #00C0D1; } 35 | .highlight span.ss { color: #A1FF24; } 36 | .highlight span.sr { color: #BBFB8D; } 37 | .highlight span.mo { color: #A1FF24; } 38 | .highlight span.mi { color: #A1FF24; } 39 | .highlight span.mh { color: #A1FF24; } 40 | .highlight span.o { color: #EBE1B4; } 41 | .highlight span.si { color: #DA96A3; } 42 | .highlight span.sh { color: #BBFB8D; } 43 | .highlight span.na { color: #AEE674; } 44 | .highlight span.sc { color: #BBFB8D; } 45 | .highlight span.k { color: #00C0D1; } 46 | .highlight span.se { color: #DA96A3; } 47 | .highlight span.sd { color: #54F79C; } 48 | -------------------------------------------------------------------------------- /examples/blog/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/examples/blog/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /examples/blog/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/examples/blog/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /examples/blog/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/examples/blog/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /examples/blog/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | -------------------------------------------------------------------------------- /examples/blog/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Blog 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% block extra_head %}{% endblock %} 12 | 13 | 14 | {% block extra_scripts %}{% endblock %} 15 | 16 | 17 | 18 | 48 | 49 |
50 | {% for category, message in get_flashed_messages(with_categories=true) %} 51 |
52 | 53 |

{{ message }}

54 |
55 | {% endfor %} 56 | 57 | {% block page_header %} 58 | 61 | {% endblock %} 62 | 63 | {% block content %}{% endblock %} 64 | 65 |
66 |
67 |

Blog, © 2015

68 |
69 |
70 | 71 | 72 | -------------------------------------------------------------------------------- /examples/blog/templates/create.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Create entry{% endblock %} 4 | 5 | {% block content_title %}Create entry{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 11 |
12 | 13 |
14 |
15 |
16 | 17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 | 27 |
28 |
29 |
30 |
31 |
32 | 33 | Cancel 34 |
35 |
36 |
37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /examples/blog/templates/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ entry.title }}{% endblock %} 4 | 5 | {% block content_title %}{{ entry.title }}{% endblock %} 6 | 7 | {% block extra_header %} 8 | {% if session.logged_in %} 9 |
  • Edit entry
  • 10 | {% endif %} 11 | {% endblock %} 12 | 13 | {% block content %} 14 |

    Created {{ entry.timestamp.strftime('%m/%d/%Y at %I:%M%p') }}

    15 | {{ entry.html_content }} 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /examples/blog/templates/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "create.html" %} 2 | 3 | {% block title %}Edit entry{% endblock %} 4 | 5 | {% block content_title %}Edit entry{% endblock %} 6 | 7 | {% block form_action %}{{ url_for('edit', slug=entry.slug) }}{% endblock %} 8 | 9 | {% block save_button %}Save changes{% endblock %} 10 | -------------------------------------------------------------------------------- /examples/blog/templates/includes/pagination.html: -------------------------------------------------------------------------------- 1 | {% if pagination.get_page_count() > 1 %} 2 | 14 | {% endif %} 15 | -------------------------------------------------------------------------------- /examples/blog/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Blog entries{% endblock %} 4 | 5 | {% block content_title %}{% if search %}Search "{{ search }}"{% else %}Blog entries{% endif %}{% endblock %} 6 | 7 | {% block content %} 8 | {% for entry in object_list %} 9 |

    10 | 11 | {{ entry.title }} 12 | 13 |

    14 |

    Created {{ entry.timestamp.strftime('%m/%d/%Y at %G:%I%p') }}

    15 | {% else %} 16 |

    No entries have been created yet.

    17 | {% endfor %} 18 | {% include "includes/pagination.html" %} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /examples/blog/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Log in{% endblock %} 4 | 5 | {% block content_title %}Log in{% endblock %} 6 | 7 | {% block content %} 8 |
    9 |
    10 | 11 |
    12 | 13 |
    14 |
    15 |
    16 |
    17 | 18 |
    19 |
    20 |
    21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /examples/blog/templates/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Log out{% endblock %} 4 | 5 | {% block content_title %}Log out{% endblock %} 6 | 7 | {% block content %} 8 |

    Click the button below to log out of the site.

    9 |
    10 | 11 |
    12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /examples/diary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from collections import OrderedDict 4 | import datetime 5 | from getpass import getpass 6 | import sys 7 | 8 | from peewee import * 9 | from playhouse.sqlcipher_ext import SqlCipherDatabase 10 | 11 | # Defer initialization of the database until the script is executed from the 12 | # command-line. 13 | db = SqlCipherDatabase(None) 14 | 15 | 16 | class Entry(Model): 17 | content = TextField() 18 | timestamp = DateTimeField(default=datetime.datetime.now) 19 | 20 | class Meta: 21 | database = db 22 | 23 | 24 | def initialize(passphrase): 25 | db.init('diary.db', passphrase=passphrase) 26 | db.create_tables([Entry]) 27 | 28 | 29 | def menu_loop(): 30 | choice = None 31 | while choice != 'q': 32 | for key, value in menu.items(): 33 | print('%s) %s' % (key, value.__doc__)) 34 | choice = input('Action: ').lower().strip() 35 | if choice in menu: 36 | menu[choice]() 37 | 38 | 39 | def add_entry(): 40 | """Add entry""" 41 | print('Enter your entry. Press ctrl+d when finished.') 42 | data = sys.stdin.read().strip() 43 | if data and input('Save entry? [Yn] ') != 'n': 44 | Entry.create(content=data) 45 | print('Saved successfully.') 46 | 47 | 48 | def view_entries(search_query=None): 49 | """View previous entries""" 50 | query = Entry.select().order_by(Entry.timestamp.desc()) 51 | if search_query: 52 | query = query.where(Entry.content.contains(search_query)) 53 | 54 | for entry in query: 55 | timestamp = entry.timestamp.strftime('%A %B %d, %Y %I:%M%p') 56 | print(timestamp) 57 | print('=' * len(timestamp)) 58 | print(entry.content) 59 | print('n) next entry') 60 | print('d) delete entry') 61 | print('q) return to main menu') 62 | action = input('Choice? (Ndq) ').lower().strip() 63 | if action == 'q': 64 | break 65 | elif action == 'd': 66 | entry.delete_instance() 67 | break 68 | 69 | 70 | def search_entries(): 71 | """Search entries""" 72 | view_entries(input('Search query: ')) 73 | 74 | 75 | menu = OrderedDict([ 76 | ('a', add_entry), 77 | ('v', view_entries), 78 | ('s', search_entries), 79 | ]) 80 | 81 | if __name__ == '__main__': 82 | # Collect the passphrase using a secure method. 83 | passphrase = getpass('Enter password: ') 84 | 85 | if not passphrase: 86 | sys.stderr.write('Passphrase required to access diary.\n') 87 | sys.stderr.flush() 88 | sys.exit(1) 89 | elif len(passphrase) < 8: 90 | sys.stderr.write('Passphrase must be at least 8 characters.\n') 91 | sys.stderr.flush() 92 | sys.exit(1) 93 | 94 | # Initialize the database. 95 | initialize(passphrase) 96 | menu_loop() 97 | -------------------------------------------------------------------------------- /examples/graph.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | 3 | 4 | db = SqliteDatabase(':memory:') 5 | 6 | 7 | class Base(Model): 8 | class Meta: 9 | database = db 10 | 11 | 12 | class Node(Base): 13 | name = TextField(primary_key=True) 14 | 15 | def outgoing(self): 16 | return (Node 17 | .select(Node, Edge.weight) 18 | .join(Edge, on=Edge.dest) 19 | .where(Edge.src == self) 20 | .objects()) 21 | 22 | def incoming(self): 23 | return (Node 24 | .select(Node, Edge.weight) 25 | .join(Edge, on=Edge.src) 26 | .where(Edge.dest == self) 27 | .objects()) 28 | 29 | 30 | class Edge(Base): 31 | src = ForeignKeyField(Node, backref='outgoing_edges') 32 | dest = ForeignKeyField(Node, backref='incoming_edges') 33 | weight = FloatField() 34 | 35 | 36 | db.create_tables([Node, Edge]) 37 | 38 | 39 | nodes = [Node.create(name=c) for c in 'abcde'] 40 | g = ( 41 | ('a', 'b', -1), 42 | ('a', 'c', 4), 43 | ('b', 'c', 3), 44 | ('b', 'd', 2), 45 | ('b', 'e', 2), 46 | ('d', 'b', 1), 47 | ('d', 'c', 5), 48 | ('e', 'd', -3)) 49 | for src, dest, wt in g: 50 | src_n, dest_n = nodes[ord(src) - ord('a')], nodes[ord(dest) - ord('a')] 51 | Edge.create(src=src_n, dest=dest_n, weight=wt) 52 | 53 | 54 | def bellman_ford(s): 55 | dist = {} 56 | pred = {} 57 | all_nodes = Node.select() 58 | for node in all_nodes: 59 | dist[node] = float('inf') 60 | pred[node] = None 61 | dist[s] = 0 62 | 63 | for _ in range(len(all_nodes) - 1): 64 | for u in all_nodes: 65 | for v in u.outgoing(): 66 | potential = dist[u] + v.weight 67 | if dist[v] > potential: 68 | dist[v] = potential 69 | pred[v] = u 70 | 71 | # Verify no negative-weight cycles. 72 | for u in all_nodes: 73 | for v in u.outgoing(): 74 | assert dist[v] <= dist[u] + v.weight 75 | 76 | return dist, pred 77 | 78 | def print_path(s, e): 79 | dist, pred = bellman_ford(s) 80 | distance = dist[e] 81 | route = [e] 82 | while e != s: 83 | route.append(pred[e]) 84 | e = pred[e] 85 | 86 | print(' -> '.join(v.name for v in route[::-1]) + ' (%s)' % distance) 87 | 88 | print_path(Node['a'], Node['c']) # a -> b -> c 89 | print_path(Node['a'], Node['d']) # a -> b -> e -> d 90 | print_path(Node['b'], Node['d']) # b -> e -> d 91 | -------------------------------------------------------------------------------- /examples/hexastore.py: -------------------------------------------------------------------------------- 1 | try: 2 | from functools import reduce 3 | except ImportError: 4 | pass 5 | import operator 6 | 7 | from peewee import * 8 | 9 | 10 | class Hexastore(object): 11 | def __init__(self, database=':memory:', **options): 12 | if isinstance(database, str): 13 | self.db = SqliteDatabase(database, **options) 14 | elif isinstance(database, Database): 15 | self.db = database 16 | else: 17 | raise ValueError('Expected database filename or a Database ' 18 | 'instance. Got: %s' % repr(database)) 19 | 20 | self.v = _VariableFactory() 21 | self.G = self.get_model() 22 | 23 | def get_model(self): 24 | class Graph(Model): 25 | subj = TextField() 26 | pred = TextField() 27 | obj = TextField() 28 | class Meta: 29 | database = self.db 30 | indexes = ( 31 | (('pred', 'obj'), False), 32 | (('obj', 'subj'), False), 33 | ) 34 | primary_key = CompositeKey('subj', 'pred', 'obj') 35 | 36 | self.db.create_tables([Graph]) 37 | return Graph 38 | 39 | def store(self, s, p, o): 40 | self.G.create(subj=s, pred=p, obj=o) 41 | 42 | def store_many(self, items): 43 | fields = [self.G.subj, self.G.pred, self.G.obj] 44 | self.G.insert_many(items, fields=fields).execute() 45 | 46 | def delete(self, s, p, o): 47 | return (self.G.delete() 48 | .where(self.G.subj == s, self.G.pred == p, self.G.obj == o) 49 | .execute()) 50 | 51 | def query(self, s=None, p=None, o=None): 52 | fields = (self.G.subj, self.G.pred, self.G.obj) 53 | expressions = [(f == v) for f, v in zip(fields, (s, p, o)) 54 | if v is not None] 55 | return self.G.select().where(*expressions) 56 | 57 | def search(self, *conditions): 58 | accum = [] 59 | binds = {} 60 | variables = set() 61 | fields = {'s': 'subj', 'p': 'pred', 'o': 'obj'} 62 | 63 | for i, condition in enumerate(conditions): 64 | if isinstance(condition, dict): 65 | condition = (condition['s'], condition['p'], condition['o']) 66 | 67 | GA = self.G.alias('g%s' % i) 68 | for part, val in zip('spo', condition): 69 | if isinstance(val, Variable): 70 | binds.setdefault(val, []) 71 | binds[val].append(getattr(GA, fields[part])) 72 | variables.add(val) 73 | else: 74 | accum.append(getattr(GA, fields[part]) == val) 75 | 76 | selection = [] 77 | sources = set() 78 | 79 | for var, fields in binds.items(): 80 | selection.append(fields[0].alias(var.name)) 81 | pairwise = [(fields[i - 1] == fields[i]) 82 | for i in range(1, len(fields))] 83 | if pairwise: 84 | accum.append(reduce(operator.and_, pairwise)) 85 | sources.update([field.source for field in fields]) 86 | 87 | return (self.G 88 | .select(*selection) 89 | .from_(*list(sources)) 90 | .where(*accum) 91 | .dicts()) 92 | 93 | 94 | class _VariableFactory(object): 95 | def __getattr__(self, name): 96 | return Variable(name) 97 | __call__ = __getattr__ 98 | 99 | class Variable(object): 100 | __slots__ = ('name',) 101 | 102 | def __init__(self, name): 103 | self.name = name 104 | 105 | def __hash__(self): 106 | return hash(self.name) 107 | 108 | def __repr__(self): 109 | return '' % self.name 110 | 111 | 112 | if __name__ == '__main__': 113 | h = Hexastore() 114 | 115 | data = ( 116 | ('charlie', 'likes', 'beanie'), 117 | ('charlie', 'likes', 'huey'), 118 | ('charlie', 'likes', 'mickey'), 119 | ('charlie', 'likes', 'scout'), 120 | ('charlie', 'likes', 'zaizee'), 121 | 122 | ('huey', 'likes', 'charlie'), 123 | ('huey', 'likes', 'scout'), 124 | ('huey', 'likes', 'zaizee'), 125 | 126 | ('mickey', 'likes', 'beanie'), 127 | ('mickey', 'likes', 'charlie'), 128 | ('mickey', 'likes', 'scout'), 129 | 130 | ('zaizee', 'likes', 'beanie'), 131 | ('zaizee', 'likes', 'charlie'), 132 | ('zaizee', 'likes', 'scout'), 133 | 134 | ('charlie', 'lives', 'topeka'), 135 | ('beanie', 'lives', 'heaven'), 136 | ('huey', 'lives', 'topeka'), 137 | ('mickey', 'lives', 'topeka'), 138 | ('scout', 'lives', 'heaven'), 139 | ('zaizee', 'lives', 'lawrence'), 140 | ) 141 | h.store_many(data) 142 | print('added %s items to store' % len(data)) 143 | 144 | print('\nwho lives in topeka?') 145 | for obj in h.query(p='lives', o='topeka'): 146 | print(obj.subj) 147 | 148 | print('\nmy friends in heaven?') 149 | X = h.v.x 150 | results = h.search(('charlie', 'likes', X), 151 | (X, 'lives', 'heaven')) 152 | for result in results: 153 | print(result['x']) 154 | 155 | print('\nmutual friends?') 156 | X = h.v.x 157 | Y = h.v.y 158 | results = h.search((X, 'likes', Y), (Y, 'likes', X)) 159 | for result in results: 160 | print(result['x'], ' <-> ', result['y']) 161 | 162 | print('\nliked by both charlie, huey and mickey?') 163 | X = h.v.x 164 | results = h.search(('charlie', 'likes', X), 165 | ('huey', 'likes', X), 166 | ('mickey', 'likes', X)) 167 | for result in results: 168 | print(result['x']) 169 | -------------------------------------------------------------------------------- /examples/reddit_ranking.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import math 3 | 4 | from peewee import * 5 | from peewee import query_to_string 6 | 7 | 8 | db = SqliteDatabase(':memory:') 9 | 10 | @db.func('log') 11 | def log(n, b): 12 | return math.log(n, b) 13 | 14 | class Base(Model): 15 | class Meta: 16 | database = db 17 | 18 | class Post(Base): 19 | content = TextField() 20 | timestamp = TimestampField() 21 | ups = IntegerField(default=0) 22 | downs = IntegerField(default=0) 23 | 24 | 25 | db.create_tables([Post]) 26 | 27 | # Populate with a number of posts. 28 | data = ( 29 | # Hours ago, ups, downs. 30 | (1, 5, 0), 31 | (1, 7, 1), 32 | (2, 10, 2), 33 | (2, 2, 0), 34 | (2, 1, 2), 35 | (3, 11, 2), 36 | (4, 20, 2), 37 | (4, 60, 12), 38 | (5, 3, 0), 39 | (5, 1, 0), 40 | (6, 30, 3), 41 | (6, 30, 20), 42 | (7, 45, 10), 43 | (7, 45, 20), 44 | (8, 11, 2), 45 | (8, 3, 1), 46 | ) 47 | 48 | now = datetime.datetime.now() 49 | Post.insert_many([ 50 | ('post %2dh %2d up, %2d down' % (hours, ups, downs), 51 | now - datetime.timedelta(seconds=hours * 3600), 52 | ups, 53 | downs) for hours, ups, downs in data]).execute() 54 | 55 | 56 | score = (Post.ups - Post.downs) 57 | order = fn.log(fn.max(fn.abs(score), 1), 10) 58 | sign = Case(None, ( 59 | ((score > 0), 1), 60 | ((score < 0), -1)), 0) 61 | seconds = (Post.timestamp) - 1134028003 62 | 63 | hot = (sign * order) + (seconds / 45000) 64 | query = Post.select(Post.content, hot.alias('score')).order_by(SQL('score').desc()) 65 | #print(query_to_string(query)) 66 | print('Posts, ordered best-to-worse:') 67 | 68 | for post in query: 69 | print(post.content, round(post.score, 3)) 70 | -------------------------------------------------------------------------------- /examples/sqlite_fts_compression.py: -------------------------------------------------------------------------------- 1 | # 2 | # Small example demonstrating the use of zlib compression with the Sqlite 3 | # full-text search extension. 4 | # 5 | import zlib 6 | 7 | from peewee import * 8 | from playhouse.sqlite_ext import * 9 | 10 | 11 | db = SqliteExtDatabase(':memory:') 12 | 13 | class SearchIndex(FTSModel): 14 | content = SearchField() 15 | 16 | class Meta: 17 | database = db 18 | 19 | 20 | @db.func('zlib_compress') 21 | def _zlib_compress(data): 22 | if data is not None: 23 | if isinstance(data, str): 24 | data = data.encode('utf8') 25 | return zlib.compress(data, 9) 26 | 27 | @db.func('zlib_decompress') 28 | def _zlib_decompress(data): 29 | if data is not None: 30 | return zlib.decompress(data) 31 | 32 | 33 | SearchIndex.create_table( 34 | tokenize='porter', 35 | compress='zlib_compress', 36 | uncompress='zlib_decompress') 37 | 38 | phrases = [ 39 | 'A faith is a necessity to a man. Woe to him who believes in nothing.', 40 | ('All who call on God in true faith, earnestly from the heart, will ' 41 | 'certainly be heard, and will receive what they have asked and desired.'), 42 | ('Be faithful in small things because it is in them that your strength ' 43 | 'lies.'), 44 | ('Faith consists in believing when it is beyond the power of reason to ' 45 | 'believe.'), 46 | ('Faith has to do with things that are not seen and hope with things that ' 47 | 'are not at hand.')] 48 | 49 | for phrase in phrases: 50 | SearchIndex.create(content=phrase) 51 | 52 | # Use the simple ranking algorithm. 53 | query = SearchIndex.search('faith things', with_score=True) 54 | for row in query: 55 | print(round(row.score, 2), row.content.decode('utf8')) 56 | 57 | print('---') 58 | 59 | # Use the Okapi-BM25 ranking algorithm. 60 | query = SearchIndex.search_bm25('believe', with_score=True) 61 | for row in query: 62 | print(round(row.score, 2), row.content.decode('utf8')) 63 | 64 | db.close() 65 | -------------------------------------------------------------------------------- /examples/twitter/requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | peewee 3 | -------------------------------------------------------------------------------- /examples/twitter/run_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | sys.path.insert(0, '../..') 5 | 6 | from app import app, create_tables 7 | create_tables() 8 | app.run() 9 | -------------------------------------------------------------------------------- /examples/twitter/static/style.css: -------------------------------------------------------------------------------- 1 | body { font-family: sans-serif; background: #eee; } 2 | a, h1, h2 { color: #377BA8; } 3 | h1, h2 { font-family: 'Georgia', serif; margin: 0; } 4 | h1 { border-bottom: 2px solid #eee; } 5 | h2 { font-size: 1.2em; } 6 | 7 | .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; 8 | padding: 0.8em; background: white; } 9 | .page ul { list-style-type: none; } 10 | .page li { clear: both; } 11 | .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; 12 | margin-bottom: 1em; background: #fafafa; } 13 | .flash { background: #CEE5F5; padding: 0.5em; 14 | border: 1px solid #AACBE2; } 15 | .avatar { display: block; float: left; margin: 0 10px 0 0; } 16 | .message-content { min-height: 80px; } 17 | -------------------------------------------------------------------------------- /examples/twitter/templates/create.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

    Create

    4 |
    5 |
    6 |
    Message:
    7 |
    8 |
    9 |
    10 |
    11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /examples/twitter/templates/homepage.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

    Home

    4 |

    Welcome to the site!

    5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /examples/twitter/templates/includes/message.html: -------------------------------------------------------------------------------- 1 | 2 |

    {{ message.content|urlize }}

    3 | -------------------------------------------------------------------------------- /examples/twitter/templates/includes/pagination.html: -------------------------------------------------------------------------------- 1 | {% if page > 1 %} 2 | 3 | {% endif %} 4 | {% if page < pages %} 5 | 6 | {% endif %} 7 | -------------------------------------------------------------------------------- /examples/twitter/templates/join.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

    Join

    4 |
    5 |
    6 |
    Username:
    7 |
    8 |
    Password:
    9 |
    10 |
    Email:
    11 |
    12 |

    (used for gravatar)

    13 |
    14 |
    15 |
    16 |
    17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /examples/twitter/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | Tweepee 3 | 4 |
    5 |

    Tweepee

    6 |
    7 | {% if not session.logged_in %} 8 | log in 9 | join 10 | {% else %} 11 | public timeline 12 | create 13 | log out 14 | {% endif %} 15 |
    16 | {% for message in get_flashed_messages() %} 17 |
    {{ message }}
    18 | {% endfor %} 19 | {% block body %}{% endblock %} 20 |
    21 | -------------------------------------------------------------------------------- /examples/twitter/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

    Login

    4 | {% if error %}

    Error: {{ error }}{% endif %} 5 |

    6 |
    7 |
    Username: 8 |
    9 |
    Password: 10 |
    11 |
    12 |
    13 |
    14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /examples/twitter/templates/private_messages.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

    Private Timeline

    4 |
      5 | {% for message in message_list %} 6 |
    • {% include "includes/message.html" %}
    • 7 | {% endfor %} 8 |
    9 | {% include "includes/pagination.html" %} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /examples/twitter/templates/public_messages.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

    Public Timeline

    4 |
      5 | {% for message in message_list %} 6 |
    • {% include "includes/message.html" %}
    • 7 | {% endfor %} 8 |
    9 | {% include "includes/pagination.html" %} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /examples/twitter/templates/user_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

    Messages from {{ user.username }}

    4 | {% if current_user %} 5 | {% if user.username != current_user.username %} 6 | {% if current_user|is_following(user) %} 7 |
    8 | 9 |
    10 | {% else %} 11 |
    12 | 13 |
    14 | {% endif %} 15 | {% endif %} 16 | {% endif %} 17 |
      18 | {% for message in message_list %} 19 |
    • {% include "includes/message.html" %}
    • 20 | {% endfor %} 21 |
    22 | {% include "includes/pagination.html" %} 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /examples/twitter/templates/user_followers.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

    Followers

    4 | 9 | {% include "includes/pagination.html" %} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /examples/twitter/templates/user_following.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

    Following

    4 | 9 | {% include "includes/pagination.html" %} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /examples/twitter/templates/user_list.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

    Users

    4 | 9 | {% include "includes/pagination.html" %} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /playhouse/README.md: -------------------------------------------------------------------------------- 1 | ## Playhouse 2 | 3 | The `playhouse` namespace contains numerous extensions to Peewee. These include vendor-specific database extensions, high-level abstractions to simplify working with databases, and tools for low-level database operations and introspection. 4 | 5 | ### Vendor extensions 6 | 7 | * [SQLite extensions](http://docs.peewee-orm.com/en/latest/peewee/sqlite_ext.html) 8 | * Full-text search (FTS3/4/5) 9 | * BM25 ranking algorithm implemented as SQLite C extension, backported to FTS4 10 | * Virtual tables and C extensions 11 | * Closure tables 12 | * JSON extension support 13 | * LSM1 (key/value database) support 14 | * BLOB API 15 | * Online backup API 16 | * [APSW extensions](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#apsw): use Peewee with the powerful [APSW](https://github.com/rogerbinns/apsw) SQLite driver. 17 | * [SQLCipher](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqlcipher-ext): encrypted SQLite databases. 18 | * [SqliteQ](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqliteq): dedicated writer thread for multi-threaded SQLite applications. [More info here](http://charlesleifer.com/blog/multi-threaded-sqlite-without-the-operationalerrors/). 19 | * [Postgresql extensions](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#postgres-ext) 20 | * JSON and JSONB 21 | * HStore 22 | * Arrays 23 | * Server-side cursors 24 | * Full-text search 25 | * [MySQL extensions](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#mysql-ext) 26 | 27 | ### High-level libraries 28 | 29 | * [Extra fields](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#extra-fields) 30 | * Compressed field 31 | * PickleField 32 | * [Shortcuts / helpers](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#shortcuts) 33 | * Model-to-dict serializer 34 | * Dict-to-model deserializer 35 | * [Hybrid attributes](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#hybrid) 36 | * [Signals](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#signals): pre/post-save, pre/post-delete, pre-init. 37 | * [Dataset](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#dataset): high-level API for working with databases popuarlized by the [project of the same name](https://dataset.readthedocs.io/). 38 | * [Key/Value Store](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#kv): key/value store using SQLite. Supports *smart indexing*, for *Pandas*-style queries. 39 | 40 | ### Database management and framework support 41 | 42 | * [pwiz](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#pwiz): generate model code from a pre-existing database. 43 | * [Schema migrations](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#migrate): modify your schema using high-level APIs. Even supports dropping or renaming columns in SQLite. 44 | * [Connection pool](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#pool): simple connection pooling. 45 | * [Reflection](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#reflection): low-level, cross-platform database introspection 46 | * [Database URLs](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#db-url): use URLs to connect to database 47 | * [Test utils](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#test-utils): helpers for unit-testing Peewee applications. 48 | * [Flask utils](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#flask-utils): paginated object lists, database connection management, and more. 49 | -------------------------------------------------------------------------------- /playhouse/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/playhouse/__init__.py -------------------------------------------------------------------------------- /playhouse/_pysqlite/cache.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/playhouse/_pysqlite/cache.h -------------------------------------------------------------------------------- /playhouse/_pysqlite/connection.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/playhouse/_pysqlite/connection.h -------------------------------------------------------------------------------- /playhouse/_pysqlite/module.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/playhouse/_pysqlite/module.h -------------------------------------------------------------------------------- /playhouse/_sqlite_udf.pyx: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | import sys 3 | from difflib import SequenceMatcher 4 | from random import randint 5 | 6 | 7 | IS_PY3K = sys.version_info[0] == 3 8 | 9 | # String UDF. 10 | def damerau_levenshtein_dist(s1, s2): 11 | cdef: 12 | int i, j, del_cost, add_cost, sub_cost 13 | int s1_len = len(s1), s2_len = len(s2) 14 | list one_ago, two_ago, current_row 15 | list zeroes = [0] * (s2_len + 1) 16 | 17 | if IS_PY3K: 18 | current_row = list(range(1, s2_len + 2)) 19 | else: 20 | current_row = range(1, s2_len + 2) 21 | 22 | current_row[-1] = 0 23 | one_ago = None 24 | 25 | for i in range(s1_len): 26 | two_ago = one_ago 27 | one_ago = current_row 28 | current_row = list(zeroes) 29 | current_row[-1] = i + 1 30 | for j in range(s2_len): 31 | del_cost = one_ago[j] + 1 32 | add_cost = current_row[j - 1] + 1 33 | sub_cost = one_ago[j - 1] + (s1[i] != s2[j]) 34 | current_row[j] = min(del_cost, add_cost, sub_cost) 35 | 36 | # Handle transpositions. 37 | if (i > 0 and j > 0 and s1[i] == s2[j - 1] 38 | and s1[i-1] == s2[j] and s1[i] != s2[j]): 39 | current_row[j] = min(current_row[j], two_ago[j - 2] + 1) 40 | 41 | return current_row[s2_len - 1] 42 | 43 | # String UDF. 44 | def levenshtein_dist(a, b): 45 | cdef: 46 | int add, delete, change 47 | int i, j 48 | int n = len(a), m = len(b) 49 | list current, previous 50 | list zeroes 51 | 52 | if n > m: 53 | a, b = b, a 54 | n, m = m, n 55 | 56 | zeroes = [0] * (m + 1) 57 | 58 | if IS_PY3K: 59 | current = list(range(n + 1)) 60 | else: 61 | current = range(n + 1) 62 | 63 | for i in range(1, m + 1): 64 | previous = current 65 | current = list(zeroes) 66 | current[0] = i 67 | 68 | for j in range(1, n + 1): 69 | add = previous[j] + 1 70 | delete = current[j - 1] + 1 71 | change = previous[j - 1] 72 | if a[j - 1] != b[i - 1]: 73 | change +=1 74 | current[j] = min(add, delete, change) 75 | 76 | return current[n] 77 | 78 | # String UDF. 79 | def str_dist(a, b): 80 | cdef: 81 | int t = 0 82 | 83 | for i in SequenceMatcher(None, a, b).get_opcodes(): 84 | if i[0] == 'equal': 85 | continue 86 | t = t + max(i[4] - i[3], i[2] - i[1]) 87 | return t 88 | 89 | # Math Aggregate. 90 | cdef class median(object): 91 | cdef: 92 | int ct 93 | list items 94 | 95 | def __init__(self): 96 | self.ct = 0 97 | self.items = [] 98 | 99 | cdef selectKth(self, int k, int s=0, int e=-1): 100 | cdef: 101 | int idx 102 | if e < 0: 103 | e = len(self.items) 104 | idx = randint(s, e-1) 105 | idx = self.partition_k(idx, s, e) 106 | if idx > k: 107 | return self.selectKth(k, s, idx) 108 | elif idx < k: 109 | return self.selectKth(k, idx + 1, e) 110 | else: 111 | return self.items[idx] 112 | 113 | cdef int partition_k(self, int pi, int s, int e): 114 | cdef: 115 | int i, x 116 | 117 | val = self.items[pi] 118 | # Swap pivot w/last item. 119 | self.items[e - 1], self.items[pi] = self.items[pi], self.items[e - 1] 120 | x = s 121 | for i in range(s, e): 122 | if self.items[i] < val: 123 | self.items[i], self.items[x] = self.items[x], self.items[i] 124 | x += 1 125 | self.items[x], self.items[e-1] = self.items[e-1], self.items[x] 126 | return x 127 | 128 | def step(self, item): 129 | self.items.append(item) 130 | self.ct += 1 131 | 132 | def finalize(self): 133 | if self.ct == 0: 134 | return None 135 | elif self.ct < 3: 136 | return self.items[0] 137 | else: 138 | return self.selectKth(self.ct // 2) 139 | -------------------------------------------------------------------------------- /playhouse/apsw_ext.py: -------------------------------------------------------------------------------- 1 | """ 2 | Peewee integration with APSW, "another python sqlite wrapper". 3 | 4 | Project page: https://rogerbinns.github.io/apsw/ 5 | 6 | APSW is a really neat library that provides a thin wrapper on top of SQLite's 7 | C interface. 8 | 9 | Here are just a few reasons to use APSW, taken from the documentation: 10 | 11 | * APSW gives all functionality of SQLite, including virtual tables, virtual 12 | file system, blob i/o, backups and file control. 13 | * Connections can be shared across threads without any additional locking. 14 | * Transactions are managed explicitly by your code. 15 | * APSW can handle nested transactions. 16 | * Unicode is handled correctly. 17 | * APSW is faster. 18 | """ 19 | import apsw 20 | from peewee import * 21 | from peewee import __exception_wrapper__ 22 | from peewee import BooleanField as _BooleanField 23 | from peewee import DateField as _DateField 24 | from peewee import DateTimeField as _DateTimeField 25 | from peewee import DecimalField as _DecimalField 26 | from peewee import Insert 27 | from peewee import TimeField as _TimeField 28 | from peewee import logger 29 | 30 | from playhouse.sqlite_ext import SqliteExtDatabase 31 | 32 | 33 | class APSWDatabase(SqliteExtDatabase): 34 | server_version = tuple(int(i) for i in apsw.sqlitelibversion().split('.')) 35 | 36 | def __init__(self, database, **kwargs): 37 | self._modules = {} 38 | super(APSWDatabase, self).__init__(database, **kwargs) 39 | 40 | def register_module(self, mod_name, mod_inst): 41 | self._modules[mod_name] = mod_inst 42 | if not self.is_closed(): 43 | self.connection().createmodule(mod_name, mod_inst) 44 | 45 | def unregister_module(self, mod_name): 46 | del(self._modules[mod_name]) 47 | 48 | def _connect(self): 49 | conn = apsw.Connection(self.database, **self.connect_params) 50 | if self._timeout is not None: 51 | conn.setbusytimeout(self._timeout * 1000) 52 | try: 53 | self._add_conn_hooks(conn) 54 | except: 55 | conn.close() 56 | raise 57 | return conn 58 | 59 | def _add_conn_hooks(self, conn): 60 | super(APSWDatabase, self)._add_conn_hooks(conn) 61 | self._load_modules(conn) # APSW-only. 62 | 63 | def _load_modules(self, conn): 64 | for mod_name, mod_inst in self._modules.items(): 65 | conn.createmodule(mod_name, mod_inst) 66 | return conn 67 | 68 | def _load_aggregates(self, conn): 69 | for name, (klass, num_params) in self._aggregates.items(): 70 | def make_aggregate(): 71 | return (klass(), klass.step, klass.finalize) 72 | conn.createaggregatefunction(name, make_aggregate) 73 | 74 | def _load_collations(self, conn): 75 | for name, fn in self._collations.items(): 76 | conn.createcollation(name, fn) 77 | 78 | def _load_functions(self, conn): 79 | for name, (fn, num_params, deterministic) in self._functions.items(): 80 | args = (deterministic,) if deterministic else () 81 | conn.createscalarfunction(name, fn, num_params, *args) 82 | 83 | def _load_extensions(self, conn): 84 | conn.enableloadextension(True) 85 | for extension in self._extensions: 86 | conn.loadextension(extension) 87 | 88 | def load_extension(self, extension): 89 | self._extensions.add(extension) 90 | if not self.is_closed(): 91 | conn = self.connection() 92 | conn.enableloadextension(True) 93 | conn.loadextension(extension) 94 | 95 | def last_insert_id(self, cursor, query_type=None): 96 | if not self.returning_clause: 97 | return cursor.getconnection().last_insert_rowid() 98 | elif query_type == Insert.SIMPLE: 99 | try: 100 | return cursor[0][0] 101 | except (AttributeError, IndexError, TypeError): 102 | pass 103 | return cursor 104 | 105 | def rows_affected(self, cursor): 106 | try: 107 | return cursor.getconnection().changes() 108 | except AttributeError: 109 | return cursor.cursor.getconnection().changes() # RETURNING query. 110 | 111 | def begin(self, lock_type='deferred'): 112 | self.cursor().execute('begin %s;' % lock_type) 113 | 114 | def commit(self): 115 | with __exception_wrapper__: 116 | curs = self.cursor() 117 | if curs.getconnection().getautocommit(): 118 | return False 119 | curs.execute('commit;') 120 | return True 121 | 122 | def rollback(self): 123 | with __exception_wrapper__: 124 | curs = self.cursor() 125 | if curs.getconnection().getautocommit(): 126 | return False 127 | curs.execute('rollback;') 128 | return True 129 | 130 | def execute_sql(self, sql, params=None): 131 | logger.debug((sql, params)) 132 | with __exception_wrapper__: 133 | cursor = self.cursor() 134 | cursor.execute(sql, params or ()) 135 | return cursor 136 | 137 | 138 | def nh(s, v): 139 | if v is not None: 140 | return str(v) 141 | 142 | class BooleanField(_BooleanField): 143 | def db_value(self, v): 144 | v = super(BooleanField, self).db_value(v) 145 | if v is not None: 146 | return v and 1 or 0 147 | 148 | class DateField(_DateField): 149 | db_value = nh 150 | 151 | class TimeField(_TimeField): 152 | db_value = nh 153 | 154 | class DateTimeField(_DateTimeField): 155 | db_value = nh 156 | 157 | class DecimalField(_DecimalField): 158 | db_value = nh 159 | -------------------------------------------------------------------------------- /playhouse/db_url.py: -------------------------------------------------------------------------------- 1 | try: 2 | from urlparse import parse_qsl, unquote, urlparse 3 | except ImportError: 4 | from urllib.parse import parse_qsl, unquote, urlparse 5 | 6 | from peewee import * 7 | from playhouse.cockroachdb import CockroachDatabase 8 | from playhouse.cockroachdb import PooledCockroachDatabase 9 | from playhouse.pool import PooledMySQLDatabase 10 | from playhouse.pool import PooledPostgresqlDatabase 11 | from playhouse.pool import PooledPsycopg3Database 12 | from playhouse.pool import PooledSqliteDatabase 13 | from playhouse.pool import PooledSqliteExtDatabase 14 | from playhouse.psycopg3_ext import Psycopg3Database 15 | from playhouse.sqlite_ext import SqliteExtDatabase 16 | 17 | 18 | schemes = { 19 | 'cockroachdb': CockroachDatabase, 20 | 'cockroachdb+pool': PooledCockroachDatabase, 21 | 'crdb': CockroachDatabase, 22 | 'crdb+pool': PooledCockroachDatabase, 23 | 'mysql': MySQLDatabase, 24 | 'mysql+pool': PooledMySQLDatabase, 25 | 'postgres': PostgresqlDatabase, 26 | 'postgresql': PostgresqlDatabase, 27 | 'postgres+pool': PooledPostgresqlDatabase, 28 | 'postgresql+pool': PooledPostgresqlDatabase, 29 | 'psycopg3': Psycopg3Database, 30 | 'psycopg3+pool': PooledPsycopg3Database, 31 | 'sqlite': SqliteDatabase, 32 | 'sqliteext': SqliteExtDatabase, 33 | 'sqlite+pool': PooledSqliteDatabase, 34 | 'sqliteext+pool': PooledSqliteExtDatabase, 35 | } 36 | 37 | def register_database(db_class, *names): 38 | global schemes 39 | for name in names: 40 | schemes[name] = db_class 41 | 42 | def parseresult_to_dict(parsed, unquote_password=False, unquote_user=False): 43 | 44 | # urlparse in python 2.6 is broken so query will be empty and instead 45 | # appended to path complete with '?' 46 | path = parsed.path[1:] # Ignore leading '/'. 47 | query = parsed.query 48 | 49 | connect_kwargs = {'database': path} 50 | if parsed.username: 51 | connect_kwargs['user'] = parsed.username 52 | if unquote_user: 53 | connect_kwargs['user'] = unquote(connect_kwargs['user']) 54 | if parsed.password: 55 | connect_kwargs['password'] = parsed.password 56 | if unquote_password: 57 | connect_kwargs['password'] = unquote(connect_kwargs['password']) 58 | if parsed.hostname: 59 | connect_kwargs['host'] = parsed.hostname 60 | if parsed.port: 61 | connect_kwargs['port'] = parsed.port 62 | 63 | # Adjust parameters for MySQL. 64 | if parsed.scheme == 'mysql' and 'password' in connect_kwargs: 65 | connect_kwargs['passwd'] = connect_kwargs.pop('password') 66 | elif 'sqlite' in parsed.scheme and not connect_kwargs['database']: 67 | connect_kwargs['database'] = ':memory:' 68 | 69 | # Get additional connection args from the query string 70 | qs_args = parse_qsl(query, keep_blank_values=True) 71 | for key, value in qs_args: 72 | if value.lower() == 'false': 73 | value = False 74 | elif value.lower() == 'true': 75 | value = True 76 | elif value.isdigit(): 77 | value = int(value) 78 | elif '.' in value and all(p.isdigit() for p in value.split('.', 1)): 79 | try: 80 | value = float(value) 81 | except ValueError: 82 | pass 83 | elif value.lower() in ('null', 'none'): 84 | value = None 85 | 86 | connect_kwargs[key] = value 87 | 88 | return connect_kwargs 89 | 90 | def parse(url, unquote_password=False, unquote_user=False): 91 | parsed = urlparse(url) 92 | return parseresult_to_dict(parsed, unquote_password, unquote_user) 93 | 94 | def connect(url, unquote_password=False, unquote_user=False, **connect_params): 95 | parsed = urlparse(url) 96 | connect_kwargs = parseresult_to_dict(parsed, unquote_password, unquote_user) 97 | connect_kwargs.update(connect_params) 98 | database_class = schemes.get(parsed.scheme) 99 | 100 | if database_class is None: 101 | if database_class in schemes: 102 | raise RuntimeError('Attempted to use "%s" but a required library ' 103 | 'could not be imported.' % parsed.scheme) 104 | else: 105 | raise RuntimeError('Unrecognized or unsupported scheme: "%s".' % 106 | parsed.scheme) 107 | 108 | return database_class(**connect_kwargs) 109 | 110 | # Conditionally register additional databases. 111 | try: 112 | from playhouse.pool import PooledPostgresqlExtDatabase 113 | except ImportError: 114 | pass 115 | else: 116 | register_database( 117 | PooledPostgresqlExtDatabase, 118 | 'postgresext+pool', 119 | 'postgresqlext+pool') 120 | 121 | try: 122 | from playhouse.apsw_ext import APSWDatabase 123 | except ImportError: 124 | pass 125 | else: 126 | register_database(APSWDatabase, 'apsw') 127 | 128 | try: 129 | from playhouse.postgres_ext import PostgresqlExtDatabase 130 | except ImportError: 131 | pass 132 | else: 133 | register_database(PostgresqlExtDatabase, 'postgresext', 'postgresqlext') 134 | -------------------------------------------------------------------------------- /playhouse/fields.py: -------------------------------------------------------------------------------- 1 | try: 2 | import bz2 3 | except ImportError: 4 | bz2 = None 5 | try: 6 | import zlib 7 | except ImportError: 8 | zlib = None 9 | try: 10 | import cPickle as pickle 11 | except ImportError: 12 | import pickle 13 | 14 | from peewee import BlobField 15 | from peewee import buffer_type 16 | 17 | 18 | class CompressedField(BlobField): 19 | ZLIB = 'zlib' 20 | BZ2 = 'bz2' 21 | algorithm_to_import = { 22 | ZLIB: zlib, 23 | BZ2: bz2, 24 | } 25 | 26 | def __init__(self, compression_level=6, algorithm=ZLIB, *args, 27 | **kwargs): 28 | self.compression_level = compression_level 29 | if algorithm not in self.algorithm_to_import: 30 | raise ValueError('Unrecognized algorithm %s' % algorithm) 31 | compress_module = self.algorithm_to_import[algorithm] 32 | if compress_module is None: 33 | raise ValueError('Missing library required for %s.' % algorithm) 34 | 35 | self.algorithm = algorithm 36 | self.compress = compress_module.compress 37 | self.decompress = compress_module.decompress 38 | super(CompressedField, self).__init__(*args, **kwargs) 39 | 40 | def python_value(self, value): 41 | if value is not None: 42 | return self.decompress(value) 43 | 44 | def db_value(self, value): 45 | if value is not None: 46 | return self._constructor( 47 | self.compress(value, self.compression_level)) 48 | 49 | 50 | class PickleField(BlobField): 51 | def python_value(self, value): 52 | if value is not None: 53 | if isinstance(value, buffer_type): 54 | value = bytes(value) 55 | return pickle.loads(value) 56 | 57 | def db_value(self, value): 58 | if value is not None: 59 | pickled = pickle.dumps(value, pickle.HIGHEST_PROTOCOL) 60 | return self._constructor(pickled) 61 | -------------------------------------------------------------------------------- /playhouse/hybrid.py: -------------------------------------------------------------------------------- 1 | from peewee import ModelDescriptor 2 | 3 | 4 | # Hybrid methods/attributes, based on similar functionality in SQLAlchemy: 5 | # http://docs.sqlalchemy.org/en/improve_toc/orm/extensions/hybrid.html 6 | class hybrid_method(ModelDescriptor): 7 | def __init__(self, func, expr=None): 8 | self.func = func 9 | self.expr = expr or func 10 | 11 | def __get__(self, instance, instance_type): 12 | if instance is None: 13 | return self.expr.__get__(instance_type, instance_type.__class__) 14 | return self.func.__get__(instance, instance_type) 15 | 16 | def expression(self, expr): 17 | self.expr = expr 18 | return self 19 | 20 | 21 | class hybrid_property(ModelDescriptor): 22 | def __init__(self, fget, fset=None, fdel=None, expr=None): 23 | self.fget = fget 24 | self.fset = fset 25 | self.fdel = fdel 26 | self.expr = expr or fget 27 | 28 | def __get__(self, instance, instance_type): 29 | if instance is None: 30 | return self.expr(instance_type) 31 | return self.fget(instance) 32 | 33 | def __set__(self, instance, value): 34 | if self.fset is None: 35 | raise AttributeError('Cannot set attribute.') 36 | self.fset(instance, value) 37 | 38 | def __delete__(self, instance): 39 | if self.fdel is None: 40 | raise AttributeError('Cannot delete attribute.') 41 | self.fdel(instance) 42 | 43 | def setter(self, fset): 44 | self.fset = fset 45 | return self 46 | 47 | def deleter(self, fdel): 48 | self.fdel = fdel 49 | return self 50 | 51 | def expression(self, expr): 52 | self.expr = expr 53 | return self 54 | -------------------------------------------------------------------------------- /playhouse/kv.py: -------------------------------------------------------------------------------- 1 | import operator 2 | 3 | from peewee import * 4 | from peewee import sqlite3 5 | from peewee import Expression 6 | from playhouse.fields import PickleField 7 | try: 8 | from playhouse.sqlite_ext import CSqliteExtDatabase as SqliteExtDatabase 9 | except ImportError: 10 | from playhouse.sqlite_ext import SqliteExtDatabase 11 | 12 | 13 | Sentinel = type('Sentinel', (object,), {}) 14 | 15 | 16 | class KeyValue(object): 17 | """ 18 | Persistent dictionary. 19 | 20 | :param Field key_field: field to use for key. Defaults to CharField. 21 | :param Field value_field: field to use for value. Defaults to PickleField. 22 | :param bool ordered: data should be returned in key-sorted order. 23 | :param Database database: database where key/value data is stored. 24 | :param str table_name: table name for data. 25 | """ 26 | def __init__(self, key_field=None, value_field=None, ordered=False, 27 | database=None, table_name='keyvalue'): 28 | if key_field is None: 29 | key_field = CharField(max_length=255, primary_key=True) 30 | if not key_field.primary_key: 31 | raise ValueError('key_field must have primary_key=True.') 32 | 33 | if value_field is None: 34 | value_field = PickleField() 35 | 36 | self._key_field = key_field 37 | self._value_field = value_field 38 | self._ordered = ordered 39 | self._database = database or SqliteExtDatabase(':memory:') 40 | self._table_name = table_name 41 | support_on_conflict = (isinstance(self._database, PostgresqlDatabase) or 42 | (isinstance(self._database, SqliteDatabase) and 43 | self._database.server_version >= (3, 24))) 44 | if support_on_conflict: 45 | self.upsert = self._postgres_upsert 46 | self.update = self._postgres_update 47 | else: 48 | self.upsert = self._upsert 49 | self.update = self._update 50 | 51 | self.model = self.create_model() 52 | self.key = self.model.key 53 | self.value = self.model.value 54 | 55 | # Ensure table exists. 56 | self.model.create_table() 57 | 58 | def create_model(self): 59 | class KeyValue(Model): 60 | key = self._key_field 61 | value = self._value_field 62 | class Meta: 63 | database = self._database 64 | table_name = self._table_name 65 | return KeyValue 66 | 67 | def query(self, *select): 68 | query = self.model.select(*select).tuples() 69 | if self._ordered: 70 | query = query.order_by(self.key) 71 | return query 72 | 73 | def convert_expression(self, expr): 74 | if not isinstance(expr, Expression): 75 | return (self.key == expr), True 76 | return expr, False 77 | 78 | def __contains__(self, key): 79 | expr, _ = self.convert_expression(key) 80 | return self.model.select().where(expr).exists() 81 | 82 | def __len__(self): 83 | return len(self.model) 84 | 85 | def __getitem__(self, expr): 86 | converted, is_single = self.convert_expression(expr) 87 | query = self.query(self.value).where(converted) 88 | item_getter = operator.itemgetter(0) 89 | result = [item_getter(row) for row in query] 90 | if len(result) == 0 and is_single: 91 | raise KeyError(expr) 92 | elif is_single: 93 | return result[0] 94 | return result 95 | 96 | def _upsert(self, key, value): 97 | (self.model 98 | .insert(key=key, value=value) 99 | .on_conflict('replace') 100 | .execute()) 101 | 102 | def _postgres_upsert(self, key, value): 103 | (self.model 104 | .insert(key=key, value=value) 105 | .on_conflict(conflict_target=[self.key], 106 | preserve=[self.value]) 107 | .execute()) 108 | 109 | def __setitem__(self, expr, value): 110 | if isinstance(expr, Expression): 111 | self.model.update(value=value).where(expr).execute() 112 | else: 113 | self.upsert(expr, value) 114 | 115 | def __delitem__(self, expr): 116 | converted, _ = self.convert_expression(expr) 117 | self.model.delete().where(converted).execute() 118 | 119 | def __iter__(self): 120 | return iter(self.query().execute()) 121 | 122 | def keys(self): 123 | return map(operator.itemgetter(0), self.query(self.key)) 124 | 125 | def values(self): 126 | return map(operator.itemgetter(0), self.query(self.value)) 127 | 128 | def items(self): 129 | return iter(self.query().execute()) 130 | 131 | def _update(self, __data=None, **mapping): 132 | if __data is not None: 133 | mapping.update(__data) 134 | return (self.model 135 | .insert_many(list(mapping.items()), 136 | fields=[self.key, self.value]) 137 | .on_conflict('replace') 138 | .execute()) 139 | 140 | def _postgres_update(self, __data=None, **mapping): 141 | if __data is not None: 142 | mapping.update(__data) 143 | return (self.model 144 | .insert_many(list(mapping.items()), 145 | fields=[self.key, self.value]) 146 | .on_conflict(conflict_target=[self.key], 147 | preserve=[self.value]) 148 | .execute()) 149 | 150 | def get(self, key, default=None): 151 | try: 152 | return self[key] 153 | except KeyError: 154 | return default 155 | 156 | def setdefault(self, key, default=None): 157 | try: 158 | return self[key] 159 | except KeyError: 160 | self[key] = default 161 | return default 162 | 163 | def pop(self, key, default=Sentinel): 164 | with self._database.atomic(): 165 | try: 166 | result = self[key] 167 | except KeyError: 168 | if default is Sentinel: 169 | raise 170 | return default 171 | del self[key] 172 | 173 | return result 174 | 175 | def clear(self): 176 | self.model.delete().execute() 177 | -------------------------------------------------------------------------------- /playhouse/mysql_ext.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | try: 4 | import mysql.connector as mysql_connector 5 | except ImportError: 6 | mysql_connector = None 7 | try: 8 | import mariadb 9 | except ImportError: 10 | mariadb = None 11 | 12 | from peewee import ImproperlyConfigured 13 | from peewee import Insert 14 | from peewee import MySQLDatabase 15 | from peewee import Node 16 | from peewee import NodeList 17 | from peewee import SQL 18 | from peewee import TextField 19 | from peewee import fn 20 | from peewee import __deprecated__ 21 | 22 | 23 | class MySQLConnectorDatabase(MySQLDatabase): 24 | def _connect(self): 25 | if mysql_connector is None: 26 | raise ImproperlyConfigured('MySQL connector not installed!') 27 | return mysql_connector.connect(db=self.database, autocommit=True, 28 | **self.connect_params) 29 | 30 | def cursor(self, commit=None, named_cursor=None): 31 | if commit is not None: 32 | __deprecated__('"commit" has been deprecated and is a no-op.') 33 | if self.is_closed(): 34 | if self.autoconnect: 35 | self.connect() 36 | else: 37 | raise InterfaceError('Error, database connection not opened.') 38 | return self._state.conn.cursor(buffered=True) 39 | 40 | def get_binary_type(self): 41 | return mysql_connector.Binary 42 | 43 | 44 | class MariaDBConnectorDatabase(MySQLDatabase): 45 | def _connect(self): 46 | if mariadb is None: 47 | raise ImproperlyConfigured('mariadb connector not installed!') 48 | self.connect_params.pop('charset', None) 49 | self.connect_params.pop('sql_mode', None) 50 | self.connect_params.pop('use_unicode', None) 51 | return mariadb.connect(db=self.database, autocommit=True, 52 | **self.connect_params) 53 | 54 | def cursor(self, commit=None, named_cursor=None): 55 | if commit is not None: 56 | __deprecated__('"commit" has been deprecated and is a no-op.') 57 | if self.is_closed(): 58 | if self.autoconnect: 59 | self.connect() 60 | else: 61 | raise InterfaceError('Error, database connection not opened.') 62 | return self._state.conn.cursor(buffered=True) 63 | 64 | def _set_server_version(self, conn): 65 | version = conn.server_version 66 | version, point = divmod(version, 100) 67 | version, minor = divmod(version, 100) 68 | self.server_version = (version, minor, point) 69 | if self.server_version >= (10, 5, 0): 70 | self.returning_clause = True 71 | 72 | def last_insert_id(self, cursor, query_type=None): 73 | if not self.returning_clause: 74 | return cursor.lastrowid 75 | elif query_type == Insert.SIMPLE: 76 | try: 77 | return cursor[0][0] 78 | except (AttributeError, IndexError): 79 | return cursor.lastrowid 80 | return cursor 81 | 82 | def get_binary_type(self): 83 | return mariadb.Binary 84 | 85 | 86 | class JSONField(TextField): 87 | field_type = 'JSON' 88 | 89 | def __init__(self, json_dumps=None, json_loads=None, **kwargs): 90 | self._json_dumps = json_dumps or json.dumps 91 | self._json_loads = json_loads or json.loads 92 | super(JSONField, self).__init__(**kwargs) 93 | 94 | def python_value(self, value): 95 | if value is not None: 96 | try: 97 | return self._json_loads(value) 98 | except (TypeError, ValueError): 99 | return value 100 | 101 | def db_value(self, value): 102 | if value is not None: 103 | if not isinstance(value, Node): 104 | value = self._json_dumps(value) 105 | return value 106 | 107 | def extract(self, path): 108 | return fn.json_extract(self, path) 109 | 110 | 111 | def Match(columns, expr, modifier=None): 112 | if isinstance(columns, (list, tuple)): 113 | match = fn.MATCH(*columns) # Tuple of one or more columns / fields. 114 | else: 115 | match = fn.MATCH(columns) # Single column / field. 116 | args = expr if modifier is None else NodeList((expr, SQL(modifier))) 117 | return NodeList((match, fn.AGAINST(args))) 118 | -------------------------------------------------------------------------------- /playhouse/psycopg3_ext.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from peewee import * 4 | from peewee import Expression 5 | from peewee import Node 6 | from peewee import NodeList 7 | from playhouse.postgres_ext import ArrayField 8 | from playhouse.postgres_ext import DateTimeTZField 9 | from playhouse.postgres_ext import IndexedFieldMixin 10 | from playhouse.postgres_ext import IntervalField 11 | from playhouse.postgres_ext import Match 12 | from playhouse.postgres_ext import TSVectorField 13 | # Helpers needed for psycopg3-specific overrides. 14 | from playhouse.postgres_ext import _JsonLookupBase 15 | 16 | try: 17 | import psycopg 18 | from psycopg.types.json import Jsonb 19 | from psycopg.pq import TransactionStatus 20 | except ImportError: 21 | psycopg = Jsonb = None 22 | 23 | 24 | JSONB_CONTAINS = '@>' 25 | JSONB_CONTAINED_BY = '<@' 26 | JSONB_CONTAINS_KEY = '?' 27 | JSONB_CONTAINS_ANY_KEY = '?|' 28 | JSONB_CONTAINS_ALL_KEYS = '?&' 29 | JSONB_EXISTS = '?' 30 | JSONB_REMOVE = '-' 31 | 32 | 33 | class _Psycopg3JsonLookupBase(_JsonLookupBase): 34 | def concat(self, rhs): 35 | if not isinstance(rhs, Node): 36 | rhs = Jsonb(rhs) # Note: uses psycopg3's Jsonb. 37 | return Expression(self.as_json(True), OP.CONCAT, rhs) 38 | 39 | def contains(self, other): 40 | return Expression(self.as_json(True), JSONB_CONTAINS, Jsonb(other)) 41 | 42 | 43 | class JsonLookup(_Psycopg3JsonLookupBase): 44 | def __getitem__(self, value): 45 | return JsonLookup(self.node, self.parts + [value], self._as_json) 46 | 47 | def __sql__(self, ctx): 48 | ctx.sql(self.node) 49 | for part in self.parts[:-1]: 50 | ctx.literal('->').sql(part) 51 | if self.parts: 52 | (ctx 53 | .literal('->' if self._as_json else '->>') 54 | .sql(self.parts[-1])) 55 | 56 | return ctx 57 | 58 | 59 | class JsonPath(_Psycopg3JsonLookupBase): 60 | def __sql__(self, ctx): 61 | return (ctx 62 | .sql(self.node) 63 | .literal('#>' if self._as_json else '#>>') 64 | .sql(Value('{%s}' % ','.join(map(str, self.parts))))) 65 | 66 | 67 | def cast_jsonb(node): 68 | return NodeList((node, SQL('::jsonb')), glue='') 69 | 70 | 71 | class BinaryJSONField(IndexedFieldMixin, Field): 72 | field_type = 'JSONB' 73 | _json_datatype = 'jsonb' 74 | __hash__ = Field.__hash__ 75 | 76 | def __init__(self, dumps=None, *args, **kwargs): 77 | self.dumps = dumps or json.dumps 78 | super(BinaryJSONField, self).__init__(*args, **kwargs) 79 | 80 | def db_value(self, value): 81 | if value is None: 82 | return value 83 | if not isinstance(value, Jsonb): 84 | return Cast(self.dumps(value), self._json_datatype) 85 | return value 86 | 87 | def __getitem__(self, value): 88 | return JsonLookup(self, [value]) 89 | 90 | def path(self, *keys): 91 | return JsonPath(self, keys) 92 | 93 | def concat(self, value): 94 | if not isinstance(value, Node): 95 | value = Jsonb(value) 96 | return super(BinaryJSONField, self).concat(value) 97 | 98 | def contains(self, other): 99 | if isinstance(other, BinaryJSONField): 100 | return Expression(self, JSONB_CONTAINS, other) 101 | return Expression(cast_jsonb(self), JSONB_CONTAINS, Jsonb(other)) 102 | 103 | def contained_by(self, other): 104 | return Expression(cast_jsonb(self), JSONB_CONTAINED_BY, Jsonb(other)) 105 | 106 | def contains_any(self, *items): 107 | return Expression( 108 | cast_jsonb(self), 109 | JSONB_CONTAINS_ANY_KEY, 110 | Value(list(items), unpack=False)) 111 | 112 | def contains_all(self, *items): 113 | return Expression( 114 | cast_jsonb(self), 115 | JSONB_CONTAINS_ALL_KEYS, 116 | Value(list(items), unpack=False)) 117 | 118 | def has_key(self, key): 119 | return Expression(cast_jsonb(self), JSONB_CONTAINS_KEY, key) 120 | 121 | def remove(self, *items): 122 | return Expression( 123 | cast_jsonb(self), 124 | JSONB_REMOVE, 125 | # Hack: psycopg3 parameterizes this as an array, e.g. '{k1,k2}', 126 | # but that doesn't seem to be working, so we explicitly cast. 127 | # Perhaps postgres is interpreting it as a string. Using the more 128 | # explicit ARRAY['k1','k2'] also works just fine -- but we'll make 129 | # the cast explicit to get it working. 130 | Cast(Value(list(items), unpack=False), 'text[]')) 131 | 132 | 133 | class Psycopg3Database(PostgresqlDatabase): 134 | def _connect(self): 135 | if psycopg is None: 136 | raise ImproperlyConfigured('psycopg3 is not installed!') 137 | if self.database.startswith('postgresql://'): 138 | conn = psycopg.connect(self.database, **self.connect_params) 139 | else: 140 | conn = psycopg.connect(dbname=self.database, **self.connect_params) 141 | if self._isolation_level is not None: 142 | conn.isolation_level = self._isolation_level 143 | conn.autocommit = True 144 | return conn 145 | 146 | def get_binary_type(self): 147 | return psycopg.Binary 148 | 149 | def _set_server_version(self, conn): 150 | self.server_version = conn.pgconn.server_version 151 | if self.server_version >= 90600: 152 | self.safe_create_index = True 153 | 154 | def is_connection_usable(self): 155 | if self._state.closed: 156 | return False 157 | 158 | # Returns True if we are idle, running a command, or in an active 159 | # connection. If the connection is in an error state or the connection 160 | # is otherwise unusable, return False. 161 | conn = self._state.conn 162 | return conn.pgconn.transaction_status < TransactionStatus.INERROR 163 | 164 | def extract_date(self, date_part, date_field): 165 | return fn.EXTRACT(NodeList((SQL(date_part), SQL('FROM'), date_field))) 166 | -------------------------------------------------------------------------------- /playhouse/signals.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provide django-style hooks for model events. 3 | """ 4 | from peewee import Model as _Model 5 | 6 | 7 | class Signal(object): 8 | def __init__(self): 9 | self._flush() 10 | 11 | def _flush(self): 12 | self._receivers = set() 13 | self._receiver_list = [] 14 | 15 | def connect(self, receiver, name=None, sender=None): 16 | name = name or receiver.__name__ 17 | key = (name, sender) 18 | if key not in self._receivers: 19 | self._receivers.add(key) 20 | self._receiver_list.append((name, receiver, sender)) 21 | else: 22 | raise ValueError('receiver named %s (for sender=%s) already ' 23 | 'connected' % (name, sender or 'any')) 24 | 25 | def disconnect(self, receiver=None, name=None, sender=None): 26 | if receiver: 27 | name = name or receiver.__name__ 28 | if not name: 29 | raise ValueError('a receiver or a name must be provided') 30 | 31 | key = (name, sender) 32 | if key not in self._receivers: 33 | raise ValueError('receiver named %s for sender=%s not found.' % 34 | (name, sender or 'any')) 35 | 36 | self._receivers.remove(key) 37 | self._receiver_list = [(n, r, s) for n, r, s in self._receiver_list 38 | if (n, s) != key] 39 | 40 | def __call__(self, name=None, sender=None): 41 | def decorator(fn): 42 | self.connect(fn, name, sender) 43 | return fn 44 | return decorator 45 | 46 | def send(self, instance, *args, **kwargs): 47 | sender = type(instance) 48 | responses = [] 49 | for n, r, s in self._receiver_list: 50 | if s is None or isinstance(instance, s): 51 | responses.append((r, r(sender, instance, *args, **kwargs))) 52 | return responses 53 | 54 | 55 | pre_save = Signal() 56 | post_save = Signal() 57 | pre_delete = Signal() 58 | post_delete = Signal() 59 | pre_init = Signal() 60 | 61 | 62 | class Model(_Model): 63 | def __init__(self, *args, **kwargs): 64 | super(Model, self).__init__(*args, **kwargs) 65 | pre_init.send(self) 66 | 67 | def save(self, *args, **kwargs): 68 | pk_value = self._pk if self._meta.primary_key else True 69 | created = kwargs.get('force_insert', False) or not bool(pk_value) 70 | pre_save.send(self, created=created) 71 | ret = super(Model, self).save(*args, **kwargs) 72 | post_save.send(self, created=created) 73 | return ret 74 | 75 | def delete_instance(self, *args, **kwargs): 76 | pre_delete.send(self) 77 | ret = super(Model, self).delete_instance(*args, **kwargs) 78 | post_delete.send(self) 79 | return ret 80 | -------------------------------------------------------------------------------- /playhouse/sqlcipher_ext.py: -------------------------------------------------------------------------------- 1 | """ 2 | Peewee integration with pysqlcipher. 3 | 4 | Project page: https://github.com/leapcode/pysqlcipher/ 5 | 6 | **WARNING!!! EXPERIMENTAL!!!** 7 | 8 | * Although this extention's code is short, it has not been properly 9 | peer-reviewed yet and may have introduced vulnerabilities. 10 | 11 | Also note that this code relies on pysqlcipher and sqlcipher, and 12 | the code there might have vulnerabilities as well, but since these 13 | are widely used crypto modules, we can expect "short zero days" there. 14 | 15 | Example usage: 16 | 17 | from peewee.playground.ciphersql_ext import SqlCipherDatabase 18 | db = SqlCipherDatabase('/path/to/my.db', passphrase="don'tuseme4real") 19 | 20 | * `passphrase`: should be "long enough". 21 | Note that *length beats vocabulary* (much exponential), and even 22 | a lowercase-only passphrase like easytorememberyethardforotherstoguess 23 | packs more noise than 8 random printable characters and *can* be memorized. 24 | 25 | When opening an existing database, passphrase should be the one used when the 26 | database was created. If the passphrase is incorrect, an exception will only be 27 | raised **when you access the database**. 28 | 29 | If you need to ask for an interactive passphrase, here's example code you can 30 | put after the `db = ...` line: 31 | 32 | try: # Just access the database so that it checks the encryption. 33 | db.get_tables() 34 | # We're looking for a DatabaseError with a specific error message. 35 | except peewee.DatabaseError as e: 36 | # Check whether the message *means* "passphrase is wrong" 37 | if e.args[0] == 'file is encrypted or is not a database': 38 | raise Exception('Developer should Prompt user for passphrase ' 39 | 'again.') 40 | else: 41 | # A different DatabaseError. Raise it. 42 | raise e 43 | 44 | See a more elaborate example with this code at 45 | https://gist.github.com/thedod/11048875 46 | """ 47 | import datetime 48 | import decimal 49 | import sys 50 | 51 | from peewee import * 52 | from playhouse.sqlite_ext import SqliteExtDatabase 53 | if sys.version_info[0] != 3: 54 | from pysqlcipher import dbapi2 as sqlcipher 55 | else: 56 | try: 57 | from sqlcipher3 import dbapi2 as sqlcipher 58 | except ImportError: 59 | from pysqlcipher3 import dbapi2 as sqlcipher 60 | 61 | sqlcipher.register_adapter(decimal.Decimal, str) 62 | sqlcipher.register_adapter(datetime.date, str) 63 | sqlcipher.register_adapter(datetime.time, str) 64 | __sqlcipher_version__ = sqlcipher.sqlite_version_info 65 | 66 | 67 | class _SqlCipherDatabase(object): 68 | server_version = __sqlcipher_version__ 69 | 70 | def _connect(self): 71 | params = dict(self.connect_params) 72 | passphrase = params.pop('passphrase', '').replace("'", "''") 73 | 74 | conn = sqlcipher.connect(self.database, isolation_level=None, **params) 75 | try: 76 | if passphrase: 77 | conn.execute("PRAGMA key='%s'" % passphrase) 78 | self._add_conn_hooks(conn) 79 | except: 80 | conn.close() 81 | raise 82 | return conn 83 | 84 | def set_passphrase(self, passphrase): 85 | if not self.is_closed(): 86 | raise ImproperlyConfigured('Cannot set passphrase when database ' 87 | 'is open. To change passphrase of an ' 88 | 'open database use the rekey() method.') 89 | 90 | self.connect_params['passphrase'] = passphrase 91 | 92 | def rekey(self, passphrase): 93 | if self.is_closed(): 94 | self.connect() 95 | 96 | self.execute_sql("PRAGMA rekey='%s'" % passphrase.replace("'", "''")) 97 | self.connect_params['passphrase'] = passphrase 98 | return True 99 | 100 | 101 | class SqlCipherDatabase(_SqlCipherDatabase, SqliteDatabase): 102 | pass 103 | 104 | 105 | class SqlCipherExtDatabase(_SqlCipherDatabase, SqliteExtDatabase): 106 | pass 107 | -------------------------------------------------------------------------------- /playhouse/sqlite_changelog.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | from playhouse.sqlite_ext import JSONField 3 | 4 | 5 | class BaseChangeLog(Model): 6 | timestamp = DateTimeField(constraints=[SQL('DEFAULT CURRENT_TIMESTAMP')]) 7 | action = TextField() 8 | table = TextField() 9 | primary_key = IntegerField() 10 | changes = JSONField() 11 | 12 | 13 | class ChangeLog(object): 14 | # Model class that will serve as the base for the changelog. This model 15 | # will be subclassed and mapped to your application database. 16 | base_model = BaseChangeLog 17 | 18 | # Template for the triggers that handle updating the changelog table. 19 | # table: table name 20 | # action: insert / update / delete 21 | # new_old: NEW or OLD (OLD is for DELETE) 22 | # primary_key: table primary key column name 23 | # column_array: output of build_column_array() 24 | # change_table: changelog table name 25 | template = """CREATE TRIGGER IF NOT EXISTS %(table)s_changes_%(action)s 26 | AFTER %(action)s ON %(table)s 27 | BEGIN 28 | INSERT INTO %(change_table)s 29 | ("action", "table", "primary_key", "changes") 30 | SELECT 31 | '%(action)s', '%(table)s', %(new_old)s."%(primary_key)s", "changes" 32 | FROM ( 33 | SELECT json_group_object( 34 | col, 35 | json_array( 36 | case when json_valid("oldval") then json("oldval") 37 | else "oldval" end, 38 | case when json_valid("newval") then json("newval") 39 | else "newval" end) 40 | ) AS "changes" 41 | FROM ( 42 | SELECT json_extract(value, '$[0]') as "col", 43 | json_extract(value, '$[1]') as "oldval", 44 | json_extract(value, '$[2]') as "newval" 45 | FROM json_each(json_array(%(column_array)s)) 46 | WHERE "oldval" IS NOT "newval" 47 | ) 48 | ); 49 | END;""" 50 | 51 | drop_template = 'DROP TRIGGER IF EXISTS %(table)s_changes_%(action)s' 52 | 53 | _actions = ('INSERT', 'UPDATE', 'DELETE') 54 | 55 | def __init__(self, db, table_name='changelog'): 56 | self.db = db 57 | self.table_name = table_name 58 | 59 | def _build_column_array(self, model, use_old, use_new, skip_fields=None): 60 | # Builds a list of SQL expressions for each field we are tracking. This 61 | # is used as the data source for change tracking in our trigger. 62 | col_array = [] 63 | for field in model._meta.sorted_fields: 64 | if field.primary_key: 65 | continue 66 | 67 | if skip_fields is not None and field.name in skip_fields: 68 | continue 69 | 70 | column = field.column_name 71 | new = 'NULL' if not use_new else 'NEW."%s"' % column 72 | old = 'NULL' if not use_old else 'OLD."%s"' % column 73 | 74 | if isinstance(field, JSONField): 75 | # Ensure that values are cast to JSON so that the serialization 76 | # is preserved when calculating the old / new. 77 | if use_old: old = 'json(%s)' % old 78 | if use_new: new = 'json(%s)' % new 79 | 80 | col_array.append("json_array('%s', %s, %s)" % (column, old, new)) 81 | 82 | return ', '.join(col_array) 83 | 84 | def trigger_sql(self, model, action, skip_fields=None): 85 | assert action in self._actions 86 | use_old = action != 'INSERT' 87 | use_new = action != 'DELETE' 88 | cols = self._build_column_array(model, use_old, use_new, skip_fields) 89 | return self.template % { 90 | 'table': model._meta.table_name, 91 | 'action': action, 92 | 'new_old': 'NEW' if action != 'DELETE' else 'OLD', 93 | 'primary_key': model._meta.primary_key.column_name, 94 | 'column_array': cols, 95 | 'change_table': self.table_name} 96 | 97 | def drop_trigger_sql(self, model, action): 98 | assert action in self._actions 99 | return self.drop_template % { 100 | 'table': model._meta.table_name, 101 | 'action': action} 102 | 103 | @property 104 | def model(self): 105 | if not hasattr(self, '_changelog_model'): 106 | class ChangeLog(self.base_model): 107 | class Meta: 108 | database = self.db 109 | table_name = self.table_name 110 | self._changelog_model = ChangeLog 111 | 112 | return self._changelog_model 113 | 114 | def install(self, model, skip_fields=None, drop=True, insert=True, 115 | update=True, delete=True, create_table=True): 116 | ChangeLog = self.model 117 | if create_table: 118 | ChangeLog.create_table() 119 | 120 | actions = list(zip((insert, update, delete), self._actions)) 121 | if drop: 122 | for _, action in actions: 123 | self.db.execute_sql(self.drop_trigger_sql(model, action)) 124 | 125 | for enabled, action in actions: 126 | if enabled: 127 | sql = self.trigger_sql(model, action, skip_fields) 128 | self.db.execute_sql(sql) 129 | -------------------------------------------------------------------------------- /playhouse/test_utils.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | import logging 3 | 4 | 5 | logger = logging.getLogger('peewee') 6 | 7 | 8 | class _QueryLogHandler(logging.Handler): 9 | def __init__(self, *args, **kwargs): 10 | self.queries = [] 11 | logging.Handler.__init__(self, *args, **kwargs) 12 | 13 | def emit(self, record): 14 | # Counts all entries logged to the "peewee" logger by execute_sql(). 15 | if record.name == 'peewee': 16 | self.queries.append(record) 17 | 18 | 19 | class count_queries(object): 20 | def __init__(self, only_select=False): 21 | self.only_select = only_select 22 | self.count = 0 23 | 24 | def get_queries(self): 25 | return self._handler.queries 26 | 27 | def __enter__(self): 28 | self._handler = _QueryLogHandler() 29 | logger.setLevel(logging.DEBUG) 30 | logger.addHandler(self._handler) 31 | return self 32 | 33 | def __exit__(self, exc_type, exc_val, exc_tb): 34 | logger.removeHandler(self._handler) 35 | if self.only_select: 36 | self.count = len([q for q in self._handler.queries 37 | if q.msg[0].startswith('SELECT ')]) 38 | else: 39 | self.count = len(self._handler.queries) 40 | 41 | 42 | class assert_query_count(count_queries): 43 | def __init__(self, expected, only_select=False): 44 | super(assert_query_count, self).__init__(only_select=only_select) 45 | self.expected = expected 46 | 47 | def __call__(self, f): 48 | @wraps(f) 49 | def decorated(*args, **kwds): 50 | with self: 51 | ret = f(*args, **kwds) 52 | 53 | self._assert_count() 54 | return ret 55 | 56 | return decorated 57 | 58 | def _assert_count(self): 59 | error_msg = '%s != %s' % (self.count, self.expected) 60 | assert self.count == self.expected, error_msg 61 | 62 | def __exit__(self, exc_type, exc_val, exc_tb): 63 | super(assert_query_count, self).__exit__(exc_type, exc_val, exc_tb) 64 | self._assert_count() 65 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend="setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import optparse 3 | import os 4 | import shutil 5 | import sys 6 | import unittest 7 | 8 | 9 | USER = os.environ.get('USER') or 'root' 10 | 11 | 12 | def runtests(suite, verbosity=1, failfast=False): 13 | runner = unittest.TextTestRunner(verbosity=verbosity, failfast=failfast) 14 | results = runner.run(suite) 15 | return results.failures, results.errors 16 | 17 | def get_option_parser(): 18 | usage = 'usage: %prog [-e engine_name, other options] module1, module2 ...' 19 | parser = optparse.OptionParser(usage=usage) 20 | basic = optparse.OptionGroup(parser, 'Basic test options') 21 | basic.add_option( 22 | '-e', 23 | '--engine', 24 | dest='engine', 25 | help=('Database engine to test, one of ' 26 | '[sqlite, postgres, mysql, mysqlconnector, apsw, sqlcipher,' 27 | ' cockroachdb, psycopg3]')) 28 | basic.add_option('-v', '--verbosity', dest='verbosity', default=1, 29 | type='int', help='Verbosity of output') 30 | basic.add_option('-f', '--failfast', action='store_true', default=False, 31 | dest='failfast', help='Exit on first failure/error.') 32 | basic.add_option('-s', '--slow-tests', action='store_true', default=False, 33 | dest='slow_tests', help='Run tests that may be slow.') 34 | parser.add_option_group(basic) 35 | 36 | db_param_map = ( 37 | ('MySQL', 'MYSQL', ( 38 | # param default disp default val 39 | ('host', 'localhost', 'localhost'), 40 | ('port', '3306', ''), 41 | ('user', USER, USER), 42 | ('password', 'blank', ''))), 43 | ('Postgresql', 'PSQL', ( 44 | ('host', 'localhost', os.environ.get('PGHOST', '')), 45 | ('port', '5432', ''), 46 | ('user', 'postgres', os.environ.get('PGUSER', '')), 47 | ('password', 'blank', os.environ.get('PGPASSWORD', '')))), 48 | ('CockroachDB', 'CRDB', ( 49 | # param default disp default val 50 | ('host', 'localhost', 'localhost'), 51 | ('port', '26257', ''), 52 | ('user', 'root', 'root'), 53 | ('password', 'blank', '')))) 54 | for name, prefix, param_list in db_param_map: 55 | group = optparse.OptionGroup(parser, '%s connection options' % name) 56 | for param, default_disp, default_val in param_list: 57 | dest = '%s_%s' % (prefix.lower(), param) 58 | opt = '--%s-%s' % (prefix.lower(), param) 59 | group.add_option(opt, default=default_val, dest=dest, help=( 60 | '%s database %s. Default %s.' % (name, param, default_disp))) 61 | 62 | parser.add_option_group(group) 63 | return parser 64 | 65 | def collect_tests(args): 66 | suite = unittest.TestSuite() 67 | 68 | if not args: 69 | import tests 70 | module_suite = unittest.TestLoader().loadTestsFromModule(tests) 71 | suite.addTest(module_suite) 72 | else: 73 | cleaned = ['tests.%s' % arg if not arg.startswith('tests.') else arg 74 | for arg in args] 75 | user_suite = unittest.TestLoader().loadTestsFromNames(cleaned) 76 | suite.addTest(user_suite) 77 | 78 | return suite 79 | 80 | if __name__ == '__main__': 81 | parser = get_option_parser() 82 | options, args = parser.parse_args() 83 | 84 | if options.engine: 85 | os.environ['PEEWEE_TEST_BACKEND'] = options.engine 86 | 87 | for db in ('mysql', 'psql', 'crdb'): 88 | for key in ('host', 'port', 'user', 'password'): 89 | att_name = '_'.join((db, key)) 90 | value = getattr(options, att_name, None) 91 | if value: 92 | os.environ['PEEWEE_%s' % att_name.upper()] = value 93 | 94 | os.environ['PEEWEE_TEST_VERBOSITY'] = str(options.verbosity) 95 | if options.slow_tests: 96 | os.environ['PEEWEE_SLOW_TESTS'] = '1' 97 | 98 | suite = collect_tests(args) 99 | failures, errors = runtests(suite, options.verbosity, options.failfast) 100 | 101 | files_to_delete = [ 102 | 'peewee_test.db', 103 | 'peewee_test', 104 | 'tmp.db', 105 | 'peewee_test.bdb.db', 106 | 'peewee_test.cipher.db'] 107 | paths_to_delete = ['peewee_test.bdb.db-journal'] 108 | for filename in files_to_delete: 109 | if os.path.exists(filename): 110 | os.unlink(filename) 111 | for path in paths_to_delete: 112 | if os.path.exists(path): 113 | shutil.rmtree(path) 114 | 115 | if errors: 116 | sys.exit(2) 117 | elif failures: 118 | sys.exit(1) 119 | 120 | sys.exit(0) 121 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | from peewee import OperationalError 5 | 6 | # Core modules. 7 | from .db_tests import * 8 | from .expressions import * 9 | from .fields import * 10 | from .keys import * 11 | from .manytomany import * 12 | from .models import * 13 | from .model_save import * 14 | from .model_sql import * 15 | from .prefetch_tests import * 16 | from .queries import * 17 | from .regressions import * 18 | from .results import * 19 | from .schema import * 20 | from .sql import * 21 | from .transactions import * 22 | 23 | # Extensions. 24 | try: 25 | from .apsw_ext import * 26 | except ImportError: 27 | print('Unable to import APSW extension tests, skipping.') 28 | try: 29 | from .cockroachdb import * 30 | except: 31 | print('Unable to import CockroachDB tests, skipping.') 32 | try: 33 | from .cysqlite import * 34 | except ImportError: 35 | print('Unable to import sqlite C extension tests, skipping.') 36 | from .dataset import * 37 | from .db_url import * 38 | from .extra_fields import * 39 | from .hybrid import * 40 | from .kv import * 41 | from .migrations import * 42 | try: 43 | import mysql.connector 44 | from .mysql_ext import * 45 | except ImportError: 46 | print('Unable to import mysql-connector, skipping mysql_ext tests.') 47 | from .pool import * 48 | try: 49 | from .postgres import * 50 | except (ImportError, ImproperlyConfigured): 51 | print('Unable to import postgres extension tests, skipping.') 52 | except OperationalError: 53 | print('Postgresql test database "peewee_test" not found, skipping ' 54 | 'the postgres_ext tests.') 55 | try: 56 | from .psycopg3_ext import * 57 | except (ImportError, ImproperlyConfigured): 58 | print('Unable to import psycopg3 extension tests, skipping.') 59 | from .pwiz_integration import * 60 | from .reflection import * 61 | from .returning import * 62 | from .shortcuts import * 63 | from .signals import * 64 | try: 65 | from .sqlcipher_ext import * 66 | except ImportError: 67 | print('Unable to import SQLCipher extension tests, skipping.') 68 | try: 69 | from .sqlite import * 70 | except ImportError: 71 | print('Unable to import sqlite extension tests, skipping.') 72 | try: 73 | from .sqlite_changelog import * 74 | except ImportError: 75 | print('Unable to import sqlite changelog tests, skipping.') 76 | from .sqliteq import * 77 | from .sqlite_udf import * 78 | from .test_utils import * 79 | 80 | 81 | if __name__ == '__main__': 82 | from peewee import print_ 83 | print_(r"""\x1b[1;31m 84 | ______ ______ ______ __ __ ______ ______ 85 | /\ == \ /\ ___\ /\ ___\ /\ \ _ \ \ /\ ___\ /\ ___\\ 86 | \ \ _-/ \ \ __\ \ \ __\ \ \ \/ ".\ \ \ \ __\ \ \ __\\ 87 | \ \_\ \ \_____\ \ \_____\ \ \__/".~\_\ \ \_____\ \ \_____\\ 88 | \/_/ \/_____/ \/_____/ \/_/ \/_/ \/_____/ \/_____/ 89 | \x1b[0m""") 90 | unittest.main(argv=sys.argv) 91 | -------------------------------------------------------------------------------- /tests/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | from tests import * 5 | 6 | 7 | if __name__ == '__main__': 8 | unittest.main(argv=sys.argv) 9 | -------------------------------------------------------------------------------- /tests/apsw_ext.py: -------------------------------------------------------------------------------- 1 | import apsw 2 | import datetime 3 | 4 | from playhouse.apsw_ext import * 5 | from .base import ModelTestCase 6 | from .base import TestModel 7 | 8 | 9 | database = APSWDatabase(':memory:') 10 | 11 | 12 | class User(TestModel): 13 | username = TextField() 14 | 15 | 16 | class Message(TestModel): 17 | user = ForeignKeyField(User) 18 | message = TextField() 19 | pub_date = DateTimeField() 20 | published = BooleanField() 21 | 22 | 23 | class VTSource(object): 24 | def Create(self, db, modulename, dbname, tablename, *args): 25 | schema = 'CREATE TABLE x(value)' 26 | return schema, VTable() 27 | Connect = Create 28 | class VTable(object): 29 | def BestIndex(self, *args): 30 | return 31 | def Open(self): 32 | return VTCursor() 33 | def Disconnect(self): pass 34 | Destroy = Disconnect 35 | class VTCursor(object): 36 | def Filter(self, *a): 37 | self.val = 0 38 | def Eof(self): return False 39 | def Rowid(self): 40 | return self.val 41 | def Column(self, col): 42 | return self.val 43 | def Next(self): 44 | self.val += 1 45 | def Close(self): pass 46 | 47 | 48 | class TestAPSWExtension(ModelTestCase): 49 | database = database 50 | requires = [User, Message] 51 | 52 | def test_db_register_module(self): 53 | database.register_module('series', VTSource()) 54 | database.execute_sql('create virtual table foo using series()') 55 | curs = database.execute_sql('select * from foo limit 5;') 56 | self.assertEqual([v for v, in curs], [0, 1, 2, 3, 4]) 57 | database.unregister_module('series') 58 | 59 | def test_db_register_function(self): 60 | @database.func() 61 | def title(s): 62 | return s.title() 63 | 64 | curs = self.database.execute_sql('SELECT title(?)', ('heLLo',)) 65 | self.assertEqual(curs.fetchone()[0], 'Hello') 66 | 67 | def test_db_register_aggregate(self): 68 | @database.aggregate() 69 | class First(object): 70 | def __init__(self): 71 | self._value = None 72 | 73 | def step(self, value): 74 | if self._value is None: 75 | self._value = value 76 | 77 | def finalize(self): 78 | return self._value 79 | 80 | with database.atomic(): 81 | for i in range(10): 82 | User.create(username='u%s' % i) 83 | 84 | query = User.select(fn.First(User.username)).order_by(User.username) 85 | self.assertEqual(query.scalar(), 'u0') 86 | 87 | def test_db_register_collation(self): 88 | @database.collation() 89 | def reverse(lhs, rhs): 90 | lhs, rhs = lhs.lower(), rhs.lower() 91 | if lhs < rhs: 92 | return 1 93 | return -1 if rhs > lhs else 0 94 | 95 | with database.atomic(): 96 | for i in range(3): 97 | User.create(username='u%s' % i) 98 | 99 | query = (User 100 | .select(User.username) 101 | .order_by(User.username.collate('reverse'))) 102 | self.assertEqual([u.username for u in query], ['u2', 'u1', 'u0']) 103 | 104 | def test_db_pragmas(self): 105 | test_db = APSWDatabase(':memory:', pragmas=( 106 | ('cache_size', '1337'), 107 | )) 108 | test_db.connect() 109 | 110 | cs = test_db.execute_sql('PRAGMA cache_size;').fetchone()[0] 111 | self.assertEqual(cs, 1337) 112 | 113 | def test_select_insert(self): 114 | for user in ('u1', 'u2', 'u3'): 115 | User.create(username=user) 116 | 117 | self.assertEqual([x.username for x in User.select()], ['u1', 'u2', 'u3']) 118 | 119 | dt = datetime.datetime(2012, 1, 1, 11, 11, 11) 120 | Message.create(user=User.get(User.username == 'u1'), message='herps', pub_date=dt, published=True) 121 | Message.create(user=User.get(User.username == 'u2'), message='derps', pub_date=dt, published=False) 122 | 123 | m1 = Message.get(Message.message == 'herps') 124 | self.assertEqual(m1.user.username, 'u1') 125 | self.assertEqual(m1.pub_date, dt) 126 | self.assertEqual(m1.published, True) 127 | 128 | m2 = Message.get(Message.message == 'derps') 129 | self.assertEqual(m2.user.username, 'u2') 130 | self.assertEqual(m2.pub_date, dt) 131 | self.assertEqual(m2.published, False) 132 | 133 | def test_update_delete(self): 134 | u1 = User.create(username='u1') 135 | u2 = User.create(username='u2') 136 | 137 | u1.username = 'u1-modified' 138 | u1.save() 139 | 140 | self.assertEqual(User.select().count(), 2) 141 | self.assertEqual(User.get(User.username == 'u1-modified').id, u1.id) 142 | 143 | u1.delete_instance() 144 | self.assertEqual(User.select().count(), 1) 145 | 146 | def test_transaction_handling(self): 147 | dt = datetime.datetime(2012, 1, 1, 11, 11, 11) 148 | 149 | def do_ctx_mgr_error(): 150 | with self.database.transaction(): 151 | User.create(username='u1') 152 | raise ValueError 153 | 154 | self.assertRaises(ValueError, do_ctx_mgr_error) 155 | self.assertEqual(User.select().count(), 0) 156 | 157 | def do_ctx_mgr_success(): 158 | with self.database.transaction(): 159 | u = User.create(username='test') 160 | Message.create(message='testing', user=u, pub_date=dt, published=1) 161 | 162 | do_ctx_mgr_success() 163 | self.assertEqual(User.select().count(), 1) 164 | self.assertEqual(Message.select().count(), 1) 165 | 166 | def create_error(): 167 | with self.database.atomic(): 168 | u = User.create(username='test') 169 | Message.create(message='testing', user=u, pub_date=dt, 170 | published=1) 171 | raise ValueError 172 | 173 | self.assertRaises(ValueError, create_error) 174 | self.assertEqual(User.select().count(), 1) 175 | 176 | def create_success(): 177 | with self.database.atomic(): 178 | u = User.create(username='test') 179 | Message.create(message='testing', user=u, pub_date=dt, 180 | published=1) 181 | 182 | create_success() 183 | self.assertEqual(User.select().count(), 2) 184 | self.assertEqual(Message.select().count(), 2) 185 | 186 | def test_exists_regression(self): 187 | User.create(username='u1') 188 | self.assertTrue(User.select().where(User.username == 'u1').exists()) 189 | self.assertFalse(User.select().where(User.username == 'ux').exists()) 190 | -------------------------------------------------------------------------------- /tests/base_models.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | 3 | from .base import TestModel 4 | 5 | 6 | class Person(TestModel): 7 | first = CharField() 8 | last = CharField() 9 | dob = DateField(index=True) 10 | 11 | class Meta: 12 | indexes = ( 13 | (('first', 'last'), True), 14 | ) 15 | 16 | 17 | class Note(TestModel): 18 | author = ForeignKeyField(Person) 19 | content = TextField() 20 | 21 | 22 | class Category(TestModel): 23 | parent = ForeignKeyField('self', backref='children', null=True) 24 | name = CharField(max_length=20, primary_key=True) 25 | 26 | 27 | class Relationship(TestModel): 28 | from_person = ForeignKeyField(Person, backref='relations') 29 | to_person = ForeignKeyField(Person, backref='related_to') 30 | 31 | 32 | class Register(TestModel): 33 | value = IntegerField() 34 | 35 | 36 | class User(TestModel): 37 | username = CharField() 38 | 39 | class Meta: 40 | table_name = 'users' 41 | 42 | 43 | class Account(TestModel): 44 | email = CharField() 45 | user = ForeignKeyField(User, backref='accounts', null=True) 46 | 47 | 48 | class Tweet(TestModel): 49 | user = ForeignKeyField(User, backref='tweets') 50 | content = TextField() 51 | timestamp = TimestampField() 52 | 53 | 54 | class Favorite(TestModel): 55 | user = ForeignKeyField(User, backref='favorites') 56 | tweet = ForeignKeyField(Tweet, backref='favorites') 57 | 58 | 59 | class Sample(TestModel): 60 | counter = IntegerField() 61 | value = FloatField(default=1.0) 62 | 63 | 64 | class SampleMeta(TestModel): 65 | sample = ForeignKeyField(Sample, backref='metadata') 66 | value = FloatField(default=0.0) 67 | 68 | 69 | class A(TestModel): 70 | a = TextField() 71 | class B(TestModel): 72 | a = ForeignKeyField(A, backref='bs') 73 | b = TextField() 74 | class C(TestModel): 75 | b = ForeignKeyField(B, backref='cs') 76 | c = TextField() 77 | 78 | 79 | class Emp(TestModel): 80 | first = CharField() 81 | last = CharField() 82 | empno = CharField(unique=True) 83 | 84 | class Meta: 85 | indexes = ( 86 | (('first', 'last'), True), 87 | ) 88 | 89 | 90 | class OCTest(TestModel): 91 | a = CharField(unique=True) 92 | b = IntegerField(default=0) 93 | c = IntegerField(default=0) 94 | 95 | 96 | class UKVP(TestModel): 97 | key = TextField() 98 | value = IntegerField() 99 | extra = IntegerField() 100 | 101 | class Meta: 102 | # Partial index, the WHERE clause must be reflected in the conflict 103 | # target. 104 | indexes = [ 105 | SQL('CREATE UNIQUE INDEX "ukvp_kve" ON "ukvp" ("key", "value") ' 106 | 'WHERE "extra" > 1')] 107 | 108 | 109 | class DfltM(TestModel): 110 | name = CharField() 111 | dflt1 = IntegerField(default=1) 112 | dflt2 = IntegerField(default=lambda: 2) 113 | dfltn = IntegerField(null=True) 114 | -------------------------------------------------------------------------------- /tests/db_url.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | from playhouse.db_url import connect 3 | from playhouse.db_url import parse 4 | 5 | from .base import BaseTestCase 6 | 7 | 8 | class TestDBUrl(BaseTestCase): 9 | def test_db_url_parse(self): 10 | cfg = parse('mysql://usr:pwd@hst:123/db') 11 | self.assertEqual(cfg['user'], 'usr') 12 | self.assertEqual(cfg['passwd'], 'pwd') 13 | self.assertEqual(cfg['host'], 'hst') 14 | self.assertEqual(cfg['database'], 'db') 15 | self.assertEqual(cfg['port'], 123) 16 | cfg = parse('postgresql://usr:pwd@hst/db') 17 | self.assertEqual(cfg['password'], 'pwd') 18 | cfg = parse('mysql+pool://usr:pwd@hst:123/db' 19 | '?max_connections=42&stale_timeout=8001.2&zai=&baz=3.4.5' 20 | '&boolz=false') 21 | self.assertEqual(cfg['user'], 'usr') 22 | self.assertEqual(cfg['password'], 'pwd') 23 | self.assertEqual(cfg['host'], 'hst') 24 | self.assertEqual(cfg['database'], 'db') 25 | self.assertEqual(cfg['port'], 123) 26 | self.assertEqual(cfg['max_connections'], 42) 27 | self.assertEqual(cfg['stale_timeout'], 8001.2) 28 | self.assertEqual(cfg['zai'], '') 29 | self.assertEqual(cfg['baz'], '3.4.5') 30 | self.assertEqual(cfg['boolz'], False) 31 | 32 | def test_db_url_no_unquoting(self): 33 | # By default, neither user nor password is not unescaped. 34 | cfg = parse('mysql://usr%40example.com:pwd%23@hst:123/db') 35 | self.assertEqual(cfg['user'], 'usr%40example.com') 36 | self.assertEqual(cfg['passwd'], 'pwd%23') 37 | self.assertEqual(cfg['host'], 'hst') 38 | self.assertEqual(cfg['database'], 'db') 39 | self.assertEqual(cfg['port'], 123) 40 | 41 | def test_db_url_quoted_password(self): 42 | cfg = parse('mysql://usr:pwd%23%20@hst:123/db', unquote_password=True) 43 | self.assertEqual(cfg['user'], 'usr') 44 | self.assertEqual(cfg['passwd'], 'pwd# ') 45 | self.assertEqual(cfg['host'], 'hst') 46 | self.assertEqual(cfg['database'], 'db') 47 | self.assertEqual(cfg['port'], 123) 48 | 49 | def test_db_url_quoted_user(self): 50 | cfg = parse('mysql://usr%40example.com:p%40sswd@hst:123/db', unquote_user=True) 51 | self.assertEqual(cfg['user'], 'usr@example.com') 52 | self.assertEqual(cfg['passwd'], 'p%40sswd') 53 | self.assertEqual(cfg['host'], 'hst') 54 | self.assertEqual(cfg['database'], 'db') 55 | self.assertEqual(cfg['port'], 123) 56 | 57 | def test_db_url(self): 58 | db = connect('sqlite:///:memory:') 59 | self.assertTrue(isinstance(db, SqliteDatabase)) 60 | self.assertEqual(db.database, ':memory:') 61 | 62 | db = connect('sqlite:///:memory:', pragmas=( 63 | ('journal_mode', 'MEMORY'),)) 64 | self.assertTrue(('journal_mode', 'MEMORY') in db._pragmas) 65 | 66 | #db = connect('sqliteext:///foo/bar.db') 67 | #self.assertTrue(isinstance(db, SqliteExtDatabase)) 68 | #self.assertEqual(db.database, 'foo/bar.db') 69 | 70 | db = connect('sqlite:////this/is/absolute.path') 71 | self.assertEqual(db.database, '/this/is/absolute.path') 72 | 73 | db = connect('sqlite://') 74 | self.assertTrue(isinstance(db, SqliteDatabase)) 75 | self.assertEqual(db.database, ':memory:') 76 | 77 | db = connect('sqlite:///test.db?p1=1?a&p2=22&p3=xyz') 78 | self.assertTrue(isinstance(db, SqliteDatabase)) 79 | self.assertEqual(db.database, 'test.db') 80 | self.assertEqual(db.connect_params, { 81 | 'p1': '1?a', 'p2': 22, 'p3': 'xyz'}) 82 | 83 | def test_bad_scheme(self): 84 | def _test_scheme(): 85 | connect('missing:///') 86 | 87 | self.assertRaises(RuntimeError, _test_scheme) 88 | -------------------------------------------------------------------------------- /tests/extra_fields.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | from playhouse.fields import CompressedField 3 | from playhouse.fields import PickleField 4 | 5 | from .base import db 6 | from .base import ModelTestCase 7 | from .base import TestModel 8 | 9 | 10 | class Comp(TestModel): 11 | key = TextField() 12 | data = CompressedField() 13 | 14 | 15 | class Pickled(TestModel): 16 | key = TextField() 17 | data = PickleField() 18 | 19 | 20 | class TestCompressedField(ModelTestCase): 21 | requires = [Comp] 22 | 23 | def test_compressed_field(self): 24 | a = b'a' * 1024 25 | b = b'b' * 1024 26 | Comp.create(data=a, key='a') 27 | Comp.create(data=b, key='b') 28 | 29 | a_db = Comp.get(Comp.key == 'a') 30 | self.assertEqual(a_db.data, a) 31 | 32 | b_db = Comp.get(Comp.key == 'b') 33 | self.assertEqual(b_db.data, b) 34 | 35 | # Get at the underlying data. 36 | CompTbl = Table('comp', ('id', 'data', 'key')).bind(self.database) 37 | obj = CompTbl.select().where(CompTbl.key == 'a').get() 38 | self.assertEqual(obj['key'], 'a') 39 | 40 | # Ensure that the data actually was compressed. 41 | self.assertTrue(len(obj['data']) < 1024) 42 | 43 | 44 | class TestPickleField(ModelTestCase): 45 | requires = [Pickled] 46 | 47 | def test_pickle_field(self): 48 | a = {'k1': 'v1', 'k2': [0, 1, 2], 'k3': None} 49 | b = 'just a string' 50 | Pickled.create(data=a, key='a') 51 | Pickled.create(data=b, key='b') 52 | 53 | a_db = Pickled.get(Pickled.key == 'a') 54 | self.assertEqual(a_db.data, a) 55 | 56 | b_db = Pickled.get(Pickled.key == 'b') 57 | self.assertEqual(b_db.data, b) 58 | -------------------------------------------------------------------------------- /tests/hybrid.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | from playhouse.hybrid import * 3 | 4 | from .base import ModelTestCase 5 | from .base import TestModel 6 | from .base import get_in_memory_db 7 | from .base import requires_models 8 | 9 | 10 | class Interval(TestModel): 11 | start = IntegerField() 12 | end = IntegerField() 13 | 14 | @hybrid_property 15 | def length(self): 16 | return self.end - self.start 17 | 18 | @hybrid_method 19 | def contains(self, point): 20 | return (self.start <= point) & (point < self.end) 21 | 22 | @hybrid_property 23 | def radius(self): 24 | return int(abs(self.length) / 2) 25 | 26 | @radius.expression 27 | def radius(cls): 28 | return fn.ABS(cls.length) / 2 29 | 30 | 31 | class Person(TestModel): 32 | first = TextField() 33 | last = TextField() 34 | 35 | @hybrid_property 36 | def full_name(self): 37 | return self.first + ' ' + self.last 38 | 39 | class SubPerson(Person): 40 | pass 41 | 42 | 43 | class TestHybridProperties(ModelTestCase): 44 | database = get_in_memory_db() 45 | requires = [Interval, Person] 46 | 47 | def setUp(self): 48 | super(TestHybridProperties, self).setUp() 49 | intervals = ( 50 | (1, 5), 51 | (2, 6), 52 | (3, 5), 53 | (2, 5)) 54 | for start, end in intervals: 55 | Interval.create(start=start, end=end) 56 | 57 | def test_hybrid_property(self): 58 | query = Interval.select().where(Interval.length == 4) 59 | self.assertSQL(query, ( 60 | 'SELECT "t1"."id", "t1"."start", "t1"."end" ' 61 | 'FROM "interval" AS "t1" ' 62 | 'WHERE (("t1"."end" - "t1"."start") = ?)'), [4]) 63 | 64 | results = sorted((i.start, i.end) for i in query) 65 | self.assertEqual(results, [(1, 5), (2, 6)]) 66 | 67 | query = Interval.select().order_by(Interval.id) 68 | self.assertEqual([i.length for i in query], [4, 4, 2, 3]) 69 | 70 | def test_hybrid_method(self): 71 | query = Interval.select().where(Interval.contains(2)) 72 | self.assertSQL(query, ( 73 | 'SELECT "t1"."id", "t1"."start", "t1"."end" ' 74 | 'FROM "interval" AS "t1" ' 75 | 'WHERE (("t1"."start" <= ?) AND ("t1"."end" > ?))'), [2, 2]) 76 | 77 | results = sorted((i.start, i.end) for i in query) 78 | self.assertEqual(results, [(1, 5), (2, 5), (2, 6)]) 79 | 80 | query = Interval.select().order_by(Interval.id) 81 | self.assertEqual([i.contains(2) for i in query], [1, 1, 0, 1]) 82 | 83 | def test_expression(self): 84 | query = Interval.select().where(Interval.radius == 2) 85 | self.assertSQL(query, ( 86 | 'SELECT "t1"."id", "t1"."start", "t1"."end" ' 87 | 'FROM "interval" AS "t1" ' 88 | 'WHERE ((ABS("t1"."end" - "t1"."start") / ?) = ?)'), [2, 2]) 89 | 90 | self.assertEqual(sorted((i.start, i.end) for i in query), 91 | [(1, 5), (2, 6)]) 92 | 93 | query = Interval.select().order_by(Interval.id) 94 | self.assertEqual([i.radius for i in query], [2, 2, 1, 1]) 95 | 96 | def test_string_fields(self): 97 | huey = Person.create(first='huey', last='cat') 98 | zaizee = Person.create(first='zaizee', last='kitten') 99 | 100 | self.assertEqual(huey.full_name, 'huey cat') 101 | self.assertEqual(zaizee.full_name, 'zaizee kitten') 102 | 103 | query = Person.select().where(Person.full_name.startswith('huey c')) 104 | huey_db = query.get() 105 | self.assertEqual(huey_db.id, huey.id) 106 | 107 | def test_hybrid_model_alias(self): 108 | Person.create(first='huey', last='cat') 109 | PA = Person.alias() 110 | query = PA.select(PA.full_name).where(PA.last == 'cat') 111 | self.assertSQL(query, ( 112 | 'SELECT (("t1"."first" || ?) || "t1"."last") ' 113 | 'FROM "person" AS "t1" WHERE ("t1"."last" = ?)'), [' ', 'cat']) 114 | self.assertEqual(query.tuples()[0], ('huey cat',)) 115 | 116 | @requires_models(SubPerson) 117 | def test_hybrid_subclass_model_alias(self): 118 | SubPerson.create(first='huey', last='cat') 119 | SA = SubPerson.alias() 120 | query = SA.select(SA.full_name).where(SA.last == 'cat') 121 | self.assertSQL(query, ( 122 | 'SELECT (("t1"."first" || ?) || "t1"."last") ' 123 | 'FROM "sub_person" AS "t1" WHERE ("t1"."last" = ?)'), [' ', 'cat']) 124 | self.assertEqual(query.tuples()[0], ('huey cat',)) 125 | 126 | 127 | class Order(TestModel): 128 | name = TextField() 129 | 130 | @hybrid_property 131 | def quantity(self): 132 | return sum([item.qt for item in self.items]) 133 | 134 | @quantity.expression 135 | def quantity(cls): 136 | return fn.SUM(Item.qt).alias('quantity') 137 | 138 | class Item(TestModel): 139 | order = ForeignKeyField(Order, backref='items') 140 | qt = IntegerField() 141 | 142 | 143 | class TestHybridWithRelationship(ModelTestCase): 144 | database = get_in_memory_db() 145 | requires = [Order, Item] 146 | 147 | def test_hybrid_with_relationship(self): 148 | data = ( 149 | ('a', (4, 3, 2, 1)), 150 | ('b', (1000, 300, 30, 7)), 151 | ('c', ())) 152 | for name, qts in data: 153 | o = Order.create(name=name) 154 | for qt in qts: 155 | Item.create(order=o, qt=qt) 156 | 157 | query = Order.select().order_by(Order.name) 158 | self.assertEqual([o.quantity for o in query], [10, 1337, 0]) 159 | 160 | query = (Order 161 | .select(Order.name, Order.quantity.alias('sql_qt')) 162 | .join(Item, JOIN.LEFT_OUTER) 163 | .group_by(Order.name) 164 | .order_by(Order.name)) 165 | self.assertEqual([o.sql_qt for o in query], [10, 1337, None]) 166 | -------------------------------------------------------------------------------- /tests/kv.py: -------------------------------------------------------------------------------- 1 | from peewee import IntegerField 2 | from playhouse.kv import KeyValue 3 | 4 | from .base import DatabaseTestCase 5 | from .base import db 6 | 7 | 8 | class TestKeyValue(DatabaseTestCase): 9 | def setUp(self): 10 | super(TestKeyValue, self).setUp() 11 | self._kvs = [] 12 | 13 | def tearDown(self): 14 | if self._kvs: 15 | self.database.drop_tables([kv.model for kv in self._kvs]) 16 | super(TestKeyValue, self).tearDown() 17 | 18 | def create_kv(self, **kwargs): 19 | kv = KeyValue(database=self.database, **kwargs) 20 | self._kvs.append(kv) 21 | return kv 22 | 23 | def test_basic_apis(self): 24 | KV = self.create_kv() 25 | KV['k1'] = 'v1' 26 | KV['k2'] = [0, 1, 2] 27 | 28 | self.assertEqual(KV['k1'], 'v1') 29 | self.assertEqual(KV['k2'], [0, 1, 2]) 30 | self.assertRaises(KeyError, lambda: KV['k3']) 31 | 32 | self.assertTrue((KV.key < 'k2') in KV) 33 | self.assertFalse((KV.key > 'k2') in KV) 34 | 35 | del KV['k1'] 36 | KV['k3'] = 'v3' 37 | 38 | self.assertFalse('k1' in KV) 39 | self.assertTrue('k3' in KV) 40 | self.assertEqual(sorted(KV.keys()), ['k2', 'k3']) 41 | self.assertEqual(len(KV), 2) 42 | 43 | data = dict(KV) 44 | self.assertEqual(data, { 45 | 'k2': [0, 1, 2], 46 | 'k3': 'v3'}) 47 | 48 | self.assertEqual(dict(KV), dict(KV.items())) 49 | 50 | self.assertEqual(KV.pop('k2'), [0, 1, 2]) 51 | self.assertRaises(KeyError, lambda: KV['k2']) 52 | self.assertRaises(KeyError, KV.pop, 'k2') 53 | 54 | self.assertEqual(KV.get('k3'), 'v3') 55 | self.assertTrue(KV.get('kx') is None) 56 | self.assertEqual(KV.get('kx', 'vx'), 'vx') 57 | 58 | self.assertTrue(KV.get('k4') is None) 59 | self.assertEqual(KV.setdefault('k4', 'v4'), 'v4') 60 | self.assertEqual(KV.get('k4'), 'v4') 61 | self.assertEqual(KV.get('k4', 'v5'), 'v4') 62 | 63 | KV.clear() 64 | self.assertEqual(len(KV), 0) 65 | 66 | def test_update(self): 67 | KV = self.create_kv() 68 | with self.assertQueryCount(1): 69 | KV.update(k1='v1', k2='v2', k3='v3') 70 | 71 | self.assertEqual(len(KV), 3) 72 | 73 | with self.assertQueryCount(1): 74 | KV.update(k1='v1-x', k3='v3-x', k4='v4') 75 | self.assertEqual(len(KV), 4) 76 | 77 | self.assertEqual(dict(KV), { 78 | 'k1': 'v1-x', 79 | 'k2': 'v2', 80 | 'k3': 'v3-x', 81 | 'k4': 'v4'}) 82 | 83 | KV['k1'] = 'v1-y' 84 | self.assertEqual(len(KV), 4) 85 | 86 | self.assertEqual(dict(KV), { 87 | 'k1': 'v1-y', 88 | 'k2': 'v2', 89 | 'k3': 'v3-x', 90 | 'k4': 'v4'}) 91 | 92 | def test_expressions(self): 93 | KV = self.create_kv(value_field=IntegerField(), ordered=True) 94 | with self.database.atomic(): 95 | for i in range(1, 11): 96 | KV['k%d' % i] = i 97 | 98 | self.assertEqual(KV[KV.key < 'k2'], [1, 10]) 99 | self.assertEqual(KV[KV.value > 7], [10, 8, 9]) 100 | self.assertEqual(KV[(KV.key > 'k2') & (KV.key < 'k6')], [3, 4, 5]) 101 | self.assertEqual(KV[KV.key == 'kx'], []) 102 | 103 | del KV[KV.key > 'k3'] 104 | self.assertEqual(dict(KV), { 105 | 'k1': 1, 106 | 'k2': 2, 107 | 'k3': 3, 108 | 'k10': 10}) 109 | 110 | KV[KV.value > 2] = 99 111 | self.assertEqual(dict(KV), { 112 | 'k1': 1, 113 | 'k2': 2, 114 | 'k3': 99, 115 | 'k10': 99}) 116 | 117 | def test_integer_keys(self): 118 | KV = self.create_kv(key_field=IntegerField(primary_key=True), 119 | ordered=True) 120 | KV[1] = 'v1' 121 | KV[2] = 'v2' 122 | KV[10] = 'v10' 123 | self.assertEqual(list(KV), [(1, 'v1'), (2, 'v2'), (10, 'v10')]) 124 | self.assertEqual(list(KV.keys()), [1, 2, 10]) 125 | self.assertEqual(list(KV.values()), ['v1', 'v2', 'v10']) 126 | 127 | del KV[2] 128 | KV[1] = 'v1-x' 129 | KV[3] = 'v3' 130 | self.assertEqual(dict(KV), { 131 | 1: 'v1-x', 132 | 3: 'v3', 133 | 10: 'v10'}) 134 | -------------------------------------------------------------------------------- /tests/libs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/peewee/fbabb56ed433009ee51ae655a6fa2b47cee93316/tests/libs/__init__.py -------------------------------------------------------------------------------- /tests/model_save.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | 3 | from .base import ModelTestCase 4 | from .base import TestModel 5 | from .base import requires_pglike 6 | 7 | 8 | class T1(TestModel): 9 | pk = AutoField() 10 | value = IntegerField() 11 | 12 | class T2(TestModel): 13 | pk = IntegerField(constraints=[SQL('DEFAULT 3')], primary_key=True) 14 | value = IntegerField() 15 | 16 | class T3(TestModel): 17 | pk = IntegerField(primary_key=True) 18 | value = IntegerField() 19 | 20 | class T4(TestModel): 21 | pk1 = IntegerField() 22 | pk2 = IntegerField() 23 | value = IntegerField() 24 | class Meta: 25 | primary_key = CompositeKey('pk1', 'pk2') 26 | 27 | class T5(TestModel): 28 | val = IntegerField(null=True) 29 | 30 | 31 | class TestPrimaryKeySaveHandling(ModelTestCase): 32 | requires = [T1, T2, T3, T4] 33 | 34 | def test_auto_field(self): 35 | # AutoField will be inserted if the PK is not set, after which the new 36 | # ID will be populated. 37 | t11 = T1(value=1) 38 | self.assertEqual(t11.save(), 1) 39 | self.assertTrue(t11.pk is not None) 40 | 41 | # Calling save() a second time will issue an update. 42 | t11.value = 100 43 | self.assertEqual(t11.save(), 1) 44 | 45 | # Verify the record was updated. 46 | t11_db = T1[t11.pk] 47 | self.assertEqual(t11_db.value, 100) 48 | 49 | # We can explicitly specify the value of an auto-incrementing 50 | # primary-key, but we must be sure to call save(force_insert=True), 51 | # otherwise peewee will attempt to do an update. 52 | t12 = T1(pk=1337, value=2) 53 | self.assertEqual(t12.save(), 0) 54 | self.assertEqual(T1.select().count(), 1) 55 | self.assertEqual(t12.save(force_insert=True), 1) 56 | 57 | # Attempting to force-insert an already-existing PK will fail with an 58 | # integrity error. 59 | with self.database.atomic(): 60 | with self.assertRaises(IntegrityError): 61 | t12.value = 3 62 | t12.save(force_insert=True) 63 | 64 | query = T1.select().order_by(T1.value).tuples() 65 | self.assertEqual(list(query), [(1337, 2), (t11.pk, 100)]) 66 | 67 | @requires_pglike 68 | def test_server_default_pk(self): 69 | # The new value of the primary-key will be returned to us, since 70 | # postgres supports RETURNING. 71 | t2 = T2(value=1) 72 | self.assertEqual(t2.save(), 1) 73 | self.assertEqual(t2.pk, 3) 74 | 75 | # Saving after the PK is set will issue an update. 76 | t2.value = 100 77 | self.assertEqual(t2.save(), 1) 78 | 79 | t2_db = T2[3] 80 | self.assertEqual(t2_db.value, 100) 81 | 82 | # If we just set the pk and try to save, peewee issues an update which 83 | # doesn't have any effect. 84 | t22 = T2(pk=2, value=20) 85 | self.assertEqual(t22.save(), 0) 86 | self.assertEqual(T2.select().count(), 1) 87 | 88 | # We can force-insert the value we specify explicitly. 89 | self.assertEqual(t22.save(force_insert=True), 1) 90 | self.assertEqual(T2[2].value, 20) 91 | 92 | def test_integer_field_pk(self): 93 | # For a non-auto-incrementing primary key, we have to use force_insert. 94 | t3 = T3(pk=2, value=1) 95 | self.assertEqual(t3.save(), 0) # Oops, attempts to do an update. 96 | self.assertEqual(T3.select().count(), 0) 97 | 98 | # Force to be an insert. 99 | self.assertEqual(t3.save(force_insert=True), 1) 100 | 101 | # Now we can update the value and call save() to issue an update. 102 | t3.value = 100 103 | self.assertEqual(t3.save(), 1) 104 | 105 | # Verify data is correct. 106 | t3_db = T3[2] 107 | self.assertEqual(t3_db.value, 100) 108 | 109 | def test_composite_pk(self): 110 | t4 = T4(pk1=1, pk2=2, value=10) 111 | 112 | # Will attempt to do an update on non-existant rows. 113 | self.assertEqual(t4.save(), 0) 114 | self.assertEqual(t4.save(force_insert=True), 1) 115 | 116 | # Modifying part of the composite PK and attempt an update will fail. 117 | t4.pk2 = 3 118 | t4.value = 30 119 | self.assertEqual(t4.save(), 0) 120 | 121 | t4.pk2 = 2 122 | self.assertEqual(t4.save(), 1) 123 | 124 | t4_db = T4[1, 2] 125 | self.assertEqual(t4_db.value, 30) 126 | 127 | @requires_pglike 128 | def test_returning_object(self): 129 | query = T2.insert(value=10).returning(T2).objects() 130 | t2_db, = list(query) 131 | self.assertEqual(t2_db.pk, 3) 132 | self.assertEqual(t2_db.value, 10) 133 | 134 | 135 | class TestSaveNoData(ModelTestCase): 136 | requires = [T5] 137 | 138 | def test_save_no_data(self): 139 | t5 = T5.create() 140 | self.assertTrue(t5.id >= 1) 141 | 142 | t5.val = 3 143 | t5.save() 144 | 145 | t5_db = T5.get(T5.id == t5.id) 146 | self.assertEqual(t5_db.val, 3) 147 | 148 | t5.val = None 149 | t5.save() 150 | 151 | t5_db = T5.get(T5.id == t5.id) 152 | self.assertTrue(t5_db.val is None) 153 | 154 | def test_save_no_data2(self): 155 | t5 = T5.create() 156 | 157 | t5_db = T5.get(T5.id == t5.id) 158 | t5_db.save() 159 | 160 | t5_db = T5.get(T5.id == t5.id) 161 | self.assertTrue(t5_db.val is None) 162 | 163 | def test_save_no_data3(self): 164 | t5 = T5.create() 165 | self.assertRaises(ValueError, t5.save) 166 | 167 | def test_save_only_no_data(self): 168 | t5 = T5.create(val=1) 169 | t5.val = 2 170 | self.assertRaises(ValueError, t5.save, only=[]) 171 | t5_db = T5.get(T5.id == t5.id) 172 | self.assertEqual(t5_db.val, 1) 173 | -------------------------------------------------------------------------------- /tests/mysql_ext.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from peewee import * 4 | from playhouse.mysql_ext import JSONField 5 | from playhouse.mysql_ext import Match 6 | 7 | from .base import IS_MYSQL_JSON 8 | from .base import ModelDatabaseTestCase 9 | from .base import ModelTestCase 10 | from .base import TestModel 11 | from .base import db_loader 12 | from .base import requires_mysql 13 | from .base import skip_if 14 | from .base import skip_unless 15 | 16 | 17 | try: 18 | import mariadb 19 | except ImportError: 20 | mariadb = mariadb_db = None 21 | else: 22 | mariadb_db = db_loader('mariadb') 23 | try: 24 | import mysql.connector as mysql_connector 25 | except ImportError: 26 | mysql_connector = None 27 | 28 | mysql_ext_db = db_loader('mysqlconnector') 29 | 30 | 31 | class Person(TestModel): 32 | first = CharField() 33 | last = CharField() 34 | dob = DateField(default=datetime.date(2000, 1, 1)) 35 | 36 | 37 | class Note(TestModel): 38 | person = ForeignKeyField(Person, backref='notes') 39 | content = TextField() 40 | timestamp = DateTimeField(default=datetime.datetime.now) 41 | 42 | 43 | class KJ(TestModel): 44 | key = CharField(primary_key=True, max_length=100) 45 | data = JSONField() 46 | 47 | 48 | @requires_mysql 49 | @skip_if(mysql_connector is None, 'mysql-connector not installed') 50 | class TestMySQLConnector(ModelTestCase): 51 | database = mysql_ext_db 52 | requires = [Person, Note] 53 | 54 | def test_basic_operations(self): 55 | with self.database.atomic(): 56 | charlie, huey, zaizee = [Person.create(first=f, last='leifer') 57 | for f in ('charlie', 'huey', 'zaizee')] 58 | # Use nested-transaction. 59 | with self.database.atomic(): 60 | data = ( 61 | (charlie, ('foo', 'bar', 'zai')), 62 | (huey, ('meow', 'purr', 'hiss')), 63 | (zaizee, ())) 64 | for person, notes in data: 65 | for note in notes: 66 | Note.create(person=person, content=note) 67 | 68 | with self.database.atomic() as sp: 69 | Person.create(first='x', last='y') 70 | sp.rollback() 71 | 72 | people = Person.select().order_by(Person.first) 73 | self.assertEqual([person.first for person in people], 74 | ['charlie', 'huey', 'zaizee']) 75 | 76 | with self.assertQueryCount(1): 77 | notes = (Note 78 | .select(Note, Person) 79 | .join(Person) 80 | .order_by(Note.content)) 81 | self.assertEqual([(n.person.first, n.content) for n in notes], [ 82 | ('charlie', 'bar'), 83 | ('charlie', 'foo'), 84 | ('huey', 'hiss'), 85 | ('huey', 'meow'), 86 | ('huey', 'purr'), 87 | ('charlie', 'zai')]) 88 | 89 | 90 | @requires_mysql 91 | @skip_if(mariadb is None, 'mariadb connector not installed') 92 | class TestMariaDBConnector(TestMySQLConnector): 93 | database = mariadb_db 94 | 95 | 96 | @requires_mysql 97 | @skip_unless(IS_MYSQL_JSON, 'requires MySQL 5.7+ or 8.x') 98 | class TestMySQLJSONField(ModelTestCase): 99 | requires = [KJ] 100 | 101 | def test_mysql_json_field(self): 102 | values = ( 103 | 0, 1.0, 2.3, 104 | True, False, 105 | 'string', 106 | ['foo', 'bar', 'baz'], 107 | {'k1': 'v1', 'k2': 'v2'}, 108 | {'k3': [0, 1.0, 2.3], 'k4': {'x1': 'y1', 'x2': 'y2'}}) 109 | for i, value in enumerate(values): 110 | # Verify data can be written. 111 | kj = KJ.create(key='k%s' % i, data=value) 112 | 113 | # Verify value is deserialized correctly. 114 | kj_db = KJ['k%s' % i] 115 | self.assertEqual(kj_db.data, value) 116 | 117 | kj = KJ.select().where(KJ.data.extract('$.k1') == 'v1').get() 118 | self.assertEqual(kj.key, 'k7') 119 | 120 | with self.assertRaises(IntegrityError): 121 | KJ.create(key='kx', data=None) 122 | 123 | 124 | @requires_mysql 125 | class TestMatchExpression(ModelDatabaseTestCase): 126 | requires = [Person] 127 | 128 | def test_match_expression(self): 129 | query = (Person 130 | .select() 131 | .where(Match(Person.first, 'charlie'))) 132 | self.assertSQL(query, ( 133 | 'SELECT "t1"."id", "t1"."first", "t1"."last", "t1"."dob" ' 134 | 'FROM "person" AS "t1" ' 135 | 'WHERE MATCH("t1"."first") AGAINST(?)'), ['charlie']) 136 | 137 | query = (Person 138 | .select() 139 | .where(Match((Person.first, Person.last), 'huey AND zaizee', 140 | 'IN BOOLEAN MODE'))) 141 | self.assertSQL(query, ( 142 | 'SELECT "t1"."id", "t1"."first", "t1"."last", "t1"."dob" ' 143 | 'FROM "person" AS "t1" ' 144 | 'WHERE MATCH("t1"."first", "t1"."last") ' 145 | 'AGAINST(? IN BOOLEAN MODE)'), ['huey AND zaizee']) 146 | -------------------------------------------------------------------------------- /tests/returning.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from peewee import * 4 | from peewee import __sqlite_version__ 5 | 6 | from .base import db 7 | from .base import skip_unless 8 | from .base import IS_SQLITE 9 | from .base import ModelTestCase 10 | from .base import TestModel 11 | 12 | 13 | class Reg(TestModel): 14 | k = CharField() 15 | v = IntegerField() 16 | x = IntegerField() 17 | class Meta: 18 | indexes = ( 19 | (('k', 'v'), True), 20 | ) 21 | 22 | 23 | returning_support = db.returning_clause or (IS_SQLITE and 24 | __sqlite_version__ >= (3, 35, 0)) 25 | 26 | 27 | @skip_unless(returning_support, 'database does not support RETURNING') 28 | class TestReturningIntegration(ModelTestCase): 29 | requires = [Reg] 30 | 31 | def test_crud(self): 32 | iq = Reg.insert_many([('k1', 1, 0), ('k2', 2, 0)]).returning(Reg) 33 | self.assertEqual([(r.id is not None, r.k, r.v) for r in iq.execute()], 34 | [(True, 'k1', 1), (True, 'k2', 2)]) 35 | 36 | iq = (Reg 37 | .insert_many([('k1', 1, 1), ('k2', 2, 1), ('k3', 3, 0)]) 38 | .on_conflict( 39 | conflict_target=[Reg.k, Reg.v], 40 | preserve=[Reg.x], 41 | update={Reg.v: Reg.v + 1}, 42 | where=(Reg.k != 'k1')) 43 | .returning(Reg)) 44 | ic = iq.execute() 45 | self.assertEqual([(r.id is not None, r.k, r.v, r.x) for r in ic], [ 46 | (True, 'k2', 3, 1), 47 | (True, 'k3', 3, 0)]) 48 | 49 | uq = (Reg 50 | .update({Reg.v: Reg.v - 1, Reg.x: Reg.x + 1}) 51 | .where(Reg.k != 'k1') 52 | .returning(Reg)) 53 | self.assertEqual([(r.k, r.v, r.x) for r in uq.execute()], [ 54 | ('k2', 2, 2), ('k3', 2, 1)]) 55 | 56 | dq = Reg.delete().where(Reg.k != 'k1').returning(Reg) 57 | self.assertEqual([(r.k, r.v, r.x) for r in dq.execute()], [ 58 | ('k2', 2, 2), ('k3', 2, 1)]) 59 | 60 | def test_returning_expression(self): 61 | Rs = (Reg.v + Reg.x).alias('s') 62 | iq = (Reg 63 | .insert_many([('k1', 1, 10), ('k2', 2, 20)]) 64 | .returning(Reg.k, Reg.v, Rs)) 65 | self.assertEqual([(r.k, r.v, r.s) for r in iq.execute()], [ 66 | ('k1', 1, 11), ('k2', 2, 22)]) 67 | 68 | uq = (Reg 69 | .update({Reg.k: Reg.k + 'x', Reg.v: Reg.v + 1}) 70 | .returning(Reg.k, Reg.v, Rs)) 71 | self.assertEqual([(r.k, r.v, r.s) for r in uq.execute()], [ 72 | ('k1x', 2, 12), ('k2x', 3, 23)]) 73 | 74 | dq = Reg.delete().returning(Reg.k, Reg.v, Rs) 75 | self.assertEqual([(r.k, r.v, r.s) for r in dq.execute()], [ 76 | ('k1x', 2, 12), ('k2x', 3, 23)]) 77 | 78 | def test_returning_types(self): 79 | Rs = (Reg.v + Reg.x).alias('s') 80 | mapping = ( 81 | ((lambda q: q), (lambda r: (r.k, r.v, r.s))), 82 | ((lambda q: q.dicts()), (lambda r: (r['k'], r['v'], r['s']))), 83 | ((lambda q: q.tuples()), (lambda r: r)), 84 | ((lambda q: q.namedtuples()), (lambda r: (r.k, r.v, r.s)))) 85 | 86 | for qconv, r2t in mapping: 87 | iq = (Reg 88 | .insert_many([('k1', 1, 10), ('k2', 2, 20)]) 89 | .returning(Reg.k, Reg.v, Rs)) 90 | self.assertEqual([r2t(r) for r in qconv(iq).execute()], [ 91 | ('k1', 1, 11), ('k2', 2, 22)]) 92 | 93 | uq = (Reg 94 | .update({Reg.k: Reg.k + 'x', Reg.v: Reg.v + 1}) 95 | .returning(Reg.k, Reg.v, Rs)) 96 | self.assertEqual([r2t(r) for r in qconv(uq).execute()], [ 97 | ('k1x', 2, 12), ('k2x', 3, 23)]) 98 | 99 | dq = Reg.delete().returning(Reg.k, Reg.v, Rs) 100 | self.assertEqual([r2t(r) for r in qconv(dq).execute()], [ 101 | ('k1x', 2, 12), ('k2x', 3, 23)]) 102 | -------------------------------------------------------------------------------- /tests/sqlcipher_ext.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | from hashlib import sha1 4 | 5 | from peewee import DatabaseError 6 | from playhouse.sqlcipher_ext import * 7 | from playhouse.sqlite_ext import * 8 | 9 | from .base import ModelTestCase 10 | from .base import TestModel 11 | 12 | 13 | PASSPHRASE = 'testing sqlcipher' 14 | PRAGMAS = { 15 | 'kdf_iter': 10, # Much faster for testing. Totally unsafe. 16 | 'cipher_log_level': 'none', 17 | } 18 | db = SqlCipherDatabase('peewee_test.dbc', passphrase=PASSPHRASE, 19 | pragmas=PRAGMAS) 20 | ext_db = SqlCipherExtDatabase('peewee_test.dbx', passphrase=PASSPHRASE, 21 | pragmas=PRAGMAS) 22 | 23 | 24 | @ext_db.func('shazam') 25 | def shazam(s): 26 | return sha1((s or '').encode('utf-8')).hexdigest()[:5] 27 | 28 | 29 | class Thing(TestModel): 30 | name = CharField() 31 | 32 | 33 | class FTSNote(FTSModel, TestModel): 34 | content = TextField() 35 | 36 | 37 | class Note(TestModel): 38 | content = TextField() 39 | timestamp = DateTimeField(default=datetime.datetime.now) 40 | 41 | 42 | class CleanUpModelTestCase(ModelTestCase): 43 | def tearDown(self): 44 | super(CleanUpModelTestCase, self).tearDown() 45 | if os.path.exists(self.database.database): 46 | os.unlink(self.database.database) 47 | 48 | 49 | class SqlCipherTestCase(CleanUpModelTestCase): 50 | database = db 51 | requires = [Thing] 52 | 53 | def test_good_and_bad_passphrases(self): 54 | things = ('t1', 't2', 't3') 55 | for thing in things: 56 | Thing.create(name=thing) 57 | 58 | # Try to open db with wrong passphrase 59 | bad_db = SqlCipherDatabase(db.database, passphrase='wrong passphrase') 60 | self.assertRaises(DatabaseError, bad_db.get_tables) 61 | 62 | # Assert that we can still access the data with the good passphrase. 63 | query = Thing.select().order_by(Thing.name) 64 | self.assertEqual([t.name for t in query], ['t1', 't2', 't3']) 65 | 66 | def test_rekey(self): 67 | things = ('t1', 't2', 't3') 68 | for thing in things: 69 | Thing.create(name=thing) 70 | 71 | self.database.rekey('a new passphrase') 72 | 73 | db2 = SqlCipherDatabase(db.database, passphrase='a new passphrase', 74 | pragmas=PRAGMAS) 75 | cursor = db2.execute_sql('select name from thing order by name;') 76 | self.assertEqual([name for name, in cursor], ['t1', 't2', 't3']) 77 | 78 | query = Thing.select().order_by(Thing.name) 79 | self.assertEqual([t.name for t in query], ['t1', 't2', 't3']) 80 | 81 | self.database.close() 82 | self.database.connect() 83 | 84 | query = Thing.select().order_by(Thing.name) 85 | self.assertEqual([t.name for t in query], ['t1', 't2', 't3']) 86 | 87 | # Re-set to the original passphrase. 88 | self.database.rekey(PASSPHRASE) 89 | 90 | def test_empty_passphrase(self): 91 | db = SqlCipherDatabase(':memory:') 92 | 93 | class CM(TestModel): 94 | data = TextField() 95 | class Meta: 96 | database = db 97 | 98 | db.connect() 99 | db.create_tables([CM]) 100 | cm = CM.create(data='foo') 101 | cm_db = CM.get(CM.data == 'foo') 102 | self.assertEqual(cm_db.id, cm.id) 103 | self.assertEqual(cm_db.data, 'foo') 104 | 105 | 106 | config_db = SqlCipherDatabase('peewee_test.dbc', pragmas={ 107 | 'kdf_iter': 1234, 108 | 'cipher_page_size': 8192}, passphrase=PASSPHRASE) 109 | 110 | class TestSqlCipherConfiguration(CleanUpModelTestCase): 111 | database = config_db 112 | 113 | def test_configuration_via_pragma(self): 114 | # Write some data so the database file is created. 115 | self.database.execute_sql('create table foo (data TEXT)') 116 | self.database.close() 117 | 118 | self.database.connect() 119 | self.assertEqual(int(self.database.pragma('kdf_iter')), 1234) 120 | self.assertEqual(int(self.database.pragma('cipher_page_size')), 8192) 121 | self.assertTrue('foo' in self.database.get_tables()) 122 | 123 | 124 | class SqlCipherExtTestCase(CleanUpModelTestCase): 125 | database = ext_db 126 | requires = [Note] 127 | 128 | def setUp(self): 129 | super(SqlCipherExtTestCase, self).setUp() 130 | FTSNote._meta.database = ext_db 131 | FTSNote.drop_table(True) 132 | FTSNote.create_table(tokenize='porter', content=Note.content) 133 | 134 | def tearDown(self): 135 | FTSNote.drop_table(True) 136 | super(SqlCipherExtTestCase, self).tearDown() 137 | 138 | def test_fts(self): 139 | strings = [ 140 | 'python and peewee for working with databases', 141 | 'relational databases are the best', 142 | 'sqlite is the best relational database', 143 | 'sqlcipher is a cool database extension'] 144 | for s in strings: 145 | Note.create(content=s) 146 | FTSNote.rebuild() 147 | 148 | query = (FTSNote 149 | .select(FTSNote, FTSNote.rank().alias('score')) 150 | .where(FTSNote.match('relational databases')) 151 | .order_by(SQL('score').desc())) 152 | notes = [note.content for note in query] 153 | self.assertEqual(notes, [ 154 | 'relational databases are the best', 155 | 'sqlite is the best relational database']) 156 | 157 | alt_conn = SqliteDatabase(ext_db.database) 158 | self.assertRaises( 159 | DatabaseError, 160 | alt_conn.execute_sql, 161 | 'SELECT * FROM "%s"' % (FTSNote._meta.table_name)) 162 | 163 | def test_func(self): 164 | Note.create(content='hello') 165 | Note.create(content='baz') 166 | Note.create(content='nug') 167 | 168 | query = (Note 169 | .select(Note.content, fn.shazam(Note.content).alias('shz')) 170 | .order_by(Note.id) 171 | .dicts()) 172 | results = list(query) 173 | self.assertEqual(results, [ 174 | {'content': 'hello', 'shz': 'aaf4c'}, 175 | {'content': 'baz', 'shz': 'bbe96'}, 176 | {'content': 'nug', 'shz': '52616'}, 177 | ]) 178 | -------------------------------------------------------------------------------- /tests/sqlite_changelog.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from peewee import * 4 | from playhouse.sqlite_changelog import ChangeLog 5 | from playhouse.sqlite_ext import JSONField 6 | from playhouse.sqlite_ext import SqliteExtDatabase 7 | 8 | from .base import ModelTestCase 9 | from .base import TestModel 10 | from .base import requires_models 11 | from .base import skip_unless 12 | from .sqlite_helpers import json_installed 13 | 14 | 15 | database = SqliteExtDatabase(':memory:', pragmas={'foreign_keys': 1}) 16 | 17 | 18 | class Person(TestModel): 19 | name = TextField() 20 | dob = DateField() 21 | 22 | 23 | class Note(TestModel): 24 | person = ForeignKeyField(Person, on_delete='CASCADE') 25 | content = TextField() 26 | timestamp = TimestampField() 27 | status = IntegerField(default=0) 28 | 29 | 30 | class CT1(TestModel): 31 | f1 = TextField() 32 | f2 = IntegerField(null=True) 33 | f3 = FloatField() 34 | fi = IntegerField() 35 | 36 | 37 | class CT2(TestModel): 38 | data = JSONField() # Diff of json? 39 | 40 | 41 | changelog = ChangeLog(database) 42 | CL = changelog.model 43 | 44 | 45 | @skip_unless(json_installed(), 'requires sqlite json1') 46 | class TestChangeLog(ModelTestCase): 47 | database = database 48 | requires = [Person, Note] 49 | 50 | def setUp(self): 51 | super(TestChangeLog, self).setUp() 52 | changelog.install(Person) 53 | changelog.install(Note, skip_fields=['timestamp']) 54 | self.last_index = 0 55 | 56 | def assertChanges(self, changes, last_index=None): 57 | last_index = last_index or self.last_index 58 | query = (CL 59 | .select(CL.action, CL.table, CL.changes) 60 | .order_by(CL.id) 61 | .offset(last_index)) 62 | accum = list(query.tuples()) 63 | self.last_index += len(accum) 64 | self.assertEqual(accum, changes) 65 | 66 | def test_changelog(self): 67 | huey = Person.create(name='huey', dob=datetime.date(2010, 5, 1)) 68 | zaizee = Person.create(name='zaizee', dob=datetime.date(2013, 1, 1)) 69 | self.assertChanges([ 70 | ('INSERT', 'person', {'name': [None, 'huey'], 71 | 'dob': [None, '2010-05-01']}), 72 | ('INSERT', 'person', {'name': [None, 'zaizee'], 73 | 'dob': [None, '2013-01-01']})]) 74 | 75 | zaizee.dob = datetime.date(2013, 2, 2) 76 | zaizee.save() 77 | self.assertChanges([ 78 | ('UPDATE', 'person', {'dob': ['2013-01-01', '2013-02-02']})]) 79 | 80 | zaizee.name = 'zaizee-x' 81 | zaizee.dob = datetime.date(2013, 3, 3) 82 | zaizee.save() 83 | 84 | huey.save() # No changes. 85 | 86 | self.assertChanges([ 87 | ('UPDATE', 'person', {'name': ['zaizee', 'zaizee-x'], 88 | 'dob': ['2013-02-02', '2013-03-03']}), 89 | ('UPDATE', 'person', {})]) 90 | 91 | zaizee.delete_instance() 92 | self.assertChanges([ 93 | ('DELETE', 'person', {'name': ['zaizee-x', None], 94 | 'dob': ['2013-03-03', None]})]) 95 | 96 | nh1 = Note.create(person=huey, content='huey1', status=1) 97 | nh2 = Note.create(person=huey, content='huey2', status=2) 98 | self.assertChanges([ 99 | ('INSERT', 'note', {'person_id': [None, huey.id], 100 | 'content': [None, 'huey1'], 101 | 'status': [None, 1]}), 102 | ('INSERT', 'note', {'person_id': [None, huey.id], 103 | 'content': [None, 'huey2'], 104 | 'status': [None, 2]})]) 105 | 106 | nh1.content = 'huey1-x' 107 | nh1.status = 0 108 | nh1.save() 109 | 110 | mickey = Person.create(name='mickey', dob=datetime.date(2009, 8, 1)) 111 | nh2.person = mickey 112 | nh2.save() 113 | 114 | self.assertChanges([ 115 | ('UPDATE', 'note', {'content': ['huey1', 'huey1-x'], 116 | 'status': [1, 0]}), 117 | ('INSERT', 'person', {'name': [None, 'mickey'], 118 | 'dob': [None, '2009-08-01']}), 119 | ('UPDATE', 'note', {'person_id': [huey.id, mickey.id]})]) 120 | 121 | mickey.delete_instance() 122 | self.assertChanges([ 123 | ('DELETE', 'note', {'person_id': [mickey.id, None], 124 | 'content': ['huey2', None], 125 | 'status': [2, None]}), 126 | ('DELETE', 'person', {'name': ['mickey', None], 127 | 'dob': ['2009-08-01', None]})]) 128 | 129 | @requires_models(CT1) 130 | def test_changelog_details(self): 131 | changelog.install(CT1, skip_fields=['fi'], insert=False, delete=False) 132 | 133 | c1 = CT1.create(f1='v1', f2=1, f3=1.5, fi=0) 134 | self.assertChanges([]) 135 | 136 | CT1.update(f1='v1-x', f2=2, f3=2.5, fi=1).execute() 137 | self.assertChanges([ 138 | ('UPDATE', 'ct1', { 139 | 'f1': ['v1', 'v1-x'], 140 | 'f2': [1, 2], 141 | 'f3': [1.5, 2.5]})]) 142 | 143 | c1.f2 = None 144 | c1.save() # Overwrites previously-changed fields. 145 | self.assertChanges([('UPDATE', 'ct1', { 146 | 'f1': ['v1-x', 'v1'], 147 | 'f2': [2, None], 148 | 'f3': [2.5, 1.5]})]) 149 | 150 | c1.delete_instance() 151 | self.assertChanges([]) 152 | 153 | @requires_models(CT2) 154 | def test_changelog_jsonfield(self): 155 | changelog.install(CT2) 156 | 157 | ca = CT2.create(data={'k1': 'v1'}) 158 | cb = CT2.create(data=['i0', 'i1', 'i2']) 159 | cc = CT2.create(data='hello') 160 | 161 | self.assertChanges([ 162 | ('INSERT', 'ct2', {'data': [None, {'k1': 'v1'}]}), 163 | ('INSERT', 'ct2', {'data': [None, ['i0', 'i1', 'i2']]}), 164 | ('INSERT', 'ct2', {'data': [None, 'hello']})]) 165 | 166 | ca.data['k1'] = 'v1-x' 167 | cb.data.append('i3') 168 | cc.data = 'world' 169 | 170 | ca.save() 171 | cb.save() 172 | cc.save() 173 | 174 | self.assertChanges([ 175 | ('UPDATE', 'ct2', {'data': [{'k1': 'v1'}, {'k1': 'v1-x'}]}), 176 | ('UPDATE', 'ct2', {'data': [['i0', 'i1', 'i2'], 177 | ['i0', 'i1', 'i2', 'i3']]}), 178 | ('UPDATE', 'ct2', {'data': ['hello', 'world']})]) 179 | 180 | cc.data = 13.37 181 | cc.save() 182 | self.assertChanges([('UPDATE', 'ct2', {'data': ['world', 13.37]})]) 183 | 184 | ca.delete_instance() 185 | self.assertChanges([ 186 | ('DELETE', 'ct2', {'data': [{'k1': 'v1-x'}, None]})]) 187 | -------------------------------------------------------------------------------- /tests/sqlite_helpers.py: -------------------------------------------------------------------------------- 1 | from peewee import sqlite3 2 | 3 | 4 | def json_installed(): 5 | if sqlite3.sqlite_version_info < (3, 9, 0): 6 | return False 7 | tmp_db = sqlite3.connect(':memory:') 8 | try: 9 | tmp_db.execute('select json(?)', (1337,)) 10 | except: 11 | return False 12 | finally: 13 | tmp_db.close() 14 | return True 15 | 16 | 17 | def json_patch_installed(): 18 | return sqlite3.sqlite_version_info >= (3, 18, 0) 19 | 20 | 21 | def json_text_installed(): 22 | return sqlite3.sqlite_version_info >= (3, 38, 0) 23 | 24 | def jsonb_installed(): 25 | return sqlite3.sqlite_version_info >= (3, 45, 0) 26 | 27 | 28 | def compile_option(p): 29 | if not hasattr(compile_option, '_pragma_cache'): 30 | conn = sqlite3.connect(':memory:') 31 | curs = conn.execute('pragma compile_options') 32 | opts = [opt.lower().split('=')[0].strip() for opt, in curs.fetchall()] 33 | compile_option._pragma_cache = set(opts) 34 | return p in compile_option._pragma_cache 35 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from .base import ModelTestCase 4 | from .base import TestModel 5 | 6 | from peewee import * 7 | from playhouse.test_utils import assert_query_count 8 | from playhouse.test_utils import count_queries 9 | 10 | 11 | class Data(TestModel): 12 | key = CharField() 13 | 14 | class Meta: 15 | order_by = ('key',) 16 | 17 | class DataItem(TestModel): 18 | data = ForeignKeyField(Data, backref='items') 19 | value = CharField() 20 | 21 | class Meta: 22 | order_by = ('value',) 23 | 24 | 25 | class TestQueryCounter(ModelTestCase): 26 | requires = [DataItem, Data] 27 | 28 | def test_count(self): 29 | with count_queries() as count: 30 | Data.create(key='k1') 31 | Data.create(key='k2') 32 | 33 | self.assertEqual(count.count, 2) 34 | 35 | with count_queries() as count: 36 | items = [item.key for item in Data.select().order_by(Data.key)] 37 | self.assertEqual(items, ['k1', 'k2']) 38 | 39 | Data.get(Data.key == 'k1') 40 | Data.get(Data.key == 'k2') 41 | 42 | self.assertEqual(count.count, 3) 43 | 44 | def test_only_select(self): 45 | with count_queries(only_select=True) as count: 46 | for i in range(10): 47 | Data.create(key=str(i)) 48 | 49 | items = [item.key for item in Data.select()] 50 | Data.get(Data.key == '0') 51 | Data.get(Data.key == '9') 52 | 53 | Data.delete().where( 54 | Data.key << ['1', '3', '5', '7', '9']).execute() 55 | 56 | items = [item.key for item in Data.select().order_by(Data.key)] 57 | self.assertEqual(items, ['0', '2', '4', '6', '8']) 58 | 59 | self.assertEqual(count.count, 4) 60 | 61 | def test_assert_query_count_decorator(self): 62 | @assert_query_count(2) 63 | def will_fail_under(): 64 | Data.create(key='x') 65 | 66 | @assert_query_count(2) 67 | def will_fail_over(): 68 | for i in range(3): 69 | Data.create(key=str(i)) 70 | 71 | @assert_query_count(4) 72 | def will_succeed(): 73 | for i in range(4): 74 | Data.create(key=str(i + 100)) 75 | 76 | will_succeed() 77 | self.assertRaises(AssertionError, will_fail_under) 78 | self.assertRaises(AssertionError, will_fail_over) 79 | 80 | def test_assert_query_count_ctx_mgr(self): 81 | with assert_query_count(3): 82 | for i in range(3): 83 | Data.create(key=str(i)) 84 | 85 | def will_fail(): 86 | with assert_query_count(2): 87 | Data.create(key='x') 88 | 89 | self.assertRaises(AssertionError, will_fail) 90 | 91 | @assert_query_count(3) 92 | def test_only_three(self): 93 | for i in range(3): 94 | Data.create(key=str(i)) 95 | --------------------------------------------------------------------------------