fqr package¶
Overview¶
Author: dan@1howardcapital.com
Summary: Zero-dependency python framework for object oriented development. Implement once, document once, in one place.
With fqr, you will quickly learn established best practice… or face the consequences of runtime errors that will break your code if you deviate from it.
Experienced python engineers will find a framework that expects and rewards intuitive magic method implementations, consistent type annotations, and robust docstrings.
Implement pythonically with fqr and you will only ever need to: implement once, document once, in one place.
Getting Started¶
Installation¶
Install from command line, with pip:
$ pip install fqr
- class Field(class_as_dict: dict[string[snake_case], Any] | None = None, /, *, type_: type | type[Type] | type[AnyType] = None, default: Any | Type | AnyType = None, required: bool = False, enum: Array[Immutable] | lib.enum.EnumMeta = None, min_length: int = None, max_length: int = None, minimum: float = None, exclusive_minimum: bool = False, maximum: float = None, exclusive_maximum: bool = False, multiple_of: float = None, pattern: str = None, min_items: int = None, max_items: int = None, unique_items: bool = False, read_only: bool = False, write_only: bool = False, **kwargs: Any)¶
Bases:
Object,Generic[AnyType]Simple field object.
Querying¶
Queries for
Objectscan be generated from their fields using the following comparison operators:field_1_eq_filter = Object.field_1 == 'test_value_123'field_1_ne_filter = Object.field_1 != 'test_value_123'field_1_ge_filter = Object.field_1 >= 'test_value_123'field_1_gt_filter = Object.field_1 > 'test_value_123'field_1_le_filter = Object.field_1 <= 'test_value_123'field_1_lt_filter = Object.field_1 < 'test_value_123'
And the following special operators:
field_1_contains_filter = Object.field_1 << 'test_value_123'field_1_similarity_filter = Object.field_1 % 'test_value_123'field_1_similarity_filter_with_threshold = Object.field_1 % ('test_value_123', 0.8)
Queries may be chained together using the
&and|bitwise operators, corresponding toandandorclauses respectively.Additionally, the invert (
~) operator may be prefixed to any Query to match the opposite of any conditions specified instead.Queries also support optional result limiting and sorting:
Result limits can be specified by setting the
limitfield.Results can be sorted any number of times using the
+=and-=operators.
Example¶
query: Query = ( ( (Object.integer_field >= 1) | (Object.string_field % ('test', 0.75)) ) & ~(Object.list_field << 'test') ) += 'string_field' -= 'integer_field'
In the example above, the query would match any
Objectfor which the string'test'isnota member oflist_fieldand for which either the value forinteger_fieldis greater than or equal to1or the value forstring_fieldis at least75%similar to'test'. Results would then be sorted first inascendingorder onstring_field, then indescendingorder oninteger_field.Parameters¶
Specify parameters to constrain values allowed for the field and control its behavior.
name: str = None
Field Name. Sourced from / overwritten by attribute name.
type: type[lib.t.Any] = None
Type of value. Sourced from / overwritten by type annotation.
default: lib.t.Any = None
Default value for field. Sourced from / overwritten by attribute value. MUST be an instance of field
typeorNone.required: bool = False
Whether or not the field SHOULD be required. Default behavior changes to assume
Trueif no attribute value is specified for the field.enum: deque | frozenset | list | tuple | set | Enum = None
Sequence of which field value SHOULD be a member, unless
"*"is included in the sequence, in which case ANY value MAY be allowed, in addition to those explicitly specified.Noneis assumed to be allowed if the field is nullable.min_length: int = None
Specify
len(value)SHOULD be>=minimum. Field type MUST bestrif specified.max_length: int = None
Specify
len(value)SHOULD be<=maximum. Field type MUST bestrif specified.minimum: float = None
Specify value SHOULD be
>=minimum. Field type MUST be numeric if specified.exclusive_minimum: bool = False
Set
Trueto specify value SHOULD be>minimum. Field minimum MUST also be specified.maximum: float = None
Specify value SHOULD be
<=maximum. Field type MUST be numeric if specified.exclusive_maximum: bool = False
Set
Trueto specify value SHOULD be<maximum. Field maximum MUST also be specified.multiple_of: float = None
Specify
value % multiple_ofSHOULD be0. Field type MUST be numeric if specified.pattern: str = None
Specify a Regex pattern for which the value SHOULD match. Field type MUST be
strif specified.min_items: int = None
Specify
len(value)SHOULD be>=min_items. Field type MUST bedeque | frozenset | list | tuple | setif specified.max_items: int = None
Specify
len(value)SHOULD be<=max_items. Field type MUST bedeque | frozenset | list | tuple | setif specified.unique_items: bool = False
Specify all elements of value SHOULD be unique. Field type MUST be
deque | frozenset | list | tuple | setif specified.read_only: bool = False
Specify this field SHOULD only be available to read operations (like
GEThttp calls).write_only: bool = False
Specify this field SHOULD only be available to write operations (like
PATCH,POST, orPUThttp calls).- enumerations: lib.t.ClassVar[dict[str, tuple[typ.Primitive, ...]]] = {}¶
- fields: lib.t.ClassVar[typ.FieldsTuple] = ('default', 'enum', 'exclusive_maximum', 'exclusive_minimum', 'max_items', 'max_length', 'maximum', 'min_items', 'min_length', 'minimum', 'multiple_of', 'name', 'pattern', 'read_only', 'required', 'type_', 'unique_items', 'write_only')¶
- hash_fields: lib.t.ClassVar[typ.FieldsTuple] = ('name',)¶
- _validate_comparison(value: Any) Never | None¶
- _validate_iterable_comparison(value: Any) Never | None¶
- parse(value: Any, raise_validation_error: bool = True) AnyType | None | Never¶
Return correctly typed value if possible,
Noneotherwise, or [optionally] raise an error if an invalid value is passed, the method’s default behavior.
- property factory: Callable[[], AnyType]¶
Return callable returning default value for field.
- class Object(class_as_dict: dict[string[snake_case], Any] | None = None, /, **kwargs: Any)¶
Bases:
ObjectBaseBase Object.
Usage¶
Subclass to create objects for your application.
General Recommendations¶
Ideally, objects should be 1:1 with their counterparts in the data store from which they are originally sourced (even if that data store is your own database, and even if that data is not ostensibly stored in a 1:1 manner, as is the case with most relational databases).
For example, if there is a SQL table called
petswith the schema below, you would want to create a correspondingpython representationsimilar to the following.
pets table¶
| id | name | type | | --- | -------- | ------ | | a1 | fido | dog | | a2 | garfield | cat | | a3 | sophie | dog | | a4 | stripes | turtle |
python representation¶
import fqr class Pet(fqr.Object): """A pet.""" id_: fqr.Field[str] # Trailing underscores are special # in fqr, check the documentation # below for more detail. name: fqr.Field[str] = 'Fido' # Setting = 'Fido' will mean that # all Pet() instances will be # named 'Fido' by default. type: fqr.Field[str] # You can make a field 'required' by # not specifying a default value.
Special Rules¶
Default Values¶
Subclassed (derivative) objects should include default values for all fields specified. In cases where a default value is not specified,
Nonewill be used instead and the field will be assumed to be ‘required’ for all downstream purposes (ex. as a query parameter for HTTP requests) unless otherwise specified explicitly.Type Annotations¶
Type annotations are required and must be a generic
Field[type]. For example:Field[int],Field[str],Field[str | bool].Not only is this best practice, these are leveraged downstream to do things like auto-document and auto-generate API’s.
Uniform Casing¶
ALL Fields must be either camelCase or snake_case, with the only exception being that fields may begin with an underscore ‘_’, so long as all following characters adhere to camelCase or snake_case conventions.
Underscore Prefix for Private Fields¶
Fields that begin with an underscore ‘*’ will be ignored on conversion to / from DBO, REST, and JSON representations, unless the field ends with ‘id’ or ‘id*’ (case insensitive), in which case it will still be converted.
This follows the broader pattern of flagging methods and attributes as private / internal to a system with a preceding underscore. It should be expected that end users of your system will not need to interact with these fields.
Underscore Suffix for Reserved Keyword Fields¶
Fields with a trailing underscore ‘_’ will automatically have the trailing underscore removed on conversion to / from DBO, REST, and JSON representations.
This allows for python keywords, such as
in_, to be used as object fields, where they would otherwise raise errors without the proceeding underscore.On translation to and from dictionaries, keys without underscores will still be checked against these fields – so, a dictionary with key
inwill correctly map to thein_field on the Object. See below for more detail.
import fqr class Pet(fqr.Object): """A pet.""" id_: fqr.Field[str] _alternate_id: fqr.Field[str] name: fqr.Field[str] type: fqr.Field[str] in_: fqr.Field[str] is_tail_wagging: fqr.Field[bool] = True # This means each of the below will work. bob_the_dog = Pet( id='abc123', _alternate_id='dog1', name='Bob', type='dog', in_='timeout', is_tail_wagging=False ) bob_the_dog = Pet( { 'id': 'abc123', '_alternate_id': 'dog1', 'name': 'Bob', 'type': 'dog', 'in': 'timeout', 'is_tail_wagging': False } ) # And so would this, since translation # automatically handles camelCase to # snake_case conversions. bob_the_dog = Pet( { 'id': 'abc123', 'alternateId': 'dog1', 'name': 'Bob', 'type': 'dog', 'in': 'timeout', 'isTailWagging': False } )
Special Method Usage¶
Objects have been designed to be almost interchangable with dictionaries. The primary difference is that values cannot be assigned to keys unless you define them on the Object’s class definition itself.
This is done to automatically maximize the efficiency of your application’s memory footprint. Feel free to read more about python slots to better understand why this is necessary.
import fqr class Pet(fqr.Object): # noqa name: fqr.Field[str] dog = Pet(name='Fido') # The below would return the string, 'Fido'. dog['name'] # The following would set the dog's name to something else. dog.setdefault('name', 'Arnold') assert dog.name == 'Arnold' dog.setdefault('name', 'Buddy') assert dog.name == 'Arnold' dog['name'] = 'Buddy' assert dog.name == 'Buddy' assert dog['name'] == 'Buddy' # The following all work exactly the same as with a dictionary. # (in the below, key will be 'name' and value 'Fido'). for key, value in dog.items(): break for key in dog.keys(): break for value in dog.values(): break # But the following will raise a KeyError. dog['field_that_does_not_exist'] = 'Buddy' # And so would this, since fields can only be added # or removed on the class definition of Pet itself. dog.setdefault('field_that_does_not_exist', 'Buddy')
Object truthiness will evaluate to True if any values for the Object instance are different from default values, otherwise False.
if Object:
Objects are designed to display themselves as neatly formatted JSON on calls to
__repr__.print(Object)
Updates Object1 with values from Object2 if they are a non-default value for the object.
Object1 << Object2
Overwrites Object1 values with those from Object2 if they are a non-default value for the object.
Object1 >> Object2
Returns a dictionary with {fieldName: fieldValue2} for any fields that differ between the two Objects.
Object1 - Object2
Get value for Object field.
value = Object['field']
Set value for Object field.
Object['field'] = value
Returns True if any one of field, field, field, or field is a valid field for the Object, otherwise False.
field in Object
Same as
len(Object.fields).len(Object)
- enumerations: lib.t.ClassVar[dict[str, tuple[typ.Primitive, ...]]] = {}¶
- fields: lib.t.ClassVar[typ.FieldsTuple] = ()¶
- hash_fields: lib.t.ClassVar[typ.FieldsTuple] = ()¶
- class_as_dict: lib.t.Final[lib.t.Optional[dict[typ.string[typ.snake_case], lib.t.Any]]] = {}¶
Instantiate class directly from passed
dict(assumed to be version of class indictform).
Subpackages¶
- fqr.cli package
- fqr.core package
- fqr.docs package
- fqr.loggers package
- fqr.objects package
- Objects Overview
FieldField.nameField.type_Field.defaultField.requiredField.enumField.min_lengthField.max_lengthField.minimumField.exclusive_minimumField.maximumField.exclusive_maximumField.multiple_ofField.patternField.min_itemsField.max_itemsField.unique_itemsField.read_onlyField.write_onlyField.enumerationsField.fieldsField.hash_fieldsField.class_as_dictField._validate_comparison()Field._validate_iterable_comparison()Field.parse()Field.factory
Object- Subpackages
- Submodules
- fqr.objects.cfg module
- fqr.objects.enm module
- fqr.objects.exc module
FieldAnnotationErrorIncorrectCasingErrorIncorrectDefaultTypeErrorIncorrectTypeErrorInvalidComparisonTypeErrorInvalidContainerComparisonTypeErrorInvalidFieldAdditionErrorInvalidFieldRedefinitionErrorInvalidObjectComparisonErrorMissingTypeAnnotationReservedKeywordErrorTypeValidationErrorBasePackageException
- fqr.objects.lib module
- fqr.objects.typ module
- fqr.objects.utl module