From 9a79aca830049269018597f96d1ef85248d21d94 Mon Sep 17 00:00:00 2001 From: Charles Leifer Date: Sat, 27 Nov 2010 17:27:20 -0600 Subject: [PATCH] Checking in first run at some docs --- docs/Makefile | 130 +++++++++++ docs/conf.py | 216 ++++++++++++++++++ docs/index.rst | 69 ++++++ docs/make.bat | 155 +++++++++++++ docs/peewee/example.rst | 271 ++++++++++++++++++++++ docs/peewee/installation.rst | 16 ++ docs/peewee/models.rst | 187 +++++++++++++++ docs/peewee/querying.rst | 432 +++++++++++++++++++++++++++++++++++ 8 files changed, 1476 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/peewee/example.rst create mode 100644 docs/peewee/installation.rst create mode 100644 docs/peewee/models.rst create mode 100644 docs/peewee/querying.rst diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 000000000..079300a5b --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,130 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/peewee.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/peewee.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/peewee" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/peewee" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..aeeb1f50b --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +# +# peewee documentation build configuration file, created by +# sphinx-quickstart on Fri Nov 26 11:05:15 2010. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'peewee' +copyright = u'2010, charles leifer' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.3.0' +# The full version, including alpha/beta/rc tags. +release = '0.3.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'peeweedoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'peewee.tex', u'peewee Documentation', + u'charles leifer', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'peewee', u'peewee Documentation', + [u'charles leifer'], 1) +] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..c32a550f2 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,69 @@ +.. peewee documentation master file, created by + sphinx-quickstart on Thu Nov 25 21:20:29 2010. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +peewee +====== + +* a small orm +* written in python +* provides a lightweight querying interface over sql +* uses sql concepts when querying, like joins and where clauses + + +Examples:: + + # a simple query selecting a user + User.get(username='charles') + + # get the staff and super users + editors = User.select().where(Q(is_staff=True) | Q(is_superuser=True)) + + # get tweets by editors + Tweet.select().where(user__in=editors) + + # how many active users are there? + User.select().where(active=True).count() + + # paginate the user table and show me page 3 (users 41-60) + User.select().order_by(('username', 'asc')).paginate(3, 20) + + # order users by number of tweets + User.select({ + User: ['*'], + Tweet: [Count('id', 'num_tweets')] + }).group_by('id').join(Tweet).order_by(('num_tweets', 'desc')) + + +Why? +---- + +peewee began when I was working on a small app in flask and found myself writing +lots of queries and wanting a very simple abstraction on top of the sql. I had +so much fun working on it that I kept adding features. My goal has always been, +though, to keep the implementation incredibly simple. I've made a couple dives +into django's orm but have never come away with a deep understanding of its +implementation. peewee is small enough that its my hope anyone with an interest +in orms will be able to understand the code without too much trouble. + + +Contents: +--------- + +.. toctree:: + :maxdepth: 3 + :glob: + + peewee/installation + peewee/example + peewee/models + peewee/querying + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 000000000..92ec5380b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,155 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\peewee.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\peewee.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/docs/peewee/example.rst b/docs/peewee/example.rst new file mode 100644 index 000000000..b1eb09b50 --- /dev/null +++ b/docs/peewee/example.rst @@ -0,0 +1,271 @@ +Example app +=========== + +.. image:: tweepee.jpg + +peewee ships with an example web app that runs on the +`Flask `_ microframework. If you already have flask +and its dependencies installed you should be good to go, otherwise install from +the included requirements file:: + + cd example/ + pip install -r requirements.txt + + +Running the example +------------------- + +After ensuring that flask, jinja2, werkzeug and sqlite3 are all installed, +switch to the example directory and execute the *run_example.py* script:: + + python run_example.py + + +Diving into the code +-------------------- + +Models +^^^^^^ + +In the spirit of the ur-python framework, django, peewee uses declarative model +definitions. If you're not familiar with django, the idea is that you declare +a class with some members which map directly to the database schema. For the +twitter clone, there are just three models: + +User: + represents a user account and stores the username and password, an email + address for generating avatars using *gravatar*, and a datetime field + indicating when that account was created + +Relationship: + this is a "utility model" that contains two foreign-keys to + the User model and represents *"following"*. + +Message: + analagous to a tweet. this model stores the text content of + the message, when it was created, and who posted it (foreign key to User). + +If you like UML, this is basically what it looks like: + +.. image:: schema.jpg + + +Here is the code:: + + database = peewee.Database(DATABASE) + + # model definitions + class User(peewee.Model): + username = peewee.CharField() + password = peewee.CharField() + email = peewee.CharField() + join_date = peewee.DateTimeField() + + class Meta: + database = database + + def following(self): + return User.select().join( + Relationship, on='to_user_id' + ).where(from_user=self).order_by('username') + + def followers(self): + return User.select().join( + Relationship + ).where(to_user=self).order_by('username') + + def is_following(self, user): + return Relationship.select().where( + from_user=self, + to_user=user + ).count() > 0 + + def gravatar_url(self, size=80): + return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ + (md5(self.email.strip().lower().encode('utf-8')).hexdigest(), size) + + + class Relationship(peewee.Model): + from_user = peewee.ForeignKeyField(User, related_name='relationships') + to_user = peewee.ForeignKeyField(User, related_name='related_to') + + class Meta: + database = database + + + class Message(peewee.Model): + user = peewee.ForeignKeyField(User) + content = peewee.TextField() + pub_date = peewee.DateTimeField() + + class Meta: + database = database + + +peewee supports a handful of field types which map to different column types in +sqlite. Conversion between python and the database is handled transparently, +including the proper handling of None/NULL. + +.. note:: you might have noticed that each model sets the database attribute + explicitly. by default peewee will use "peewee.db". explicitly setting this + instructs peewee to use the database specified by ``DATABASE`` (tweepee.db). + + +Creating the initial tables +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In order to start using the models, its necessary to create the tables. This is +a one-time operation and can be done quickly using the interactive interpreter:: + + >>> from app import * + >>> create_tables() + +The ``create_tables()`` method is defined in the app module and looks like this:: + + def create_tables(): + database.connect() # <-- note the explicit call to connect() + User.create_table() + Relationship.create_table() + Message.create_table() + +Every model has a ``create_table()`` classmethod which runs a ``CREATE TABLE`` +statement in the database. Usually this is something you'll only do once, +whenever a new model is added. + +.. note:: adding fields after the table has been created will required you to + either drop the table and re-create it or manually add the columns using + ``ALTER TABLE``. + + +Connecting to the database +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You may have noticed in the above model code that there is a class defined +within each model named ``Meta`` that sets the ``database`` attribute. peewee +allows every model to specify which database it uses, defaulting to "peewee.db", +but since you probably want a bit more control, you can instantiate your own +database and point your models at it:: + + # config + DATABASE = 'tweepee.db' + + # ... more config here, omitted + + database = peewee.Database(DATABASE) # tell our models to use "tweepee.db" + +Because sqlite likes to have a separate connection per-thread, we will tell +flask that during the request/response cycle we need to create a connection to +the database. Flask provides some handy decorators to make this a snap:: + + @app.before_request + def before_request(): + g.db = database + g.db.connect() + + @app.after_request + def after_request(response): + g.db.close() + return response + +Note that we're storing the db on the magical variable ``g`` - that's a +flask-ism and can be ignored as an implementation detail. The meat of this code +is in the idea that we connect to our db every request and close that connection +every response. Django does the `exact same thing `_. + + +Doing queries +^^^^^^^^^^^^^ + +In the ``User`` model there are a few instance methods that encapsulate some +user-specific functionality, i.e. + +* ``following()``: who is this user following? +* ``followers()``: who is following this user? + +These methods are rather similar in their implementation but with one key +difference:: + + def following(self): + return User.select().join( + Relationship, on='to_user_id' + ).where(from_user=self).order_by('username') + + def followers(self): + return User.select().join( + Relationship + ).where(to_user=self).order_by('username') + +.. note: the ``following()`` method specifies an extra bit of metadata, + ``on='to_user_id'``. because there are two foreign keys to ``User``, peewee + will automatically assume the first one, which happens to be ``from_user``. + + +Specifying the foreign key manually instructs peewee to join on the ``to_user_id`` field. +the queries end up looking like:: + + # following: + SELECT t1.* + FROM user AS t1 + INNER JOIN relationship AS t2 + ON t1.id = t2.to_user_id # <-- joining on to_user_id + WHERE t2.from_user_id = ? + ORDER BY username ASC + + # followers + SELECT t1.* + FROM user AS t1 + INNER JOIN relationship AS t2 + ON t1.id = t2.from_user_id # <-- joining on from_user_id + WHERE t2.to_user_id = ? + ORDER BY username ASC + + +Creating new objects +^^^^^^^^^^^^^^^^^^^^ + +So what happens when a new user wants to join the site? Looking at the +business end of the ``join()`` view, we can that it does a quick check to see +if the username is taken, and if not executes a ``.create()``:: + + try: + user = User.get(username=request.form['username']) + flash('That username is already taken') + except StopIteration: + user = User.create( + username=request.form['username'], + password=md5(request.form['password']).hexdigest(), + email=request.form['email'], + join_date=datetime.datetime.now() + ) + +Much like the ``create()`` method, all models come with a built-in method called +``get_or_create`` which is used when one user follows another:: + + Relationship.get_or_create( + from_user=session['user'], # <-- the logged-in user + to_user=user, # <-- the user they want to follow + ) + + +Doing subqueries +^^^^^^^^^^^^^^^^ + +If you are logged-in and visit the twitter homepage, you will see tweets from +the users that you follow. In order to implement this, it is necessary to do +a subquery:: + + >>> qr = Message.select().where(user__in=some_user.following()) + >>> print qr.sql()[0] # formatting cleaned up for readability + SELECT * + FROM message + WHERE user_id IN ( + SELECT t1.id + FROM user AS t1 + INNER JOIN relationship AS t2 + ON t1.id = t2.to_user_id + WHERE t2.from_user_id = ? + ORDER BY username ASC + ) + +peewee supports doing subqueries on any ``ForeignKeyField`` or +``PrimaryKeyField``. diff --git a/docs/peewee/installation.rst b/docs/peewee/installation.rst new file mode 100644 index 000000000..0d78a0932 --- /dev/null +++ b/docs/peewee/installation.rst @@ -0,0 +1,16 @@ +Installing peewee +================= + +First you need to grab a checkout of the code. There are a couple ways:: + + pip install peewee + + +To get the latest development version:: + + git clone http://github.com/coleifer/peewee.git + + +If you grabbed the checkout, to install system-wide, use:: + + python setup.py install diff --git a/docs/peewee/models.rst b/docs/peewee/models.rst new file mode 100644 index 000000000..6a8807a7f --- /dev/null +++ b/docs/peewee/models.rst @@ -0,0 +1,187 @@ +Model API (smells like django) +============================== + +Models and their fields map directly to database tables and columns. Consider +the following:: + + class Blog(peewee.Model): + name = peewee.CharField() # <-- VARCHAR + + + class Entry(peewee.Model): + headline = peewee.CharField() + content = peewee.TextField() # <-- TEXT + pub_date = peewee.DateTimeField() # <-- DATETIME + blog = peewee.ForeignKeyField() # <-- INTEGER referencing the Blog table + + +Creating tables +--------------- + +In order to start using these models, its necessary to open a connection to the +database and create the tables first:: + + >>> import peewee + >>> peewee.database.connect() # <-- opens connection to the default db, `peewee.db` + >>> Blog.create_table() + >>> Entry.create_table() + + +Model instances +--------------- + +Assuming you've created the tables and connected to the database, you are now +free to create models and execute queries. + +Creating models from the command-line is a snap:: + + >>> blog = Blog.create(name='Funny pictures of animals blog') + >>> cat_entry = Entry.create( + ... headline='maru the kitty', + ... content='http://www.youtube.com/watch?v=xdhLQCYQ-nQ', + ... pub_date=datetime.datetime.now(), + ... blog=blog + ... ) + >>> entry.blog + <__main__.Blog object at 0x151f4d0> + >>> entry.blog.name + 'Funny pictures of animals blog' + +As you can see from above, the foreign key from ``Entry`` to ``Blog`` can be +traversed automatically. The reverse is also true:: + + >>> for entry in blog.entry_set: + ... print entry.headline + ... + maru the kitty + +Under the hood, the ``entry_set`` attribute is just a ``SelectQuery``:: + + >>> blog.entry_set + + >>> blog.entry_set.sql() + ('SELECT * FROM entry WHERE blog_id = ?', [1]) + + +Model options +------------- + +In order not to pollute the model namespace, model-specific configuration is +placed in a special class called ``Meta``:: + + import peewee + + custom_db = peewee.Database('custom.db') + + class CustomModel(peewee.Model): + ... fields ... + + class Meta: + database = custom_db + + +This instructs peewee that whenever a query is executed on CustomModel to use +the custom database. Like the default database, a connection to ``custom_db`` +must be created before any queries can be executed. + + +Model methods +------------- + +.. py:method:: save(self) + + save the given instance, creating or updating depending on whether it has a + primary key. + + example:: + + >>> some_obj.title = 'new title' # <-- does not touch the database + >>> some_obj.save() # <-- change is persisted to the db + +.. py:method:: create_table(cls) + + create the table for the given model. executes a ``CREATE TABLE IF NOT EXISTS`` + so it won't throw an error if the table already exists. + + example:: + + >>> database.connect() + >>> SomeModel.create_table() # <-- creates the table for SomeModel + +.. py:method:: drop_table(cls) + + drops the table for the given model. will fail if the table does not exist. + +.. py:method:: create(cls, **attributes) + + create an instance of ``cls`` with the given attributes set. + + :param attributes: key/value pairs of model attributes + + example:: + + >>> user = User.create(username='admin', password='test') + +.. py:method:: get(cls, **attributes) + + get an instance of ``cls`` with the given attributes set. if the instance + does not exist, a ``StopIteration`` exception will be raised. + + :param attributes: key/value pairs of model attributes + + example:: + + >>> admin_user = User.get(username='admin') + +.. py:method:: get_or_create(cls, **attributes) + + get the instance of ``cls`` with the given attributes set. if the instance + does not exist it will be created. + + :param attributes: key/value pairs of model attributes + + example:: + + >>> CachedObj.get_or_create(key=key, val=some_val) + +.. py:method:: select(cls, query=None) + + create a SelectQuery for the given ``cls`` + + example:: + + >>> User.select().where(active=True).order_by('username') + +.. py:method:: update(cls, **query) + + create an UpdateQuery for the given ``cls`` + + example:: + + >>> q = User.update(active=False).where(registration_expired=True) + >>> q.sql() + ('UPDATE user SET active=? WHERE registration_expired = ?', [0, 1]) + >>> q.execute() # <-- execute it + +.. py:method:: delete(cls, **query) + + create an DeleteQuery for the given ``cls`` + + example:: + + >>> q = User.delete().where(active=False) + >>> q.sql() + ('DELETE FROM user WHERE active = ?', [0]) + >>> q.execute() # <-- execute it + +.. py:method:: insert(cls, **query) + + create an InsertQuery for the given ``cls`` + + example:: + + >>> q = User.insert(username='admin', active=True, registration_expired=False) + >>> q.sql() + ('INSERT INTO user (username,active,registration_expired) VALUES (?,?,?)', ['admin', 1, 0]) + >>> q.execute() + 1 diff --git a/docs/peewee/querying.rst b/docs/peewee/querying.rst new file mode 100644 index 000000000..910fc9ac4 --- /dev/null +++ b/docs/peewee/querying.rst @@ -0,0 +1,432 @@ +Querying API +============ + +Constructing queries +-------------------- + +Queries in peewee are constructed one piece at a time. + +The "pieces" of a peewee query are generally representative of clauses you might +find in a SQL query. All pieces are chainable so rather complex queries are +possible. + +:: + + >>> user_q = User.select() # <-- query is not executed + >>> user_q + + >>> [u.username for u in user_q] # <-- query is evaluated here + [u'admin', u'staff', u'editor'] + + +We can build up the query by adding some clauses to it:: + + >>> user_q = user_q.where(username__in=['admin', 'editor']).order_by(('username', 'desc')) + >>> [u.username for u in user_q] # <-- query is re-evaluated here + [u'editor', u'admin'] + + +Where clause +------------ + +All queries except ``InsertQuery`` support the ``where()`` method. If you are +familiar with Django's ORM, it is analagous to the ``filter()`` method. + +:: + + >>> User.select().where(is_staff=True).sql() + ('SELECT * FROM user WHERE is_staff = ?', [1]) + + +.. note:: ``User.select()`` is equivalent to ``SelectQuery(User)``. + +The ``where()`` method acts on the ``Model`` that is the current "context". +This is either: + +* the model the query class was initialized with +* the model most recently JOINed on + +Here is an example using JOINs:: + + >>> User.select().where(is_staff=True).join(Blog).where(status=LIVE) + +This query grabs all staff users who have a blog that is "LIVE". This does the +opposite, grabs all the blogs that are live whose author is a staffer:: + + >>> Blog.select().where(status=LIVE).join(User).where(is_staff=True) + +.. note:: to ``join()`` from one model to another there must be a + ``ForeignKeyField`` linking the two. + +Another way to write the above query would be:: + + >>> Blog.select().where(status=LIVE, user__in=User.select().where(is_staff=True)) + +The above bears a little bit of explanation. First off the SQL generated will +not perform any explicit JOINs - it will rather use a subquery in the WHERE +clause:: + + # using subqueries + SELECT * FROM blog + WHERE ( + status = ? AND + user_id IN ( + SELECT t1.id FROM user AS t1 WHERE t1.is_staff = ? + ) + ) + + # using joins + SELECT t1.* FROM blog AS t1 + INNER JOIN user AS t2 + ON t1.user_id = t2.id + WHERE + t1.status = ? AND + t2.is_staff = ? + + +The other bit that's unique about the query is that it specifies "user__in". +Users familiar with Django will recognize this syntax - lookups other than "=" +are signified by a double-underscore followed by the lookup type. The following +lookup types are available in peewee: + +``__eq``: + x = y, the default + +``__lt``: + x < y + +``__lte``: + x <= y + +``__gt``: + x > y + +``__gte``: + x >= y + +``__ne``: + x != y + +``__is``: + x IS y, used for testing against NULL values + +``__contains``: + case-sensitive check for substring + +``__icontains``: + case-insensitive check for substring + +``__in``: + x IN y, where y is either a list of values or a ``SelectQuery`` + + +Performing advanced queries +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As you may have noticed, all the examples up to now have shown queries that +combine multiple clauses with "AND". Taking another page from Django's ORM, +peewee allows the creation of arbitrarily complex queries using a special +notation called **Q objects**. + +:: + + >>> sq = User.select().where(Q(is_staff=True) | Q(is_superuser=True)) + >>> print sq.sql()[0] + SELECT * FROM user WHERE (is_staff = ? OR is_superuser = ?) + + +Q objects can be combined using the bitwise "or" and "and" operators. In order +to negate a Q object, use the bitwise "invert" operator:: + + >>> staff_users = User.select().where(is_staff=True) + >>> Blog.select().where(~Q(user__in=staff_users)) + +This query generates the following SQL:: + + SELECT * FROM blog + WHERE + NOT user_id IN ( + SELECT t1.id FROM user AS t1 WHERE t1.is_staff = ? + ) + +Rather complex lookups are possible:: + + >>> sq = User.select().where( + ... (Q(is_staff=True) | Q(is_superuser=True)) & + ... (Q(join_date__gte=datetime(2009, 1, 1)) | Q(join_date__lt=datetime(2005, 1 1))) + ... ) + >>> print sq.sql()[0] # cleaned up + SELECT * FROM user + WHERE ( + (is_staff = ? OR is_superuser = ?) AND + (join_date >= ? OR join_date < ?) + ) + +This query selects all staff or super users who joined after 2009 or before +2005. + + +Query evaluation +---------------- + +In order to execute a query, it is *always* necessary to call the ``execute()`` +method. + +To get a better idea of how querying works let's look at some example queries +and their return values:: + + >>> dq = User.delete().where(active=False) # <-- returns a DeleteQuery + >>> dq + + >>> dq.execute() # <-- executes the query and returns number of rows deleted + 3 + + >>> uq = User.update(active=True).where(id__gt=3) # <-- returns an UpdateQuery + >>> uq + + >>> uq.execute() # <-- executes the query and returns number of rows updated + 2 + + >>> iq = User.insert(username='new user') # <-- returns an InsertQuery + >>> iq + + >>> iq.execute() # <-- executes query and returns the new row's PK + 3 + + >>> sq = User.select().where(active=True) # <-- returns a SelectQuery + >>> sq + + >>> qr = sq.execute() # <-- executes query and returns a QueryResultWrapper + >>> qr + + >>> [u.id for u in qr] + [1, 2, 3, 4, 7, 8] + >>> [u.id for u in qr] # <-- re-iterating over qr does not re-execute query + [1, 2, 3, 4, 7, 8] + + >>> [u.id for u in sq] # <-- as a shortcut, you can iterate directly over + >>> # a SelectQuery (which uses a QueryResultWrapper + >>> # behind-the-scenes) + [1, 2, 3, 4, 7, 8] + + +.. note:: iterating over a SelectQuery will cause it to be evaluated, but iterating + over it multiple times will not result in the query being executed again. + + +QueryResultWrapper +------------------ + +As I hope the previous bit showed, Delete, Insert and Update queries are all +pretty straightforward. Select queries are a little bit tricky in that they +return a special object called a ``QueryResultWrapper``. The sole purpose of this +class is to allow the results of a query to be iterated over efficiently. In +general it should not need to be dealt with explicitly. + +The preferred method of iterating over a result set is to iterate directly over +the ``SelectQuery``, allowing it to manage the ``QueryResultWrapper`` internally. + + +SelectQuery +----------- + +``SelectQuery`` is by far the most complex of the 4 query classes available in +peewee. It supports JOINing on other tables, aggregation via GROUP BY and HAVING +clauses, ordering via ORDER BY, and can be sliced to return only a subset of +results. All methods are chain-able. + +.. py:method:: __init__(self, model, query=None) + + if no query is provided, it will default to '*'. this parameter can be + either a dictionary or a string:: + + >>> sq = SelectQuery(Blog, {Blog: ['id', 'title']}) + >>> sq = SelectQuery(Blog, { + ... Blog: ['*'], + ... Entry: [peewee.Count('id')] + ... }).group_by('id').join(Entry) + >>> print sq.sql()[0] # formatted + SELECT t1.*, COUNT(t2.id) AS count + FROM blog AS t1 + INNER JOIN entry AS t2 + ON t1.id = t2.blog_id + GROUP BY t1.id + + >>> sq = SelectQuery(Blog, 'id, title') + >>> print sq.sql()[0] + SELECT id, title FROM blog + +.. py:method:: where(self, *args, **kwargs) + + generate a WHERE clause for the current "query context". *args is either + a list of ``Q`` or ``Node`` objects, and **kwargs is a mapping of + column + lookup to value:: + + >>> sq = SelectQuery(Blog).where(title='some title', author=some_user) + >>> sq = SelectQuery(Blog).where(Q(title='some title') | Q(title='other title')) + +.. py:method:: join(self, model, join_type=None, on=None) + + generate a JOIN clause from the current "query context" to the ``model`` passed + in, and establishes ``model`` as the new "query context". + + :param model: the model to join on. there must be a ``ForeignKeyField`` between + the current "query context" and the model passed in. + :param join_type: allows the type of JOIN used to be specified explicitly + :param on: if multiple foreign keys exist between two models, this parameter + is a string containing the name of the ForeignKeyField to join on. + + >>> sq = SelectQuery(Blog).join(Entry).where(title='Some Entry') + >>> sq = SelectQuery(User).join(Relationship, on='to_user_id').where(from_user=self) + +.. py:method:: switch(self, model) + + switches the "query context" to the given model. raises an exception if the + model has not been selected or joined on previously. + + >>> sq = SelectQuery(Blog).join(Entry).switch(Blog).where(title='Some Blog') + +.. py:method:: count(self) + + returns an integer representing the number of rows in the current query + + >>> sq = SelectQuery(Blog) + >>> sq.count() + 45 # <-- number of blogs + >>> sq.where(status=DELETED) + >>> sq.count() + 3 # <-- number of blogs that are marked as deleted + +.. py:method:: group_by(self, field_name) + + adds field_name to the GROUP BY clause where field_name is a field on the + current "query context":: + + >>> sq = Blog.select({ + ... Blog: ['*'], + ... Entry: [Count('id')] + ... }).group_by('id').join(Entry) + +.. py:method:: having(self, clause) + + adds the clause to the HAVING clause + + >>> sq = Blog.select({ + ... Blog: ['*'], + ... Entry: [Count('id', 'num_entries')] + ... }).group_by('id').join(Entry).having('num_entries > 10') + +.. py:method:: order_by(self, clause) + + adds the provided clause (a field name or alias) to the query's + ORDER BY clause. if a field name is passed in, it must be a field on the + current "query context", otherwise it is treated as an alias. peewee also + provides two convenience methods to allow ordering ascending or descending, + called ``asc()`` and ``desc()``. + + example:: + + >>> sq = Blog.select().order_by('title') + >>> sq = Blog.select({ + ... Blog: ['*'], + ... Entry: [Max('pub_date', 'max_pub')] + ... }).join(Entry).order_by(desc('max_pub')) + + check out how the query context applies to ordering:: + + >>> blog_title = Blog.select().order_by('title').join(Entry) + >>> print blog_title.sql()[0] + SELECT t1.* FROM blog AS t1 + INNER JOIN entry AS t2 + ON t1.id = t2.blog_id + ORDER BY t1.title + + >>> entry_title = Blog.select().join(Entry).order_by('title') + >>> print entry_title.sql()[0] + SELECT t1.* FROM blog AS t1 + INNER JOIN entry AS t2 + ON t1.id = t2.blog_id + ORDER BY t2.title # <-- note that it's using the title on Entry this time + +.. py:method:: paginate(self, page_num, paginate_by=20) + + applies a LIMIT and OFFSET to the query. + + >>> Blog.select().order_by('username').paginate(3, 20) # <-- get blogs 41-60 + +.. py:method:: distinct(self) + + indicates that this query should only return distinct rows. results in a + SELECT DISTINCT query. + +.. py:method:: execute(self) + + executes the query and returns a ``QueryResultWrapper`` for iterating over + the result set. the results are managed internally by the query and whenever + a clause is added that would possibly alter the result set, the query is + marked for re-execution. + +.. py:method:: __iter__(self) + + executes the query:: + + >>> for user in User.select().where(active=True): + ... print user.username + + +UpdateQuery +----------- + +``UpdateQuery`` is fairly straightforward and is used for updating rows in the +database. + +.. py:method:: __init__(self, model, **kwargs) + + creates an ``UpdateQuery`` instance for the given model. "kwargs" is a dictionary + of field: value pairs:: + + >>> uq = UpdateQuery(User, active=False).where(registration_expired=True) + >>> print uq.sql() + ('UPDATE user SET active=? WHERE registration_expired = ?', [0, 1]) + +.. py:method:: execute(self) + + performs the query, returning the number of rows that were updated + + +DeleteQuery +----------- + +``DeleteQuery`` deletes rows of the given model. It will *not* traverse +foreign keys or ensure that constraints are obeyed, so use it with care. + +.. py:method:: __init__(self, model) + + creates a ``DeleteQuery`` instance for the given model:: + + >>> dq = DeleteQuery(User).where(active=False) + >>> print dq.sql() + ('DELETE FROM user WHERE active = ?', [0]) + +.. py:method:: execute(self) + + performs the query, returning the number of rows that were deleted + + +InsertQuery +----------- + +``InsertQuery`` creates a new row for the given model. + +.. py:method:: __init__(self, model, **kwargs) + + creates an ``InsertQuery`` instance for the given model where kwargs is a + dictionary of field name to value:: + + >>> iq = InsertQuery(User, username='admin', password='test', active=True) + >>> print iq.sql() + ('INSERT INTO user (username, password, active) VALUES (?, ?, ?)', ['admin', 'test', 1]) + +.. py:method:: execute(self) + + performs the query, returning the primary key of the row that was added