ft3 package

Overview

Author: dan@1howardcapital.com

Summary: Zero-dependency python framework for object oriented development. Implement once, document once, in one place.

With ft3, 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 ft3 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 ft3

class Api(class_as_dict: dict[str | string[StringType], Any] | None = None, /, **kwargs: Any)

Bases: Component

OpenAPI OpenAPI Object.

info: Field[Info]
openapi: Field[str]
paths: Field[dict[str, Path]]
tags: Field[list[Tag]]
servers: Field[list[ServerObject] | None]
components: Field[dict[str, dict[str, Any]] | None]
enumerations: lib.t.ClassVar[dict[str, tuple[typ.Primitive, ...]]] = {}
fields: lib.t.ClassVar[typ.FieldsTuple] = ('_ref_', 'components', 'info', 'openapi', 'paths', 'servers', 'tags')
hash_fields: lib.t.ClassVar[typ.FieldsTuple] = ()
classmethod register(obj_: type[ObjectType]) type[ObjectType]

Register an Object to be served from the API.

class Field(class_as_dict: dict[str | string[StringType], Any] | None = None, /, *, type_: type | type[Type] | type[AnyType] = None, description: str = None, default: Any | Callable[[], Any] = None, required: bool = False, enum: list[Immutable] | set[Immutable] | tuple[Immutable, ...] | EnumType = None, min_length: int = None, max_length: int = None, minimum: float = None, exclusive_minimum: bool = None, maximum: float = None, exclusive_maximum: bool = None, multiple_of: float = None, pattern: str = None, min_items: int = None, max_items: int = None, unique_items: bool = None, read_only: bool = None, write_only: bool = None, **kwargs: Any)

Bases: Object, Generic[AnyType]

Simple field object.

Querying

Queries for Objects can 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 to and and or clauses 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 limit field.

  • 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 Object for which the string 'test' is not a member of list_field and for which either the value for integer_field is greater than or equal to 1 or the value for string_field is at least 75% similar to 'test'. Results would then be sorted first in ascending order on string_field, then in descending order on integer_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 type or None.

required: bool = False

Whether or not the field SHOULD be required. Default behavior changes to assume True if 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. None is always allowed.

min_length: int = None

Specify len(value) SHOULD be >= minimum. Field type MUST be str if specified.

max_length: int = None

Specify len(value) SHOULD be <= maximum. Field type MUST be str if specified.

minimum: float = None

Specify value SHOULD be >= minimum. Field type MUST be numeric if specified.

exclusive_minimum: bool = False

Set True to 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 True to specify value SHOULD be < maximum. Field maximum MUST also be specified.

multiple_of: float = None

Specify value % multiple_of SHOULD be 0. Field type MUST be numeric if specified.

pattern: str = None

Specify a Regex pattern for which the value SHOULD match. Field type MUST be str if specified.

min_items: int = None

Specify len(value) SHOULD be >= min_items. Field type MUST be deque | frozenset | list | tuple | set if specified.

max_items: int = None

Specify len(value) SHOULD be <= max_items. Field type MUST be deque | frozenset | list | tuple | set if specified.

unique_items: bool = False

Specify all elements of value SHOULD be unique. Field type MUST be deque | frozenset | list | tuple | set if specified.

read_only: bool = False

Specify this field SHOULD only be available to read operations (like GET http calls).

write_only: bool = False

Specify this field SHOULD only be available to write operations (like PATCH, POST, or PUT http calls).

_object_: Field[type[typ.Object]]
name: Field[str]
type_: Field[type[typ.AnyType]]
description: Field[str]
default: Field[typ.AnyType]
required: Field[bool]
enum: Field[typ.Enum]
min_length: Field[int]
max_length: Field[int]
minimum: Field[float]
exclusive_minimum: Field[bool]
maximum: Field[float]
exclusive_maximum: Field[bool]
multiple_of: Field[float]
pattern: Field[str]
min_items: Field[int]
max_items: Field[int]
unique_items: Field[bool]
read_only: Field[bool]
write_only: Field[bool]
DELETE(fn: Callable[[api.events.obj.Request], None]) Callable[[api.events.obj.Request], None]
GET(fn: Callable[[api.events.obj.Request], list[typ.Object] | typ.Object | str]) Callable[[api.events.obj.Request], list[typ.Object] | typ.Object | str]
OPTIONS(fn: Callable[[api.events.obj.Request], None]) Callable[[api.events.obj.Request], None]
PATCH(fn: lib.t.Callable[[api.events.obj.Request], typ.Object]) lib.t.Callable[[api.events.obj.Request], typ.Object]
POST(fn: lib.t.Callable[[api.events.obj.Request], typ.Object]) lib.t.Callable[[api.events.obj.Request], typ.Object]
PUT(fn: lib.t.Callable[[api.events.obj.Request], typ.Object]) lib.t.Callable[[api.events.obj.Request], typ.Object]
_validate_comparison(value: Any) Never | None
_validate_iterable_comparison(value: Any) Never | None
enumerations: lib.t.ClassVar[dict[str, tuple[typ.Primitive, ...]]] = {}
property factory: Callable[[], AnyType]

Return callable returning default value for field.

fields: lib.t.ClassVar[typ.FieldsTuple] = ('_object_', 'default', 'description', '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',)
parse(value: Any, raise_validation_error: bool = True) AnyType | None | Never

Return correctly typed value if possible, None otherwise, or [optionally] raise an error if an invalid value is passed, the method’s default behavior.

class File(class_as_dict: dict[str | string[StringType], Any] | None = None, /, **kwargs: Any)

Bases: Object

A file object, useful for serving static files.

path: Field[str]

The path at which to serve the file.

content: Field[bytes | str]

File content.

content_type: Field[str]
enumerations: lib.t.ClassVar[dict[str, tuple[typ.Primitive, ...]]] = {}
fields: lib.t.ClassVar[typ.FieldsTuple] = ('content', 'content_type', 'path')
hash_fields: lib.t.ClassVar[typ.FieldsTuple] = ()
class Object(class_as_dict: dict[str | string[StringType], Any] | None = None, /, **kwargs: Any)

Bases: ObjectBase

Base 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 pets with the schema below, you would want to create a corresponding python representation similar to the following.

pets table

| id  | name     | type   |
| --- | -------- | ------ |
| a1  | fido     | dog    |
| a2  | garfield | cat    |
| a3  | sophie   | dog    |
| a4  | stripes  | turtle |

python representation

import ft3


class Pet(ft3.Object):
    """A pet."""

    id_: ft3.Field[str] # Trailing underscores are special
                         # in ft3, check the documentation
                         # below for more detail.
    name: ft3.Field[str] = 'Fido'  # Setting = 'Fido' will mean that
                                   # all Pet() instances will be
                                   # named 'Fido' by default.
    type: ft3.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, None will 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’, ‘name’, or ‘key’ (case and underscore 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 in will correctly map to the in_ field on the Object. See below for more detail.

import ft3


class Pet(ft3.Object):
    """A pet."""

    id_: ft3.Field[str]
    _alternate_id: ft3.Field[str]

    name: ft3.Field[str]
    type: ft3.Field[str]
    in_: ft3.Field[str]
    is_tail_wagging: ft3.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 ft3


class Pet(ft3.Object):  # noqa

    name: ft3.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.AnyString, lib.t.Any]]] = {}

Instantiate class directly from passed dict (assumed to be version of class in dict form).

Subpackages