Standing on the Shoulders of Giants
The Kotti Web Application Framework
- obviuosly about Kotti
-
will cover
- why Kotti came into existance in the first place
- who these giants are, on whose shoulders Kotti stands
- how Kotti is built on top of them
- and finally I'll show you some code examples, to make it easier for you to understand, what it's like to work with Kotti
This Talk
-
Why does Kotti exist?
-
Who are these giants?
-
How are they utilized by Kotti?
-
Example / Code
-
Q&A (if we have time)
Why?
- so…
- another web application framework
- really???
- aren't there already dozens, or even hundreds of them around?
- why not just pick one of them?
-
As a matter of fact all of the Kotti core developers have some common history
Yet another web framework?
Features
- full featured CMS
- lots of add ons (varying quality)
- OFS (object file system)
- security (permissions, roles, groups)
- workflows
- Full featured CMSHas lots and lots of add ons (of varying quality, but still.Uses Object File SystemA simple, yet effective way to persist objects.More or less plain Python objects are stored in an object DB.It supports a folder / file like object tree.securityHas great security features.Has the concepts of groups, users, roles and permissions which enables you to write applications with very fine grained security.also: great security track record with very few vulnerabilities compared to other systems of that kind.Workflowsare an absolute killer feature, that was unprecedented at that time.
Can be a perfect choice when it fits your needs
but
does not fit all kinds of applications.
-
if you have a classic CMS use case, or need an enterprise CMS application, it's probably a perfect choice.
- BUT
-
if you need to implement a highly customized application with some CMS like features, it can quickly become overkill and a major pain.
It often has too many features out of the box, that you just don't need.
and you constantly find yourself fighting the framework
-
This is particulary caused by the underlying software stack
- huge: consists of some 300 packages
-
uses multiple competing technologies, which
-
don't conform to "The Zen of Python"
-
to have “preferably only one obvious way to do” things
- based on the monolithic Zope 2 AND the component based Zope 3 stacks
- uses adapters, multiadapters and utilities all over the place
- long story short…
-
multiple competing technologies
-
doesn't conform to "The Zen of Python"
-
complex
fortunately there are smarter people than me, who
-
have a similar history and
-
wanted to preserve the most important features, that Zope pioneered back in the days.
-
This all with a modern, clean and maintainable code base.
-
This was the birth of Pyramid, then known as repoze.bfg
Greatest thing about Pyramid: fits even my brain
Features
-
small core
-
excellent documentation
-
pythonic
-
low level (micro framework)
It has
- small core,
- excellent documentation,
Is
- pythonic and rather
- low level, which is why you can also sort it into the group of microframeworks
And it's also… unopinionated, which means it
-
makes no assumptions about stuff like persistence and forms
-
and only basic assumptions about authentication and authorization, which you could call
-
the least common denominator for all kinds of web applications.
This all makes it a great…
- framework framework
- a framework to build your own framework, which supports your opinions
-
unopinionated
-
persistence
-
templating / forms
-
authentication / authorization sources
-
“framework framework”
Conclusion
-
only provides stuff we need
-
doesn't come with unneeded ballast
-
no need to
“waste time fighting the framework's decisions”
- perfect foundation!
so…
- Pyramid only provides what you need in every web application.
- Doesn't come with unneeded ballast.
- There's no need to waste time fighting the framework's decisions
- This makes it a perfect foundation for your own frameowrk.
…what's left to do: make some choices that pyramid didn't make for us (by intention).
Make some choices!
-
persistence
-
traversal or URL dispatch
-
templating & forms
-
authentication & authorization sources
Choices we need to make are about
-
persistence: obviosly need some means of storage for most applications
- templating, forms and user input validation:
- authentication & authorization sources
-
probably the most advanced ORM for Python
-
database agnostic
-
has many nice, useful features
- hybrid properties
- association proxies
- ordering list
-
transaction integration with pyramid
(bulletpoints)
-
Particularly useful: transactions can be bound to the lifecycle of a Pyramid request through the use of the pyramid_tm and zope.sqlalchemy packages.
Kotti implements a Node class in pure SQLAlchemy, that implements what we had with the Object Filesystem in Zope.
(bulletpoints)
The Node Class
-
adjacency list pattern
-
single root node => node tree
-
dictionary protocol
The Node Class
Dictionary Protocol
from kotti.resources import Document
from kotti.resources import get_root
root = get_root()
root['about']
<Document 2 at /about>
root['my-document'] = Document(title='My Document', description='foo',
body='<p>some HTML</p>')
implementing the dictionary protocol means that
-
you can treat any instance of the Node class as an dictionary and
-
get items by their name / key and
-
also set them like you would with a dictionary.
The Node Class
Traversal
a = root['a'] = Document(title='A', description='Document A')
b = root['a']['b'] = Document(title='B', description='Document B')
c = root['a']['b']['c'] = Document(title='C', description='Document C')
Object
URL
a
/a
b
/a/b
c
/a/b/c
This instantaneously gives us traversal support with Pyramid, as Pyramid "only" requires a __getitem__ method to be implemented, which is exactly the same what a dict does.
Beyond that we only need to have an ACL property on the node class to have persistent and inheritable ACLs.
Polimorphic queries
from kotti.resources import get_root
from kotti.resources import Node
root = get_root()
print root.children:
print(type(c))
"<class 'kotti.resources.Document'>"
"<class 'kotti.resources.File'>"
print Node.query.filter(Node.title == 'My Document').one()
"<Document 5 at /my-document>"
- In Kotti SQLAlchemy is set up to do polymorphic queries.
- Single query can return objects of multiple types.
- For example querying for Nodes will hand you back instances of Document and File if the Nodes happen to be of that types.
Joined Table Inheritance
- class hierarchy is broken up among dependent tables
- each class represented by its own table
- the respective table only includes attributes local to that class
Joined Table Inheritance is another nice feature of SQLAlchemy that we use in Kotti.
It means that…
- class hierarchy is broken up among dependent tables.
- Each class is represented by its own table.
- The respective table only includes attributes local to that class.
Events
-
before_flush
- ObjectUpdate
- ObjectInsert
- ObjectDelete
The last feature of SQLAlchemy, that I'd like to mention are Events.
-
SQLAlchemy has a lot of them, but in Kotti we use only one: before_flush
-
A flush is basically the moment when SQLAlchemy talks to the database.
-
It is NOT the same as a commit. It rather is SQLAlchemy emitting SQL and sending it to the DB, which can happen multiple times during a transaction. ((unit of work pattern))
-
Of course a commit always includes a flush.
A SQLAlchemy before flush event can trigger multiple more nuanced events in Kotti. This is a mere convenience thing to ease creation of respective event subscribers in Kotti.
Alembic
- DB migrations
- DDL (transactional, if supported by DBMS)
- DML
- environments
Not exactly part of SQLAlchemy, but closely related (and also written by Mike Bayer) is Alembic.
- DB Migration tool that supports
- transactional Data Definition Language operations (such as CREATE or ALTER TABLE) and
- transactional Data Manipulation Language operations.
Another useful feature:
- supports multiple environments.
- Comes in extremely handy for add ons in Kotti: each add on can have its own, independent migration steps.
Kotti provides a script to perform either specific migrations or all migrations for itself and all add ons at once. You can either upgrade to specific revisions or to the latest known revision.
Downgrades are also supported for the case that something goes south during a migration and your DB does not support transactional DDL.
-
has all the components for modern UIs
-
responsive
-
well known
-
easy to customize
Unless you have a UI designer on your team and can be sure that he or she will stay, you shouldn't even try to invent something yourself.
Instead: just use Bootstrap.
(bulletpoints)
Next: forms. Most form libraries handle
- creation of forms as well as their validation.
- Colander and Deform are different in that aspect.
- With Colander you can (bulletpoints).
- But it doesn't know anything about forms at all.
This is where deforms comes in…
Colander
-
define data schema
-
validate & deserialize
-
serialize Python structures to
Deform
-
render HTML forms from structures serialized by Colander
-
outputs Bootstrap 3 forms (Deform 2)
Deform…
- …only renders forms from datastructures passed by colander.
- Deform 2 uses Bootstrap 3 by default.
- In fact Deform 2 is a merge of Deform 1 and deform_bootstrap, that we developed for earlier versions of Kotti. (it was a set of bootstrap templates for deform)
- Doesn't know anything about serialization or validation
This decoupling absolutely makes sense, as you might want to
- take user input not only from forms, but as well as from say Javascript applications or XML RPC endpoints or similar.
repoze.workflow is another package from the Pylons ecosystem.
- A content workflow system that supports the concepts of states and transitions.
-
States define a role to permission mapping
-
Transitions define transitions between states, and the circumstances in which the transition may be executed
repoze.workflow allows us to implement the complete and exact feature set of the aforementioned killer feature of Zope and Plone in a very easy way.
-
a content workflow system
-
states define
-
role / permission mapping
-
transitions define
-
from_state
-
to_state
-
required permission
-
storing and serving files in web applications
-
multiple backends
- local filesystem
- S3
- GridFS
- roll your own
-
integrates with SQLAlchemy
-
files are handled like a plain model attribute
-
transaction aware
Another giant that recently made it into Kotti is Depot. It's a package for…
- storing and serving files in web applications.
- it supports: (bulletpoints)
-
transaction aware rollback or abort of a transaction causes the files to be deleted from the configured backend
- Don't get drawn away by its version (0.0.6 ATM)!
- Excellent docs, good codebase, complete test coverage.
- There's a dedicated talk on Depot tomorrow at 11:00 in the Barria2 room. Don't miss it!
Wiring it all together…
Let's finally come to Kotti itself.
As mentioned in the talk outline, it's a rather small package, that wires all those giants together in a sensible way.
- Started by Daniel Nouri in 2011.
- He did the first version in just 2 days and that version already contained most of the Node class i mentioned before.
- I learned about Kotti and joined the project a year later, in 2012 (Plone Konferenz)
- 1.0.0 in January 2015
- Biggest mistake in terms of marketing: we should've released a v1.0 in place of 0.6 or 0.7 already.
- Kotti was already absolutely stable and used in several production systems at that time, so calling it 1.0 would definitively have been justified.
- The Number of features in 0.10 alone would have justified 3 or 4 releases / versions.
- With 1.0.0 switched to semver
- major version increases with backward incompatible changes
- minor version increases with feature additions
- and patch is reserved for bugfixes
- ~9k dl/month according to PyPI
- 5 contributors with push permission
- ~40 contibutors overall.
- That makes us a still small, but active & healthy community.
- Of course contributions are always welcome, even (or especially) if it's just a missing comma or some unclear wording in the docs.
- started by Daniel Nouri in 2011
- BSD licensed
- 1.0.0 in January 2015
- current version: 1.1.4
- 9k downloads per month
- still small, but active & healthy community
- contributions are always welcome
Code Quality
a.k.a. "everyone loves badges"
We pay great attention to code quality
- extensive test suite based on py.test
- Heavy use of reusable fixtures.
- Fixtures are exposed a a pytest plugin, that you can easily use in your addons' tests.
- Travis CI
- every commit and pull request
- recently a number of tools for static source code analysis came into life
- you shouldn't blindly follow all of their suggestions / issues they report, some of them are non-issues
- BUT they can give VERY valueable insights on your code and help you improve
- Also try very hard to keep all of our requirements up to date and continue to succeed with that with one minor exception in our testing requirements. So technically the last badge is lying a bit.
almost Heisenberg quality
continuous integration (Python 2.6, 2.7, PostgreSQL, MySQL, SQLite)
static code analysis (
Codacy,
Code Climate,
QuantifiedCode)
(except 1 testing requirement)
Configuration of Kotti is done completely through a PasteScript INI file as shown here. Kotti is a plain Pyramid and therefore also WSGI application that can be run under your prefered WSGI server.
fanstatic
Fanstatic is a framework for the automatic publication of Javascript and CSS resources on a web page.
When we started using it in 2012 most JS libraries were not available through a package manager (NPM / Bower) and build tools for JS/CSS didn't exist in the way they do today.
We currently are thinking bout if it makes sense to get rid of fanstatic in Kotti v2, but we're not sure yet what would be a better story.
Configuration
[app:kotti]
use = egg:kotti
sqlalchemy.url = sqlite:///%(here)s/Kotti.db
# sqlalchemy.url = postgres://user:pass@host/db
kotti.configurators =
kotti_tinymce.kotti_configure
kotti_youraddon.kotti_configure
[filter:fanstatic]
use = egg:fanstatic#fanstatic
[pipeline:main]
pipeline =
fanstatic
kotti
[server:main]
use = egg:waitress#main
host = 127.0.0.1
port = 5000
-
Almost every aspect of Kotti can be configured with an option in the INI file.
-
Kotti provides sensible defaults for each option, so you don't have to specify any of them.
-
I'm starting to run out of time already, so I won't go into detail here.
-
The important thing to take away from this is: you can override almost everthing in Kotti, but you dont need to.
Example Options
Option
Purpose
kotti.available_types
List of active content types
kotti.configurators
List of advanced functions for config
kotti.root_factory
Override Kotti’s default Pyramid root factory
kotti.populators
List of functions to fill initial database
kotti.search_content
Override Kotti’s default search function
kotti.asset_overrides
Override Kotti’s templates
kotti.authn_policy_factory
Component used for authentication
kotti.authz_policy_factory
Component used for authorization
Example Options (continued)
Option
Purpose
kotti.caching_policy_chooser
Component for choosing the cache header policy
kotti.url_normalizer
Component used for url normalization
kotti.max_file_size
Max size for file uploads
kotti.depot.*.*
Configure the blob storage
kotti.sanitizers
Configure available sanitizers
kotti.sanitize_on_write
Configure sanitizers to be used on write access to resource objects
Let me give you just 1 example of how we combine multiple "best of breed" components to a fully functional system.
w.r.t. security we use: (bulletpoints)
-
-
ACLs are JSON serialized Pyramid ACLs
Security
-
use SQLAlchemy to…
-
store pricipals (users & groups) in the DB
-
attach (inheritable) ACLs to each node
-
use Pyramid for…
-
authentication
-
authorization
-
use repoze.workflow to…
-
recompute ACLs on workflow state changes
Example
Finally let's create a Kotti add on, to show you what it's like to work with Kotti.
- We provide a complete Kotti scaffold (based on Pyramid's pcreate command) with Kotti.
- After running that command, you have a fully functional add-on for Kotti, with a complete test suite and CI on Travis CI setup already.
The created package also contains…
- a custom content type
- a default and alternate view for that type
- completely setup i18n infrastructure
- an also completely setup alembic environment, so that you only need to write the actual migration steps (if that should become necessary in future versions of your add on)
- fanstatic resources local to this add on
The scaffold is also…
- …always kept up to date, to adhere to the most recent coding conventions as suggested by the Kotti team
- completely tested within our CI setup, so that we can be sure it is working as expected at any time.
Creating an addon
$ pcreate -s kotti kotti_myaddon
Author name [Andreas Kaiser]:
Author email [disko@binary-punks.com]:
Github username [Kotti]:
[… lot of output …]
===============================================================================
Welcome to Kotti!
Documentation: http://kotti.readthedocs.org/
Development: https://github.com/Kotti/Kotti/
Issues: https://github.com/Kotti/Kotti/issues?state=open
IRC: irc://irc.freenode.net/#kotti
Mailing List: https://groups.google.com/group/kotti
===============================================================================
The custom content type looks similar to this.
- It inherits from Kotti's Content class, which is again a sane default that you probably should inherit all your types from.
- It has an ID column, that is both the PK and a FK to the parent table's id column. This is needed for SQLAlchemy's Joined Table Inheritance.
- Then it has the attributes that are actualy added by this class in addition to the inherited ones.
- Last there's a type_info, that tells Kotti how, where and under wich conditions that Content Type should be made available through the UI.
Custom Content Type
from kotti.resources import Content
from sqlalchemy import *
class Document(Content):
id = Column(Integer(),
ForeignKey('contents.id'),
primary_key=True)
body = Column(UnicodeText())
mime_type = Column(String(30))
type_info = Content.type_info.copy(
name=u'Document',
title=_(u'Document'),
add_view=u'add_document',
addable_to=[u'Document'])
Next there's the Colander schema definition, responsible for serialization, deserialization, validation and form creation through Deform.
It by default also inherits from the parent's schema and only adds schema nodes provided by the custom content type.
Schema definition for
validation and form creation
import colander
import deform
from kotti.views.edit.content import ContentSchema
class DocumentSchema(ContentSchema):
body = colander.SchemaNode(
colander.String(),
title=_(u'Body'),
widget=deform.widget.RichTextWidget(),
missing=u"")
The add and edit forms are declared like this:
- They're, like everything in Kotti, plain Pyramid views that are configured by the usual view_config decorator.
- They're protected by some permissions, that you can – again, like everything in Kotti – adjust to fit your specific needs.
- The actual form rendering and validation is again provided by the base classes. You don't have to do anything yourself, unless you want something to be done differently.
- What you see here is a complete and working example!
Add / edit forms
from kotti.resources import Document
from kotti.views.form import AddFormView
from kotti.views.form import EditFormView
from pyramid.view import view_config
@view_config(name=Document.type_info.add_view, permission='add',
renderer='kotti:templates/edit/node.pt')
class DocumentAddForm(AddFormView):
schema_factory = DocumentSchema
add = Document
item_type = _(u"Document")
@view_config(context=Document, name='edit', permission='edit',
renderer='kotti:templates/edit/node.pt')
class DocumentEditForm(EditFormView):
schema_factory = DocumentSchema
This is what the forms from that example look like: plain Bootstrap, good looking by default, easily customizable.
This is a validation error.
- All error messages, as well as labels are fully internationalized.
-
We currently ship with translations for 8 languages. As always: contributions are very welcome.
Last but not least, here's the code for some views for our content type
- again, it's just ordinary Pyramid views, just like everything in Kotti.
- In this first view we specify a template for rendering.
- In Kotti we use Chameleon templates (as we're used to them because of our Plone / Zope history).
- If you prefer some other templating language: just use it. You can even mix arbitrary templating languages, because Pyramid supports that of course.
This does exactly the same, just in a class based approach.
- Nice thing about it: you can group multiple views that share some common code or view configuration.
- You can then add additional views with even less code or configuration.
- Of course views don't need to be rendered via templates, they can for example also provide JSON.
- Again: this all is working and complete code, not just excerpts.
View(s)
from pyramid.view import view_config
@view_config(name='view', context=Document, permission='view',
renderer='kotti:templates/view/document.pt')
def document_view(context, request):
return {}
or
from pyramid.view import view_config
from pyramid.view import view_defaults
from kotti.views import BaseView
@view_defaults(context=Document, permission='view')
class DocumentViews(BaseView):
@view_config(name='view', renderer='kotti:templates/view/document.pt')
def view(self):
return {}
@view_config(name='view2', renderer='kotti:templates/view/document2.pt')
def view(self):
return {'answer': 42}
@view_config(name='json', renderer='json')
def view(self):
return {'title': self.context.title, 'body': self.context.body, ...}
# return self.context
Finally this is the template our view uses. Nothing special again.
The templates provided by Kotti (like the tags template included in this example) can of course be individually overriden.
Template(s)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
metal:use-macro="api.macro('kotti:templates/view/master.pt')">
<article metal:fill-slot="content" class="document-view content">
<h1>${context.title}</h1>
<p class="lead">
${context.description}
</p>
<div tal:replace="api.render_template('kotti:templates/view/tags.pt')" />
<div class="body" tal:content="structure context.body | None">
</div>
</article>
</html>
This is what it looks like. And that finishes the example.
So, what are the future plans for Kotti?
-
Most important: Kotti will always stay “lean and mean in all of the right ways”
-
Nothing will go into the core unless really necessary.
-
We're even thinking of moving some code and features out of the core again and provide them as add-ons to make Kotti even leaner than it already is.
-
Examples for that would be the multifile upload, which is a nice and very user friendly feature, but that's simply not needed for most applications and brings in some JS requirements that were not needed otherwise.
Python 3 support
-
shouldn't actually be that hard.
-
All of our dependencies are Python 3 ready by now and the Kotti code already has a lot "from future imports" and stuff like that.
-
As Kotti's development is primarily need driven and apparently no one has had the urgent enough need for Python 3 support yet, it simply hasn't been done yet.
-
If someone would be interested in working on that during the conference: I'd be happy to team up on that!
The Future
-
will always stay “lean and mean in all of the right ways”
-
Python 3 support
- So: that's it.
- Try it out yourself to experience all the fun it is to develop with Kotti.
- Thank you for listening!
- Any questions?
Thank you!
Questions?
Standing on the Shoulders of Giants
The Kotti Web Application Framework