pfunk

Pfunk logo

A Python library to make writing applications with FaunaDB easier. Includes GraphQL and generic ABAC auth workflow integrations.

Key Features

  • DRY (Don't Repeat Yourself) code to help you create multiple collections, indexes, roles, and user-defined functions quickly. This helps you create more functionality with less code.
  • Mix and match authorization workflows (Group and user based)
  • Group level permissions
  • Create a GraphQL endpoint and a schema validating ORM with the same code.
  • Authentication collections, indexes, user-defined functions, and roles included.
  • Generic CRUD user-defined functions included
  • Create a REST API to integrate Fauna and Non-Fauna related tasks
  • Schema validation based on the Valley library.

Full Documentation

Documentation

Table of Contents

Getting Started

Installation

pip install pfunk

Setup the Connection

Using Environment Variables (Preferred Method)

If you can easily set environment variables just set the FAUNA_SECRET environment variable to your key.

Define your Collections (collections.py)

For an example project let's create a project management app.

from pfunk import (Collection, StringField, ReferenceField, DateField,
                   Project, Enum, EnumField, IntegerField, FloatField,
                  SlugField)
from pfunk.contrib.auth.collections import User, Group
from pfunk.contrib.auth.resources import GenericGroupBasedRole, GenericUserBasedRole



STATUS = Enum(name='ProductStatus', choices=['unstarted', 'started', 'completed', 'delivered'])


class AbstractGroupCollection(Collection):
    """
    Abstract Collection very similar to abstract models in Django. 
    The main difference is you don't have to add abstract=True in the Meta class.
    """
    # Here we are defining the roles that should be 
    # created for this collection (by proxy this role will be applied 
    # to its subclasses. We could however change the list of roles below.
    collection_roles = [GenericGroupBasedRole]
    title = StringField(required=True)
    group = ReferenceField(Group, required=True)

    def __unicode__(self):
        return self.title


class Product(AbstractGroupCollection):
    slug = SlugField(required=True)
    description = StringField()
    due_date = DateField(required=False)

    class Meta:
        """
        Add unique together index to ensure that the same slug is not used 
        for more than one product that belongs to a group. Similar to the
        Github's repo naming convention ex. https://github.com/capless/pfunk. 
        In that example 'capless' is the group slug and 'pfunk' is the 
        product's slug
        """
        unique_together = ('slug', 'group')


class Sprint(AbstractGroupCollection):
    product = ReferenceField(Product, required=True)
    start_date = DateField(required=True)
    end_date = DateField(required=True)


class Story(AbstractGroupCollection):
    description = StringField()
    points = IntegerField()
    product = ReferenceField(Product, required=True)
    sprint = ReferenceField(Sprint, required=False)
    status = EnumField(STATUS)


# Create a Project
project = Project()

# Add Collections to the Project
project.add_resources([User, Group, Product, Story, Sprint])

Auth Workflows

In the example above, the collection_roles list on the Collection classes attaches generic roles to the collection. Currently, you can choose a group based workflow, user based, or a mix of the two.

User Based Role

This role allows you to create, read, write, and delete documents that you haveyou are the owner of. The user field determines if you are the owner.

Group Based Role

This role allows you to create, read, write, and delete documents if both of the following conditions are true:

  1. You belong to the group that owns the document
  2. You have the create permission to perform the action (create, read, write, and delete)

Publish

project.publish()

Create Group and User

Both of the code samples below will create a new user, group, and users_groups (many-to-many relationship through collection) record.

from pfunk.contrib.auth.collections import Group, User
group = Group.create(name='base', slug='base')

# Example 2: You could also create a user with the following code
user = User.create(username='notthegoat', first_name='Lebron', last_name='James',
                   email='notthegoat@gmail.com', account_status='ACTIVE', groups=[group],
                   _credentials='hewillneverbethegoat')

Give User New Permissions

# User instance from above (username: notthegoat)
user.add_permissions(group, ['create', 'read', 'write', 'delete'])

Login

from pfunk.contrib.auth.collections import User

#The login classmethod returns the authenticated token if the credentials are correct.
token = User.login('goat23', 'cruelpass')

Save Some Data

Let's use the Video collection, and the authenticated token we created above to save some data.

# Use the token from above so the current users permissions are used.


product = Product.create(
    title='PFunk Project',
    slug='pfunk',
    description='Some example project for test Pfunk',
    group=group,
    _token=token #The token gained in the previous step. If you do not specify a token here it defaults to the key used in the FAUNA_SECRET env.
)

Query your Data

Let's get the video you just created.

product = Product.get('the-key-for-the-previous-video', _token=token)

Let's query for all videos using the server key.

products = Products.all()

Let's query for all videos that the logged in user has access to.

products = Products.all(_token=token)

Delete a Record

Let's delete the record from above.

product.delete()

Tutorial

In this tutorial we'll show you how to create a simple PFunk project. Let's call our project SchoolZone, a demo app for tracking students and teachers in a school.

Step 1: Create New Project

Let's use the PFunk CLI to create a new project.

pfunk init schoolzone

Step 2: Create Collections

Let's create some collections to store our data in. If you're new to Fauna, collections are the equivalent to tables in relational database.

from pfunk.collection import Collection
from pfunk.fields import StringField, IntegerField, ReferenceField, ManyToManyField
from pfunk.contrib.auth.collections import User, Group
from pfunk.contrib.auth.resources import GenericGroupBasedRole


class School(Collection):
    collection_roles = [GenericGroupBasedRole]
    name = StringField(required=True)
    address = StringField(required=False)
    group = ReferenceField(Group, required=True)


class Teacher(Collection):
    collection_roles = [GenericGroupBasedRole]
    first_name = StringField(required=True)
    last_name = StringField(required=True)
    group = ReferenceField(Group, required=True)


class Class(Collection):
    collection_roles = [GenericGroupBasedRole]
    name = StringField(required=True)
    teacher = ReferenceField(Teacher)
    group = ReferenceField(Group, required=True)


class Student(Collection):
    collection_roles = [GenericGroupBasedRole]
    first_name = StringField(required=True)
    last_name = StringField(required=True)
    grade = IntegerField(required=True)
    group = ReferenceField(Group, required=True)


class StudentClass(Collection):
    collection_roles = [GenericGroupBasedRole]
    class_ref = ReferenceField(Class)
    student_ref = ReferenceField(Student)
    final_grade = IntegerField(required=False)
    absences = IntegerField()
    tardies = IntegerField()
    group = ReferenceField(Group, required=True)

Step 3: Add Collections to Project

Open schoolzone/project.py and add the collections.

from pfunk import Project
from pfunk.contrib.auth.collections import Key
from .collections import School, Teacher, Class, Student, StudentClass, User, Group


project = Project(_collections=[User, Group, School, Teacher, Class, Student, StudentClass])

Step 4: Seed Keys

PFunk uses encrypted JWTs with private keys that unencrypt the JWT during request/response loop. We need to seed the keys.

pfunk seed_keys staging

Step 5: Run the Local Server

Next, we run the local server to test things out.

pfunk local

Step 6: Deploy

Publish the GraphQL schema to Fauna and deploy the API to Lambda and API Gateway.

pfunk deploy staging

If you don't want to deploy the API and just want to publish the GraphQL schema to Fauna.

pfunk publish staging

Command Line Interface (CLI)

Create Project

pfunk init <project_name>

Add Stage

pfunk add_stage <stage_name>

Publish

pfunk publish <stage_name>

Create Admin User

pfunk create_admin_user <stage_name>

Seed Keys

pfunk seed_keys <stage_name>

Deploy

pfunk deploy <stage_name>

Run Server

pfunk local 

Contribute to PFunk

Install Docker and Docker Compose

Setup Local Environment

Example Env file

Please create a .env file and copy the following to variables to it. You will need to supply values for the following variables.

  • FAUNA_SECRET - This should be generated by using the iPython notebook instructions below.
  • DEFAULT_FROM_EMAIL
  • PROJECT_NAME
  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
FAUNA_SECRET=your-locally-generated-fauna-secret-key
FAUNA_SCHEME=http
FAUNA_DOMAIN=fauna
FAUNA_PORT=8443
FAUNA_GRAPHQL_IMPORT_URL=http://fauna:8084/import
FAUNA_GRAPHQL_URL=http://fauna:8084/graphql
DEFAULT_FROM_EMAIL=youremail@yourdomain.com
PROJECT_NAME=TestProject
TEMPLATE_ROOT_DIR=/code/pfunk/carcamp/templates
AWS_ACCESS_KEY_ID=YOUR-AWS-ACCESS-KEY
AWS_SECRET_ACCESS_KEY=YOUR-AWS-SECRET-ACCESS-KEY

Run Docker Compose

Run the command below and copy and paste URL that is generated. Change the port from 8888 to 8010.

docker-compose up

Get Local Fauna Key

Copy fauna.ipynb.tmpl to fauna.ipynb and run the commands. Copy the generated key from the Create Key to the FAUNA_SECRET variable in your .env file above.

Run Unit Tests

docker-compose run web poetry run python -m unittest

Pull Requests

Please open PRs that will merge to the develop branch.

View Source
"""
.. include:: ../README.md
.. include:: ../TUTORIAL.md
.. include:: ../CLI.md
.. include:: ../CONTRIBUTE.md
"""
__docformat__ = "google"
from .collection import Collection, Enum
from .fields import (StringField, IntegerField, DateField, DateTimeField, BooleanField, FloatField, EmailField,
                     EnumField, ReferenceField, ManyToManyField, SlugField)
from .project import Project
from .client import FaunaClient