Skip to main content

Strict Dict (json) with schema validation embedded

Project description

stricto

release

pylint test

Strict json structure with schema validation

The way to use is very simple, see Quickstart for a basic setup.

The main difference with jsonschema is that the schema is directly in types of data. You don't have to validate them.

Installation

pip install stricto

Quickstart

from stricto import Dict, Int, String, List

a=Dict({
    "name" : String(),
    "address" : Dict({
        "num" : Int(),
        "street" : String()
    }),
    "nicknames" : List( String() )
})


a.set({ 
    "name" : "Edward",
    "address" : {
        "num" : 22, 
        "street" : "acacia avenue"
    },
    "nicknames" : [ "Ed" ]
})


print(a.address.num) # 22
print(a.address) # { "num" : 22, "street" : "acacia avenue" }

a.name = 666 # -> raise a typeError (must be a string)

print (a) # { "name" : "Edward", ... }

a.nicknames.append(666) # -> raise a typeError (must be a string)
a.nicknames.append("Eddy")
a.nickname[1] # -> Eddy

b=a # b is a reference on a
c=a.copy() # c is a different object : c is a copy

c == b # return True (you can test and do operators directly on objects)
b.nicknames.pop()
c == b # return False

Basic types

All basic class from python are implemented in stricto.

python class type in stricto
bool Bool()
int Int()
float Float()
string String()
list List()
dict Dict()
tuple Tuple()
bytes Bytes()
In()
# example
from stricto import Dict, Int

a = Int()
a.set(22) # -> ok
a.set(23.1) # raise an error
a.set("the number of the beast") # raise an error

# WARNING
a = "the number of the beast" # works ! the affectation of "a" change. Now it is a string. This is python.

# Inside a Dict().
test=Dict({
    "a" : Int()
})

test.a = 22 # -> ok
test.a = 23.1 # raise an error
test.a = "the number of the beast" # raise an error

json

use .get_value() to extract a dict from a Dict and do the json.dumps like usual.

# example
from stricto import Int, List, String, Dict, Error, StrictoEncoder
import json

model={
    "b" : Int(),
    "e" : List( String())
}
a=Dict(model)
b=Dict(model)
a.set({ "b" : 1, "e" : [ "aa", "bb"]})

sa = json.dumps(a, cls=StrictoEncoder) # json dumps. Need to user StrictoEncoder for specific types (see extend)
b.set( json.loads(sa) ) 
b == a # return True

selectors

You can use json selectors to find the object according to rfc9535

from stricto import Int, List, String, Dict, Error

a = Dict(
    {
        "a": Int(default=1),
        "b": Dict({
            "l" : List( Dict({
                "i" : String()
            }) )
        }),
        "c": Tuple( (Int(), String()) )
    }
)
a.set({ "a" : 12, "b" : { "l" : [ { "i" : "fir"}, { "i" : "sec"}, ] }, "c" : ( 22, "h") })

a.select('$.a') # 12

# To make the difference :

a.select('$.f.d') # None
a.f.d # -> raise an error

a.select("$.b.l[0].i") # "fir"
a.select("$.*.l.i") # ["fir", "sec"]

Types and options

All types

available options for all types ares :

Option Default Description
notNone=True|False False cannot be None
required=True|False False similar to notNone
description="whatever you want" None a description of this object
default=666 None the default value
in=[ 1, 2, 3, 5 ]|func None the value must be one of those elements
union=[ 1, 2, 3, 5 ]|func None similar to in
transform=func None a function to transform the value before setting it
constraint=func None a constraints to check the value before setting it
constraints=[func] None a list of constraints to check the value before setting it
onchange=func None a onchange function trigged when the value change
onChange=func None similar to onchange
set=func None a read only value, calculated from other .See set or compute function
compute=func None similar to set
exists=func True a function to say if the object "exists", depending on values from other attributs. See exists for details
can_read=func True a function to say if the object can be read. see can_read for details
can_modify=func True a function to say if the object can be modified (read only value). see can_modify for details
on=(event_name, function) None trigged to an event. see events for details
views=[ "view1", "!view2" ] [] Say if this element belong to a view. see views for details

See functions for mor details and examples how to use them.

Int()

Int( options ) is for integer.

Int( options ) use generic options.

available specific options for Int() ares :

Option Default Description
min= None minimum value
minimum=21 None similar to min
max=99 None maximum value
maximum=99 None similar to max=99
# example
from stricto import Dict, Int, String

client = Dict{
    "age" : Int( min=21, max=120)
}

client.age = 12  # -> raise an error
client.age = 120  # -> Ok

newAge = client.age+1 # -> raise an Error ( > max ) newAge is implicitly an Int( min=21, max=120))
newAge = 1+client.age # -> Ok (newAge is implicitly an int)

String()

String( options ) is for strings.

String( options ) use generic options.

available specific options for Int() ares :

Option Default Description
pattern=regexp None must match this regexp
patterns=[reg1, reg2] None must match all regexps
regexp= None similar to pattern

Examples

a=String( pattern='^A' )
a.set('Foo')        # -> raise an error
a.set('AZERTY')     # OK

# list of regexp
a=String( patterns=[ '^A', r'.*Z$' ] )
a.set('Allo')        # -> raise an error
a.set('AtoZ')        # OK

# function return a regexp
a=String( pattern=lambda self, value, root : r'.*Z$')
a.set('Allo')        # -> raise an error
a.set('AtoZ')        # OK

List()

List( options ) is for list.

List( options ) use generic options.

available specific options for List() ares :

Option Default Description
min= None minimum number of elements in the list
minimum=21 None similar to min
max=99 None maximum number of elements in the list
maximum=99 None similar to max=99
uniq=True None duplicate values are forbidden
# example
from stricto import Dict, List

client = Dict{
    "nicknames" : List( String(), default=[], uniq=True, min=0, max=3)
}

client.nicknames = [ "Ed", "Eddy", "Edward" ]  # -> raise an error
client.nicknames = [ "Ed" ]  # -> Ok
client.nicknames.append( "Ed" ) # -> raise an error (must be uniq)

Tuple()

Tuple( options ) is for tuple.

Tuple( options ) use generic options.

Ther is no available specific options for Tuple().

# example
from stricto import Dict, Tuple

client = Dict{
    "address" : Tuple( (Int(), String()) )
}

print(client.address) # -> None
client.address = ( 12, "accacia avenue" )  # -> Ok
client.address[1] # -> "acacia avenue"
client.address[0] = 13  # -> raise an error like a standard tuple
client.address = ( 13, "accacia avenue" )  # -> Ok

In()

In( [ Array of types ] ) is not a type, but an union of diffferent types.

In( options ) use generic options.

# example
from stricto import In, Int, String

a = In( [ Int(), String() ] )

a.set("hello") # -> OK
a.count('h') # -> return 1

a.set(12) # -> OK
a.bit_length() # -> return 4
a.count('h') # -> return None

a.set(3.14) # -> raise an error

Functions

a func can return a value to adapt the result. It can bee a lambda too.

transform

Please see transform function

# example
from stricto import Dict, Int, String

def upper(value, o):
    """
    transform the value into upper

    value : the current value given ("worldcompagny" in this example).
    o     : the full object
    """
    return value.upper()

company=Dict({
    "name" : String( transform=upper ),
})

company.name="worldcompagny"
print(company.name) # -> "WORLDCOMPAGNY"

set or compute

# example
from stricto import Dict, Int, String

a=Dict({
    "b" : Int( default = 0, set=lambda o: o.c+1 ),
    "d" : Int( default = 0, set=lambda o: o.b+1 ),
    "c" : Int( ),
})

# "b" and "d" cannot be modified by hand. the are recalculated every time another value 
# change in the Dict.

a.b = 3 # -> raise an error

a.c = 2
print(a.b) # -> 3
print(a.d) # -> 4

constraints

# example
from stricto import Dict, Int, String



def check_pair(value, o): # pylint: disable=unused-argument
    """
    return true if pair
    """
    return not value % 2

a=Dict({
    "b" : Int( default = 0, constraint=check_pair ),        # check before setting
    "d" : Int( constraint=lambda value, o : not value % 2 ), # same as above, with a lambda
    "c" : Int( constraints=[ check_pair ] ),                # A list of constraints
})

a.b = 2 # OK
a.c = 3 # -> raise an error

onchange

# example
from stricto import Dict, Int, String



def change_test(old_value, value, o): # pylint: disable=unused-argument
    """
    just a change option
    old_value   -> The previous value
    value       -> the new one
    o           -> the root object = a in our example
    """
    print(f"The value of b as changed from {old_value} to {value}")

a=Dict({
    "b" : Int( default = 0, onchange=change_test )
})

a.b = 2     # -> output "The value of b as changed from 0 to 2"
a.b = 3-1   # -> nothing displayed

exists

A function wich must return True|False to say if this key exists.

# example
from stricto import Dict, Int, String

def check_if_female(value, o):
    """
    return true if Female
    """
    if o.gender == "Male":
        return False
    return True

cat=Dict({
    "name" : String(),
    "gender" : String( default = 'Male', in=[ 'Male', 'Female' ]),
    "female_infos" : Dict(
        {
        "number_of_litter" : Int(default=0, required=True)
        # ... some other attributes

    }, exists=check_if_female )
})

cat.set({ "name" : "Felix", "gender" : "Male" }
cat.female_infos   # -> None
cat.female_infos.number_of_litter = 2 # -> Raise an Error

cat.gender = "Female"
cat.female_infos.number_of_litter = 2 # -> Ok
cat.female_infos # -> { "number_of_litter" : 2 }

can_read

A function wich must return True|False to say if this key can be read. Differ from exists in the sens a object can exists but currently not be readable

# example
from stricto import Dict, Int, String

current_user_name="John"

def can_see_and_modify_salary(value, o):

    """
    return true if can read the salary
    """
    global current_user_name
    if current_user_name == o.name:
        return True
    return False


user=Dict({
    "name" : String(),
    "salary" : Int( default=0, can_read=can_see_and_modify_salary, can_modify=can_see_and_modify_salary ),
})

user.set({ "name" : "John", "salary" : 20000 }
user.salary   # -> 20000

user.name="Jack"
user.salary # -> raise an error

Events

A stricto object can be trigged by custom events.

import random
from stricto import Dict, Int, String

def random( event_name, root, me ):
    me.set(random.randint(1, 6))


user=Dict({
    "name" : String(),
    "dice1" : Int( default=1, on=('roll' , random) ),
    "dice2" : Int( default=1, on=[ ('roll' , random)] ),
})


user.set({ "name" : "dice1and2" })
# Later
user.trigg('roll')
user.dice1 # -> A number 1-6
user.dice2 # -> A number 1-6

Views

Viewspermits "extraction" of a sub objects.

You can specify in views :

  • Belong explicitely to a view with views=[ "my_view" ]
  • Belong explicitely not to be in a view with views=[ "!my_view" ]

You can specify in get_view() :

  • an view with all fields excepts those explixitely marked with a "!"with get_view("my_view")
  • an explicite view (only those explicitely marked in view) with get_view("+my_view")

For example

from stricto import Dict, Int, String

# ISO 3166 country reference
country=Dict({
    "name" : String( view=[ "short" ] ),
    "a2" : String( view=[ "short" ] ),
    "a3" : String(),
    "num" : String(),
    "flag_url" : String( set=lambda o: f"https://flagcdn.com/256x192/{o.a2}.png", view=["!save", "short" ] ),
})

country.set({ "name" : "Ukraine", "a2" : "UA", a3 : "UKR", "num" : "804" })

# Whant only fields explicitely in view "short"
v = country.get_view("+short")
# type(v) is a Dict
# v = { "name" : "Ukraine", "a2" : "UA", "flag_url" : "https://flagcdn.com/256x192/UA.png" }

# Whant all fields excepts those with "!short". so all
l = country.get_view("short")
# l == country

s = country.get_view("save")
# type(s) is a Dict
# s = { "name" : "Ukraine", "a2" : "UA", a3 : "UKR", "num" : "804" }
l = country.get_view("+save")
# l == None

l = country.get_view("blabla")
# l == country
l = country.get_view("+blabla")
# l == None

Schemas

You can extract a schema as a dict Example :

import stricto

def check_pair():
    pass

a = Dict(
    {
        "b": List(String()),
        "c": In([String(), Int( constraint=check_pair )]),
        "d": Tuple([String(require=True), Bool()]),
    }
)
b = Dict(
    {
        "b": List(String()),
        "c": In([String(), Int( constraints=[check_pair])]),
        "d": Tuple([String(), Bool()]),
    }
)

a.get_schema() == b.get_schema() # False, a.d and b.d differs.

extended types

Using Extend

You can define your own stricto compatible type using Extend.

For that, you have to derivate your type from Extend, and define methods for enconding and decoding the object. You can define the __repr__ funtion too.

Example with datetime :

from datetime import datetime
from stricto import Extend


class Datetime(Extend):
    """
    A specific class to play with datetime
    """

    def __init__(self, **kwargs):
        """
        initialisation. Must pass the type (datetime) in args for Extend
        """
        super().__init__(datetime, **kwargs)

    def __json_encode__(self):
        """
        Called by the specific Encoder
        to encode datetime
        """
        return self.get_value().isoformat()

    def __json_decode__(self, value):
        """
        Called by the specific Decoder
        to decode a datetime
        """
        return self._type.fromisoformat(value)


a=Datetime()
a.set(datetime(2000, 1, 1))
a.year # 2000 

Using Dict

You can define your own structure. An exemple with complex :

from stricto import Dict, Float

class Complex(Dict):
    """
    A specific class to play with Dict
    """

    def __init__(self, **kwargs):
        """
        initialisation. Must define the struct
        """
        super().__init__(
            {
            "real": Float(), 
            "imag": Float()
            },
            **kwargs)

    def __repr__(self):
        return f"({self.real}+{self.imag}i)"

    def __add__(self, other):
        """
        add two complex
        """
        if not isinstance(other, Complex):
            raise TypeError("can only add Complex")

        r = self.__copy__()
        r.real = self.real + other.real
        r.imag = self.imag + other.imag
        return r

a = Dict({"b": Complex(), "c": Int(default=0)})
a.b.real = 12.0
a.b.imag = 9.0
self.assertEqual(repr(a.b), "(12.0+9.0i)")

Tests & co

# all tests
python -m unittest tests
# or for only some tests
python -m unittest tests/test_bool.py
# or for a specific test
python -m unittest tests.TestDict.test_simple_type

# reformat
python -m black .

# pylint
pylint $(git ls-files '*.py')

# coverage
coverage run -m unittest tests
coverage html # report under htmlcov/index.html

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stricto-0.0.5.tar.gz (28.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stricto-0.0.5-py3-none-any.whl (25.8 kB view details)

Uploaded Python 3

File details

Details for the file stricto-0.0.5.tar.gz.

File metadata

  • Download URL: stricto-0.0.5.tar.gz
  • Upload date:
  • Size: 28.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.10.14

File hashes

Hashes for stricto-0.0.5.tar.gz
Algorithm Hash digest
SHA256 ecd06a7f1fde427e10355fe257a2c9ed977d2390d64709e1f6d4995ed83dc080
MD5 a851d18aeb50c166305bf5add419bc03
BLAKE2b-256 63b11274ac05024cb62ffa9b83e9268843ef07f463ae20d6e453e3c6d55bf739

See more details on using hashes here.

File details

Details for the file stricto-0.0.5-py3-none-any.whl.

File metadata

  • Download URL: stricto-0.0.5-py3-none-any.whl
  • Upload date:
  • Size: 25.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.10.14

File hashes

Hashes for stricto-0.0.5-py3-none-any.whl
Algorithm Hash digest
SHA256 80c1e0fb611d93588563ece60ee3f8d956d79ad89ae0832691849bffddda5ccc
MD5 3bbc82b91bffb3971279100586f0fa42
BLAKE2b-256 91e5d8b80cf53400b3f84cc98dd919825ef341e8edaa1595f38acca97823d237

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page