Skip to content

Welcome to Epure

New way to store data, smart query constructor, concise syntax


Documentation: https://epurelib.github.io/latest/

Source Code: https://github.com/nagvalhm/epure

PyPI: https://pypi.org/project/epure/

Official telegram channel: https://t.me/epuremedia/


Epure is python type-hints based ORM - you can store and retrieve data having no idea about database, table and columns. All technical details hidden from you. Care only about your business logic 🤑.

Key features:

  • Convenient: Many useful and intuitive tools, like Elist, Eset, JoinResource and many more
  • Fast: Create tables based on your classes in no time
  • Concise: Encourages efficient building of flexible and smart queries
  • Simple, yet powerful: Simple for novice, powerful for expert

Installing

Install and update using pip:

$ pip install -U epure

Install and update using poetry:

$ poetry add epure@latest

A Simple Epurized Class Example

Let's start with simple example for Epure, you will need your DataBase to be up and running. We will be using PostgresSQL for this example.

Type Hinting Required!

Note that you need to use type hints for class attributes in order to save class to DB. Class attribute without a type-hint will not be saved in DB.

If you dont know which type-hint you want to use, but still want to save your field: use object type-hint and your instance will be saved as JSON.

Read more about supported types for type-hinting ➡ here ⬅

Connect it

Connect Epure to your DB:

from epure import epure, escript, GresDB
from ..epure.generics import NotNull # (1)!
from typing import List, Dict

GresDb('postgres://postgres:postgres@localhost:5432',
log_level=3).connect() # (2)!
  1. Check out more about generic Epure types here: link
  2. Format of string to connect ('database://user:password@host:port'); Note that there is two ways to connect to db, read here:

GresDB and supported DB's

Read more about GresDB class and supported by Epure DataBases ➡ here ⬅

Define it

We will define four classes:

  • three of them (Publication,Reporter,Publisher) will serve as subordinate classes

  • and the one main, supreme class Article that will accumulate instances of these other classes:

# base class for Article
@epure()
class Publication:
    text_style: str = "scientific"

@epure()
class Reporter:
    full_name: str = "Victor Bennet"

class Publisher:
    publisher_name = "Future CodeWeave"

@epure() # (1)!
class Article(Publication):
    # text_style: str = "scientific" # inherits
    reporter: Reporter
    title: str
    times_published: NotNull[int] = 3
    authors: List[str] = ["Charles Dickens", "Frank Herbert"]
    publisher: object = Publisher()

    def __init__(self, reporter, title):
        self.reporter = reporter
        self.title = title

    def get_articles_using_kwargs():

        articles = self.resource.read(times_published=5) # -> [<Article object at 0x0...>, <Article object at 0x2...>]

        return articles[0]
  1. @epure is metaclass decorator function, that modifies class; more info here:

Supported by Epure type-hint types

Read more about supported types for type-hinting ➡ here ⬅

🪄 Magic 🪄 method and smart queries

Read more about 🪄 magic 🪄 methods and @escript decorator ➡ here ⬅

object type in type-hinting of a class

The Article class has a field publisher, and is initialized by instance of not epurized class, so if we dont know by which type we want to save an object like this - we can simply type hint it by object type!

Save it

Create and save instances of your class as such:

my_reporter = Reporter()

article_one = Article(my_reporter, "Why Epure is the best ORM?")
article_one.save()

article_two = Article(my_reporter, "Why Eset is so magnificent?")
article_two.save()

Retrieve it

Now when your instances saved in DB table named public.article, we can talk about creating queries variations:

1. Smart queries with use of @escript magic method 🤩

Bit of @escript, Model and Resource theory 🧐

Method itself becomes magical after we decorated it with @escript. Every "epurized"(1) class has its Model and Resource.

  1. "epurized" class - is class that was decorated by @epure() decorator

Resource is a field of class that contains all saved instances of this class.

Model is used to get data from Resource. With use of Model, we can address fields of class to construct smart queries 😄

Smart query is a predicate

Smart query is a predicate (logic expression), which is used to filter objects, hence every logic expression can be used as part of smart query, e.g. filtering by True will return all objects from Resource

We will define a 🪄 magic 🪄 method get_articles for class Article

Imagine if we want to get all stored articles with names that are defined in list of title names:

1
2
3
4
5
6
7
...
@escript # (1)!
def get_articles(self):

    model = self.md # (2)!

    title_names = ["Why Epure is the best ORM?", "Why Elist is so powerfull?", "What is magic method?"]

  1. @escript is method decorator for @epure decorated classes; more info here:
  2. note that md (short for Model) is only available in @escript decorated method scope and only; more info here:

Here you can see example for using logical expression with smart query:

    query = False

    for name in title_names:
        query = query or model.title == name

By using in we will get same result 😃:

    query = model.title in title_names # (1)!
  1. read more about supported SQL operators in Epure like in here:

Passing smart query then to .read() will retrieve Reporter object(s) with title value either: "Why Epure is the best ORM?", "Why Elist is so powerfull?" or "What is magic method?".

    articles = self.resource.read(query) # (1)!

    return articles
...

  1. learn more about read() here:

Resource implements CRUD interface, you can create (.create()), update (.update()), delete (.delete()) and read (.read()).

And calling .smart_query_example(), our 🪄 magic 🪄 method will return your retrieved object. ✨ Viola! ✨

my_articles = article_one.get_articles() # -> [<Article object at 0x0...>, <Article object at 0x2...>]

my_articles[0].reporter # -> <Reporter object at 0x0...>
my_articles[0].title # -> "Why Epure is the best ORM?"

🪄 Magic 🪄 method and smart queries

Read more about 🪄 magic 🪄 methods, smart_queries and advanced work with Models ➡ here ⬅

2. Shortcut read() with **kwargs parameters

More on shortcut .read() method

Read more about retrieving data using kwargs with .read() method ➡ here ⬅

Advanced example with smart query, for and cat aliens 👽🐈

Example with creating long smart query with for statement and other cool stuff

Check out more on such example where we create a long smart query using for statement ➡ here ⬅

Example with join using JoinResource

What is join, JoinResource and how joining models works

Check out what is JoinResource and more advanced example with join ➡: here ⬅:

Elist and Eset: easy store, easy load collections

What is Elist and Eset

Elist and Eset is super convinient when: you want to store your items in just a regular list or set...

         ...but at the same time in DB 😋

Elist

Elist - is simple tool to store small collections in single-user aplications.

You can look at Elist like generic strictly typed list with mechanism of easy saving and retrieving its contents from DB.

Elist is a strictly typed list because it can only store instances of subscripted (defined) for Elist type (e.g Elist[str] can't store int value)

Elist is not multiuser-friendly

Elist does not guarantee numerated order of big collections in multi-user aplications.

In cases of big collections with multiple users use Eset!

Let's look at an example:

Elist example for storing and retrieving items

We will create House and District classes. Both House and District will store a Elist.

from epure import epure, Elist

@epure()
class House:
    tenant_names:Elist[str]
    house_number:int
    street_name:str

    def __init__(self, house_number, street_name, tenant_names):
        self.house_number = house_number
        self.street_name = street_name
        self.tenant_names = tenant_names

@epure()
class District:
    houses_list:Elist[House]
After we will create two instances of House, add them to houses_list (Elist) of District obj and save all them.

Saving object with Elist or Eset field

Note that saving object with Elist field will triger saving for all Elist or Eset fields of this object respectivly.

house_one = House(42, "Crow Str.", Elist[str](["Mary", "Charles"]))

house_two = House(14, "Babbidge Str.", Elist[str](["Daniel"]))

district = District()
district.houses_list = Elist[House]([house_one, house_two])

district.save() # (1)!
  1. Saving this Epure instance with Elist field will triger saving for Elist

Then using .read() we will get our District object from DB (district_retrived) using district.data_id (unique UUID id). House objects will be already present in house_list Elist

district_retrived = District.resource.read(data_id = district.data_id)

district_retrived[0].houses_list[0] # -> [<House object at 0x0...>]
district_retrived[0].houses_list[0].tenant_names[0] # -> "Mary"
Working with Elist collection is really easy as you can see in this example

How to store Elist outside of class, more examples and etc.

How to store Elist outside class field, examples on Elist ➡ here ⬅

Eset

Eset is similar to Elist, though (as much as set differes from list) Eset has no order of its stored contents.

Because of the way Eset is built, it is really convenient when working with big chunks of data! 🤤

You dont need to load whole Eset at a time: when Eset is retrived from DB - it will be empty by default and is easily loaded by .load() method.

(In future Eset will support partial loading, based on specified properties of objects)

In a way, Eset is Epure's interpretation of Many2Many field.

Eset with JSON example for storing, loading and retrieving items

Let's declare Customer and ShipmentCompany.

Because ShipmentCompany.customers_set Elist type is object - instances of Customer will be stored as JSON

from epure import epure, Eset

class Customer:
    name:str
    adress:str

    def __init__(self, name, adress):
        self.name = name
        self.adress = adress

@epure()
class ShipmentCompany:
    customers_set:Eset[object]
    offices_names:Eset[str] = Eset[str](["Charlie", "Bravo", "Alpha"])

Eset allows to store copious amounts of data, so lets load it up!

Storing loads of instances in big_customers_set
customer_1 = Customer("Sion Mccall", "Heathfield Road, 30")
customer_2 = Customer("Darcy Montes", "Ash Street, 13")
customer_3 = Customer("Isaiah Hughes", "Grasmere Avenue, 19")
customer_4 = Customer("Jodie Sandoval", " Meadow Rise, 15")
...
...
...
customer_50 = Customer("Emily Mahoney", "Park Road, 62")
customer_51 = Customer("Inaaya Hodge", "Rectory Lane, 42")
customer_52 = Customer("Blanche Colon", "Ferndale Road, 10")
customer_54 = Customer("Lucia Davenport", "Warwick Street, 56")
customer_55 = Customer("Vivian Lin", "Carlton Road, 23")
customer_56 = Customer("Kane Flores", "Beaufort Road, 87")

big_customers_set = Eset[object]([customer_1, ..., customer_56])

We will create our shipment_company and assign big_customers_set to its customers_set, then we will save it

shipment_company = ShipmentCompany()

shipment_company.customers_set = big_customers_set

shipment_company.save() # (1)!
Retriving is easily done with read(), and as you can see you need to load() Eset for it to be sub-loaded.

retrieved_shipment_company = EsetExample.resource.read(data_id = eset_ex.data_id)[0]

empty_eset = retrieved_shipment_company.customers_set # -> {}
loaded_set = empty_eset.load() # (2)!
loaded_set # -> {<Customer object at 0x0...>, <Customer object at 0x0...>, ...}

empty_set = retrieved_shipment_company.offices_names # -> {}
loaded_set = empty_set.load() # (2)!
" ".join(loaded_set) # -> "Alpha Charlie Bravo"
  1. Note that saving this will triger recursive saving for all elists and esets bounded to this object
  2. Eset is empty when is retrieved, you need to use .load() method of Eset to fill the eset with its content.

Read more on Eset

More info and examples on Eset ➡ here ⬅

JSON and dict serialization and deserialization of Epure objects

Epure allows to serialize your "epurized" class object to JSON using .to_dict() and .to_json() methods

As much as it allows to deserialize JSON back to a Epure object using .from_dict() and .from_json()

More on JSON and dict serialization and deserialization

  • For serialization with .to_dict() and .to_json() example head down ➡ here ⬅

  • And for deserialization using .from_dict() and .from_json() check out example ➡ here ⬅

Ini File Parser

This section of library appeared mainly because there is no adequate solution for working with ini files in python

File Ini Parser allows to easily work with sections of .ini file using dot (.) notation

Using Ini File Parser

Save this as example.ini file:

example.ini
db_host = localhost

[general]
db_port = 5432

[epure.best.app.forever]
friend = true

Now we can easily access fields of this ini file:

from epure.files import IniFile

config = IniFile('./example.ini')

config.db_host # -> "localhost"

config.general.db_port # -> 5432

config.epure.best.app.forever.friend # -> True

Learn more

Learn more about Ini Parser ➡ here ⬅

Developers

Nikita Umarov (Pichugin), Pavel Pichugin

  • Documentation: https://github.com/nagvalhm/epure/blob/main/README.md
  • Changes: https://github.com/nagvalhm/epure
  • PyPI Releases: https://pypi.org/project/epure/
  • Source Code: https://github.com/nagvalhm/epure
  • Issue Tracker: https://github.com/nagvalhm/epure/issues
  • Website: https://pypi.org/project/epure/