pfunk
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
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:
- You belong to the group that owns the document
- You have the create permission to perform the action (
create
,read
,write
, anddelete
)
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
- Follow the instructions here to install Docker
- Follow the instructions here to install 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