Skip to content

Use Postgresql/Sqlite as if it were a Python Dictionary

License

Notifications You must be signed in to change notification settings

rolobio/DictORM

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

514 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DictORM

Codacy Badge Build Status Coverage Status PyPi Version PyPI Project Status

Manipulate Postgresql/Sqlite using Python Dictionaries

What if you could insert a Python dictionary into the database? DictORM allows you to select/insert/update/delete rows of a database as if they were Python Dictionaries.

Installation

Install dictorm using pip, with the default sqlite backend:

pip install dictorm

Install with the Postgres backend:

pip install dictorm[Postgresql]

Quick & Simple Example!

# Create a dictionary that contains all tables in the database
>>> from dictorm import DictDB
>>> db = DictDB(db_conn)
# Get the Table object that was automatically found by DictDB
>>> Person = db['person']

# Define Will's initial column values
>>> will = Person(name='Will')
>>> will
{'name':'Will',}

# Insert Will
>>> will.flush()
>>> will
{'name':'Will', 'id':1}

# Change Will however you want
>>> will['name'] = 'Steve'
>>> will
{'name':'Steve', 'id':1}
# Send the changes to the database, all columns will be overwritten to what this
# "dictionary" now contains.
>>> will.flush()

# DictORM will NEVER commit or rollback changes, that is up to you.
# Make sure to commit your changes:
db_conn.commit()

References will be represented as a sub-dictionary

# Define a relationship to another table, access that one-to-one relationship
# as if it were a sub-dictionary.
>>> Car = db['car']
>>> Person['car'] = Person['car_id'] == Car['id']
# 'car'            : the key of the sub-dictionary you are defining
# Person['car_id'] : the column of the "person" table that references car.id
# Car['id']        : the foreign key of the "car" table, referenced by person.car_id

# When defining a reference, it is important to order the columns correctly, the
# foreign-key/foreign-table should be on the right:
# Person['car'] = Person['car_id'] == Car['id']            # Correct
# Person['car'] = Car['id'] == Person['car_id']            # Incorrect
# Person['manager'] = Person['manager_id'] == Person['id'] # Correct
# Person['manager'] = Person['id'] == Person['manager_id'] # Incorrect

>>> wills_car = Car(name='Dodge Stratus', plate='123ABC')
>>> wills_car.flush()
>>> wills_car
{'id':1, 'name':'Dodge Stratus', 'plate':'123ABC'}

>>> will['car_id'] = wills_car['id']
# Update the database row by updating the "will" object with his new car. Flush.
>>> will.flush()
>>> will
{'name':'Will', 'id':1, 'car_id':1, 'car':{'id':1, 'name':'Dodge Stratus', 'plate':'123ABC'}}
>>> will['car'] == wills_car
True

# I did not show 'car_id' in the first Will examples, this was to avoid
# confusion.  You must define 'car_id' in the database before it can be
# accessed by DictORM.

Detailed Basic Usage

Create your tables with at least one primary key:

CREATE TABLE person (
    id SERIAL PRIMARY KEY,
    name TEXT,
    car_id INTEGER REFERENCES car(id),
    manager_id INTEGER REFERENCES person(id)
);
CREATE TABLE car (
    id SERIAL PRIMARY KEY,
    license TEXT
);

Connect to the database using psycopg2

>>> import psycopg2
>>> conn = psycopg2.connect(**db_login)

or Sqlite3

>>> import sqlite3
>>> conn = sqlite3.connect(':memory:')

Finally, use DictORM:

# DictDB queries the database for all tables and allows them to be gotten as
# if DictDB was a dictionary.
>>> db = DictDB(conn)

# Get a Table object for table 'person'
# person table built using: (id SERIAL PRIMARY KEY, name TEXT)
>>> Person = db['person']

# DictORM relies on primary keys to successfully Update a row in the 'person'
# table.  The primary keys found are listed when the Person object is printed.
>>> Person
Table(dave, ['id',])

# You can define your own primary keys
Person.pks = ['id',]

# Insert into "person" table by calling "Person" object as if it were a
# dictionary.
>>> dave = Person(name='Dave').flush()
>>> dave
{'name':'Dave', 'id':1}

# dave behaves just like a dictionary
>>> dave['name']
Dave
>>> dave['id']
1

# Change any value
>>> dave['name'] = 'Bob'
# Send the changes to the database
>>> dave.flush()
# Commit any changes is up to you.
>>> conn.commit()

Get a row from the database as a Dict

# Get a row from the database, you may specify which columns must contain what
# value.
>>> bob = Person.get_one(id=1)
# Or, if the table has primary key(s), you may forgo specifying a column name.
# PyPyTable.get_one will pair the arguments you provide with the primary keys in
# their respective orders:
>>> bob = Person.get_one(1)
>>> bob
{'name':'Bob', 'id':1}
# Get all rows in a table.
>>> list(Person.get_where())
[{'name':'Bob', 'id':1},]
# get_where returns a ResultsGenerator, which behaves just like a python
# generator.  It will not retreive a result from the database until you request
# it.
>>> Person.get_where()
ResultsGenerator()
>>> for person in Person.get_where():
>>>     person
{'name':'Bob', 'id':1}

DictORM's Features

Update a Dict without overwriting Primary Keys

# A Dict behaves like a Python dictionary and can be updated/set.  Update bob
# dict with steve dict, but don't overwrite bob's primary keys.
>>> steve = Person(name='Steve').flush()
>>> steve
{'name':'Steve', 'id':2}
>>> steve.remove_pks()
{'name':'Steve'}
>>> bob.update(steve.remove_pks())
>>> bob.flush()
# Bob is a copy of steve, except for bob's primary key
>>> bob
{'name':'Steve', 'id':1}

Set a one-to-one reference to another table

# person              | car
# --------------------+-------
# car_id -----------> | id
>>> Car = db['car']
>>> Person['car'] = Person['car_id'] == Car['id']
# Give Steve a car
>>> steve = Person.get_one(1)
>>> steves_car = Car().flush()
>>> steve['car_id'] = steves_car['id']
>>> steve.flush()
>>> steve['car'] == steves_car
True

Reference a person's manager, and a manager's subordinates

# person             | person
# -------------------+-----------
# id --------------> | manager_id
>>> Person['manager'] = Person['id'] == Person['manager_id']
>>> steve = Person.get_one(1)
>>> bob = Person(name='Bob', manager_id=steve['id']).flush()
>>> aly = Person(name='Aly', manager_id=steve['id']).flush()
>>> bob['manager'] == steve
True
>>> aly['manager'] == steve
True

# Define that "subordinates" contains many rows from the Person table
>>> Person['subordinates'] = Person['id'].many(Person['manager_id'])
>>> list(steve['subordinates'])
[bob, aly]

Add in more tables

CREATE TABLE department (
    id SERIAL PRIMARY KEY,
    name TEXT
);
CREATE TABLE person_department (
    person_id INTEGER REFERENCES person(id),
    department_id INTEGER REFERENCES department(id),
    PRIMARY KEY (person_id, department)
);

Set a many-to-many reference to another table using an intermediary table

# person              | person_department            | department
# --------------------+------------------------------+-------------------
# id <-------+-+----- | person_id   department_id -> | id
#             \ \---- | person_id   department_id -> | id
#              \----- | person_id   department_id -> | id
>>> Department = db['department']
>>> PD = db['person_department']
>>> PD['department'] = PD['department_id'] == Department['id']
>>> PD['person'] = PD['person_id'] == Person['id']

# Reference many rows using .many:
>>> Person['person_departments'] = Person['id'].many(PD['person_id'])

# Create HR and Sales departments
>>> hr = Department(name='HR').flush()
>>> hr
{'name':'HR', 'id':1}
>>> sales = Department(name='Sales').flush()

# Add PD rows for Steve for both departments
>>> PD(person_id=steve['id'], department_id=hr['id']).flush()
>>> PD(person_id=steve['id'], department_id=sales['id']).flush()

>>> steve['person_departments']
[{'department': hr, 'department_id': 1, 'person_id': 1},
 {'department': sales, 'department_id': 2, 'person_id': 1}]

# Iterate through Steve's departments
>>> for pd in steve['person_departments']:
>>>    pd['department']
{'name':'HR', 'id':1}
{'name':'Sales', 'id':2}

# Get all persons who are in sales:
>>> PD(person_id=aly['id'], department_id=sales['id']).flush()
>>> PD(person_id=bob['id'], department_id=sales['id']).flush()
>>> for pd in PD.get_where(department_id=sales['id']):
>>>     pd['person']
steve
aly
bob

Substratum

# Having to remember to iterate through steve['person_departments'] and then
# access ['department'] is a little cumbersome, why not skip over the join-table
# (person_departments) and go straight to the referenced department?
>>> Person['departments'] = Person['person_departments'].substratum('department')
# Person['person_departments'] must be created first (it was created in the
# previous example), then you can substratum a reference on it.

>>> steve['departments']
[{'name':'HR', 'id':1},
 {'name':'Sales', 'id':2},]

# Much easier and intuitive!
>>> for dept in steve['departments']:
>>>    dept
{'name':'HR', 'id':1}
{'name':'Sales', 'id':2}

Aggregate

# aggregate is a short-hand for an aggregate substratum.
# When a substratum returns a list of lists, aggregate will return a
# single list:
# i.e. [[a,b], [c,d]]  becomes  [a,b,c,d]

# Lets promote Bob to Steve and Alice's manager
steve['manager_id'] = bob['id']
alice['manager_id'] = bob['id']
bob['manager_id'] = None

# Bob is now the manager with two subordinates
>>> bob['subordinates']
[steve, alice]

# The substratum method (remember that "subordinates" returns "many")
>>> Person['subordinates_departments'] = Person['subordinates'].substratum('deparments')
>>> bob['subrodinates_departments']
[
  [hr, sales] # First, Steve's departments
  [sales]     # Last, Alice's departments
]

# The aggregate method
>>> Person['subordinates_departments'] = Person['subordinates'].aggregate('deparments')
>>> bob['subrodinates_departments']
[hr, sales, sales]

Reuse a ResultsGenerator

# get_where and get_one return a ResultsGenerator, which does nothing until you
# attempt to get a result from it.  This means we can reuse a ResultsGenerator
# to refine the results.
>>> minions = bob['subordinates']
>>> minions
[steve, aly]

# Limit the results to only one row
>>> list(minions.limit(1))
[steve,]

# Reverse the order, limit to one row
>>> list(minions.order_by('id ASC').limit(1))
[aly,]

Check if a Dict belongs to a Table

>>> bob = Person(name='bob').flush()
>>> bob.table
Person
>>> bob in Person
True
>>> bob in Car
False

Advanced query'ing

# DictORM supports many simple expressions.  It is by no means exhaustive, but
# it supports the basic features.

# Inserting Frank for these examples
>>> frank = Person(name='Frank', manager_id=bob['id']).flush()
>>> frank
{'name':'Frank', 'id':4, 'manager_id':1, 'manager':bob}

# Search using pythonic expressions
>>> list(Person.get_where(Person['name'] == 'Steve'))
[steve,]
>>> Person.get_where(Person['id'] > 1)
[steve, aly, frank]

# Custom results ordering (reverse the previous results)
>>> Person.get_where(Person['id'] > 1).order_by('id DESC')
[frank, aly, steve]

# Limit the results
>>> Person.get_where(Person['id'] > 1).order_by('id DESC').limit(1)
[frank,]

# Offset the results
>>> Person.get_where(Person['id'] > 1).order_by('id DESC').limit(1).offset(1)
[aly,]

# Refine a subordinate search
>>> bob['subordinates'].refine(Person['name']=='Aly')
[aly,]

# Subordinate that has a car
>>> bob['subordinates'].refine(Person['car_id']>0)
[steve,]
>>> bob['subordinates'].refine(Person['car_id'].IsNot(None))
[steve,]

Pythonic Comparisons create SQL Comparisons

>>> Person['foo'] == 'bar'
"foo" = 'bar'
>>> Person['foo'] > 'bar'
"foo" > 'bar'
>>> Person['foo'] >= 'bar'
"foo" >= 'bar'
>>> Person['foo'] < 'bar'
"foo" < 'bar'
>>> Person['foo'] <= 'bar'
"foo" <= 'bar'
>>> Person['foo'] != 'bar'
"foo" <= 'bar'
>>> Person['foo'].Is('bar')
"foo" IS 'bar'
>>> Person['foo'].IsNot('bar')
"foo" IS NOT 'bar'
>>> Person['foo'].IsDistinct('bar')
"foo" IS DISTINCT FROM 'bar'
>>> Person['foo'].IsNotDistinct('bar')
"foo" IS NOT DISTINCT FROM 'bar'
>>> Person['foo'].IsNull()
"foo" IS NULL
>>> Person['foo'].IsNotNull()
"foo" IS NOT NULL
>>> Person['foo'].Like('bar')
"foo" LIKE 'bar'

# The following comparisons not supported for Sqlite3
>>> Person['foo'].In(('bar', 'baz'))
"foo" IN ('bar', 'baz')
>>> Person['foo'].Ilike('bar')
"foo" ILIKE 'bar'
>>> Person['foo'].Any(['foo', 'bar'])
"foo" = ANY ('foo', 'bar')

Operators

>>> from dictorm import And, Or

>>> And(Person['foo'] == 'bar', Person['id'] > 3)
"foo" == 'bar' AND "id" > 3

>>> Person.get_where(And(Person['foo'] == 'bar', Person['id'] > 3))
ResultsGenerator()

>>> Or(Person['foo'] == 'bar', Person['id'] > 3)
"foo" == 'bar' Or "id" > 3

# Nested
>>> And(Person['id'] > 3, Or(Person['name'] == 'Bob', Person['name'] == 'Dave',
>>>     Person['name'] == 'Alice'))
"id" > 3 AND ("name" == 'Bob' OR "name" == 'Dave' OR "name" == 'Alice')

Delete

# Delete a single row
>>> frank.delete()

Transaction

DictORM provides a minimal transaction context manager on it's db object:

>>> with db.transaction():
        bob = Person(name='Bob').flush()

Raw queries

You can execute a raw query on a Table. The resulting rows will be converted to Dict's for that table. In this example, we get all persons whose ID is 1, 2, 3 or 4. This returns a ResultsGenerator that creates Dicts from the person table.

>>> persons = Person.get_raw('SELECT * FROM person WHERE id IN (1,2,3,4)')

Testing

# Get postgres container
sudo docker run --name dictorm -e POSTGRES_PASSWORD=dictorm -p 54321:5432 -d postgres:12
# Install test requirements & run:
pip install -e .[testing]
python setup.py test

About

Use Postgresql/Sqlite as if it were a Python Dictionary

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages