{ "info": { "author": "George Barb\u0103ro\u0219ie", "author_email": "george.barbarosie@gmail.com", "bugtrack_url": null, "classifiers": [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy" ], "description": "\n- [Introduction](#introduction)\n - [RESTful API](#restful-api)\n - [Opinionated](#opinionated)\n - [Modified JSON](#modified-json)\n- [How to use](#how-to-use)\n- [Reference](#reference)\n - [JsonObject](#jsonobject)\n - [JSONEncoder](#jsonencoder)\n - [JSONDecoder](#jsondecoder)\n - [pyramid_json_renderer_factory](#pyramidjsonrendererfactory)\n - [pyramid_json_decoder](#pyramidjsondecoder)\n - [patch_sqlalchemy_base_class](#patchsqlalchemybaseclass)\n - [monkeypatch: obj.apply_changes](#monkeypatch-objapplychanges)\n - [CRUDView](#crudview)\n - [ConvertMatchdictPredicate](#convertmatchdictpredicate)\n - [CatchallPredicate](#catchallpredicate)\n - [CatchallView](#catchallview)\n - [Hints syntax](#hints-syntax)\n - [Drilldown support](#drilldown-support)\n - [Single element from collection](#single-element-from-collection)\n - [Filtering, sorting, pagination](#filtering-sorting-pagination)\n - [JsonGuardProvider](#jsonguardprovider)\n - [SearchPathSetter](#searchpathsetter)\n - [EnumAttrs and PythonEnum](#enumattrs-and-pythonenum)\n\n# Introduction\n\nPy-liant is a library of helpers for rapid creation of opinionated RESTful APIs\nusing pyramid and SQLAlchemy. It provides a read-write set of operations using\na slightly modified object-graph aware JSON structure which is tightly coupled\nwith the data models being exposed.\n\nIt was created by Trip Solutions for internal projects but we feel it may prove \nuseful for general consumption.\n\n## RESTful API\n\nThe [CRUDView](#crudview) base class assumes the API follows REST conventions\nand provides CRUD ([C]reate, [R]ead, [U]pdate, [D]elete) functionality, or a\nsubset of that. It does not make any assumptions about the endpoints, which are\nstill defined in user code. There are assumptions being made about the format of\nthe payloads, see [Modified JSON](#modified-json) and [CrudView](#crudview)\n\n## Opinionated\n\nThe [CatchallView](#catchallview) base class however provides a custom parser \nfor the URL string and is heavily opinionated about the structure of the API. \nThis allows it to be effortlessly deployed on top of existing SQLAlchemy data \nstructures but has the disadvantage of being less customizable.\n\n## Modified JSON\n\nORM data models are not always trees. Any real-world application beyond a\ncertain complexity level is bound to get to a point where mapping deep data\nmodels directly to JSON is [not\nfeasible](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value).\nIn our first iterations we've worked around this issue by manually decoupling\nthe JSON from the structure, but any manual process quickly turns into a time\nsink; it adds a lot of complexity for both client and server code.\n\nPy-liant solves the graph awareness issue by reserving two keywords for internal\nuse in the JSON graph. Any object that needs to be referenced from within the\nJSON structure will get a special key `_id` with a generated value. References\nto an object are codified using an object with a sigle key `_ref` matching the\n`_id` of the referenced object. Please note, this is only true for SQLAlchemy\nmodel objects.\n\nFor example, given the model declaration below:\n```python\nfrom sqlalchemy.orm import relationship, backref\nfrom sqlalchemy import Column, Integer, Text, ForeignKey\nfrom sqlalchemy.ext.declarative import declarative_base\nBase = declarative_base()\n\n\nclass Parent(Base):\n __tablename__ = 'parent'\n id = Column(Integer, primary_key=True)\n data = Column(Text)\n\n\nclass Child(Base):\n __tablename__ = 'child'\n id = Column(Integer, primary_key=True)\n parent_id = Column(ForeignKey(Parent.id))\n data = Column(Text)\n\n parent = relationship(Parent, backref=backref('children'))\n```\n\nthe following code snippent\n\n```python\nfrom py_liant.json_encoder import JSONEncoder\nencoder = JSONEncoder(base_type=Base, check_circular=False, indent=4*' ')\nparent = Parent(id=1, data=\"parent object\")\nparent.children.extend([\n Child(id=1, data=\"child 1\"),\n Child(id=2, data=\"child 2\")\n])\nprint(encoder.encode(parent))\n```\n\nwill output\n\n```json\n{\n \"id\": 1,\n \"data\": \"parent object\",\n \"children\": [\n {\n \"id\": 1,\n \"data\": \"child 1\",\n \"parent\": {\n \"_ref\": 1\n },\n \"_id\": 2\n },\n {\n \"id\": 2,\n \"data\": \"child 2\",\n \"parent\": {\n \"_ref\": 1\n },\n \"_id\": 3\n }\n ],\n \"_id\": 1\n}\n```\n\nThe encoder will also extract metadata information from SQLAlchemy models to\nsupport serialization. It will serialize only column and relationship\nproperties, which means it will not display any non-SQLAlchemy properties. It\nalso expects all relationships to be eagerly loaded and will avoid triggering\nany lazy-loaded properties. Deferred columns are also avoided.\n\nConversely, the [JSONDecoder](#jsondecoder) will turn a simlarly codified JSON \nstructure and return a completed graph, with potentially cyclic or multiple \nreferences, for use in the application.\n\nThe decoder will generate a structure of [JsonObject](#jsonobject)s. If the base\nclass is patched using [patch_sqlalchemy_base_class](#patchsqlalchemybaseclass),\nthe decoded object can be used to patch an existing or new SQLAlchemy model\ninstance.\n\nWe also provide a pair of encoder / decoder functions for use in javascript\nin [pyliant.js](./pyliant.js).\n\n# How to use\n\nIn pyramid's config block you can override the default JSON renderer using the\nfollowing:\n\n```python\nfrom py_liant.pyramid import pyramid_json_renderer_factory\nconfig.add_renderer('json', pyramid_json_renderer_factory(Base))\n```\n\nThen use `renderer='json'` in any `@view_config()` or `add_view()`.\n\nYou can use py-liant's JSON decoder by adding the following in pyramid's config:\n\n```python\nfrom py_liant.pyramid import pyramid_json_decoder\nconfig.add_request_method(pyramid_json_decoder, 'json', reify=True)\n```\n\nThus, for any request with a JSON payload in body you can access the decoded\nJsonObject structure using `request.json`.\n\nPatching the SQLAlchemy model's base class:\n\n```python\npatch_sqlalchemy_base_class(Base)\n```\n\nAdding the view predicates:\n\n```python\nconfig.add_view_predicate('convert_matchdict', ConvertMatchdictPredicate)\nconfig.add_view_predicate('catchall', CatchallPredicate)\n```\n\nPy-liant also provides a callable factory to do all of the above:\n\n```python\nfrom py_liant.pyramid import includeme_factory\nconfig.include(includeme_factory(base_class=Base))\n# identical to includeme_factory(base_class=Base)(config)\n```\n\nConcrete usage examples of [CRUDView](#crudview) and\n[CatchallView](#catchallview) can be found in the reference documentation\n\n# Reference\n\n## JsonObject\n\nThis class is a `dict` implementation that exposes all string keys as\nproperties. It eliminates the need to access dictionary values using index\nnotation (`request.json['prop']` becomes `request.json.prop`). The\n[JSONDecoder](#jsondecoder) returns instances of this class.\n\n## JSONEncoder\n\nA `simplejson.JSONEncoder` implementation that adds the following:\n- converts `date`, `time` and `datetime` objects to ISO8859 strings\n- converts `byte` values to Base64\n- strigifies python `Enum` values to their name, `uuid.UUID` values\n- tracks SQLAlchemy models (if provided a base class) as discussed in [Modified JSON](#modified-json)\n\nConstructor arguments:\n```python\nJSONDecoder(request=None, base_type=None, **kwargs)\n```\n`request` should be a pyramid request object. If provided it's used to apply \n[JsonGuardProvider](#jsonguardprovider) fencing for serialization.\n\n`base_type` is the SQLAlchemy models base class. If not provided the\nfunctionality related to SQLAlchemy is disabled.\n\n`kwargs` is passed to `simplejson.JSONEncoder`'s constructor\n## JSONDecoder\n\nA `simplejson.JSONDecoder` implementation that returns a\n[JsonObject](#jsonobject) as a result and handles `_id`/`_ref` logic as\ndescribed in [Modified JSON](#modified-json).\n\nConstructor argumets:\n```python\nJSONDecoder(**kwargs)\n```\n\n`**kwargs` is passed to `simplejson.JSONDecoder`'s constructor.\n\n## pyramid_json_renderer_factory\n\nFactory for a pyramid renderer that provides JSON serialization using\n[JSONEncoder](#jsonencoder). See [How to use](#how-to-use) for usage.\n\nArguments:\n```python\npyramid_json_renderer_factory(base_type=None, wsgi_iter=False, \n separators=(',',':'))\n```\n\n`base_type` and `separators` are passed to [JSONEncoder](#jsonencoder)'s\nconstructor. The default value for `separators` is meant to minimize payload\nsize by skipping any unnecessary spaces.\n\n`wsgi_iter` can be used to optimize rendering of JSON by passing an iterable\ndirectly to the WSGI layer. By default the renderer writes directly in the\npyramid `response` object. When activated pyramid can no longer handle error\nredirects for execptions thrown during serialization. \n\n## pyramid_json_decoder\n\nThis is a fucnction that can be added to pyramid using\n`config.add_request_method`. See [How to use](#how-to-use) for usage.\n\n## patch_sqlalchemy_base_class\n\nThis is the function that adds the method\n[apply_changes](#monkeypatch-objapplychanges) to SQLAlchemy's base class.\n\n## monkeypatch: obj.apply_changes\n\n```python\nobj.apply_changes(data, object_dict=None, context=None, for_update=True)\n```\n\nOnce SQLAlchemy's base class is patched using\n[patch_sqlalchemy_base_class](#patchsqlalchemybaseclass) all model instances get\na method that can be used to apply patches. This can be used directly but most\nof the time, if you use [CRUDView](#crudview) and/or\n[CatchallView](#catchallview), you won't have to.\n\nThe method will apply changes in any depth required. It converts the data types\nbased on metadata extracted from SQLAlchemy. It handles relationships, both\ncollections and instances, by tracking and comparing the primary keys provided in JSON. Where needed it will add new instances.\n\nFor an object without relationships it applies the values from `data` to their\ncorresponding column properties in `obj`. No property values are overwritten\nunless specified in the `data` object.\n\nIf an object has relationships the `data` object can drill down into them. For\ncollection relationships the `apply_changes` method expects all objects to be\nprovided in the corresponding array, at a minimum with their primary key\npresent. If a member of the array does not provide a primary key it is presumed\nto be a new instance. If a member of the object's collection cannot be tracked\nback to a member of the array in data, it will be removed from the collection.\n\nIf the primary key of the descendants is a composite that includes any of the\ncolumns in the foreign key the caller can provide the partial primary key and\npy-liant will reconstruct the remaining columns based on the relatonship to the\nparent.\n\nIf a pyramid `context` is provided that implements\n[JsonGuardProvider](#jsonguardprovider), it will be used for security fencing\nthe patching.\n\n## CRUDView\n\nThis class provides CRUD functionality for a given model class. You can\nconfigure the routes and views as needed for your application but the\nrecommended way is shown below:\n\n```python\nconfig.add_route('parent_pk', 'parent/{id}')\nconfig.add_route('parent_list', 'parent')\n\n@view_config(route_name='parent_pk', request_method='GET', attr='get')\n@view_config(route_name='parent_pk', request_method='POST', attr='update')\n@view_config(route_name='parent_pk', request_method='DELETE', attr='delete')\n@view_config(route_name='parent_list', request_method='GET', attr='list')\n@view_config(route_name='parent_list', request_method='POST', attr='insert')\nclass ParentView(CRUDView):\n target_type = Parent\n target_name = 'parent'\n\n def __init__(self, request):\n super().__init__(request)\n self.filters = self.auto_filters()\n self.accept_order = self.auto_order()\n\n def identity_filter(self):\n return Parent.id == int(self.request.matchdict('id'))\n```\n\nThis is enough to provide a complete read-write endpoint for objects of type\n`Parent`.\n\nUse `GET /parent/1 HTTP/1.1` to retrieve parent with id=1. It should return\nsomething along the lines of: \n\n```json\n{\n \"parent\": {\n \"id\": 1,\n \"data\": \"parent object\",\n \"_id\": 1\n }\n}\n```\nUse \n```\nPOST /parent/1 HTTP/1.1\n\n{\n \"parent\": {\n \"data\": \"parent object changed\"\n }\n}\n```\n\nto update the data in instance of parent with id=1.\n\nPosting to `/parent` instead of `/parent/1` will create a new instance instead\nof updating an existing one.\n\n`DELETE /parent/2 HTTP/1.1` will delete the parent with id=2.\n\nFinally, `GET /parent HTTP/1.1` will provide a list of all parent instances in\nthe database. \n\nFor the listing endpoint the following response will be returned:\n\n```json\n{\n \"items\": [\n {\n \"id\": 1,\n \"data\": \"parent object\",\n \"_id\": 1\n }\n ],\n \"total\": 1\n}\n```\n\nThe `CRUDView` class also offers pagination support, implicit and explicit\nfiltering, implicit and explicity sorting.\n\nPagination is supported via GET parameters `page` and `pageSize` (i.e., `GET\n/parent?page=3&pageSize=20`).\n\nImplicit filters and sorting are provided for all column properties. Assuming\ncolumn properties `id` and `data` for class User, the following filters will be\nadded to `self.filters` (in the example usage above, during construction, see\nthe `auto_filters()` call): id, id_lt, id_le, id_gt, id_ge, data, data_lt,\ndata_le, data_gt, data_ge, data_like. The filters [field_name]_[operator]\nprovide filtering using the less-than, less-or-equal, greater-than,\ngreater-or-equal and contains operators. The last one is automatically generated\nfor string column properties only.\n\nAutomatic filters are also be added (in the example usage above see the call to\n`auto_order()`) for both fields.\n\nFiltering in a listing endpoint is done as such: `GET /parent?data_like=object`.\nMultiple filters can be applied, i.e. `GET /parent?id_lt=10&id_gt=5`.\n\nSorting is done by using the GET parameter `order`, i.e. `GET\n/parent?order=data`. Multipe sorting expressions can be applied, i.e.\n`order=data,id`. In other words the value passed in `order` is a comma-separated\nlist of sorting keys. Each sorting key also accepts the descending modifier,\ni.e. `order=data+desc,id`.\n\nSorting and filtering keys can also be manually defined. In the usage example above we could have defined some filters and orderings by hand as such:\n\n```python\nclass ParentView(CRUDView):\n filters = {\n 'id': lambda _: Parent.id == int(_),\n 'id_lt': lambda _: Parent.id < int(_),\n 'data': lambda _: Parent.data == _,\n 'data_like': lambda _: Parent.data.contains(_)\n }\n accept_order = {\n 'data': Parent.data,\n 'data_lowercase': func.lower(Parent.data)\n }\n```\n\nDoing this is obviously more laborious but allows you to define custom filters or soring expressions.\n\nThe implementation assumes `request.dbsession` is a request method that returns\na SQLAlchemy database session valid for the model.\n\n## ConvertMatchdictPredicate\n\nIf pyramid has been configured to use this predicate as indicated in [How to\nuse](#how-to-use) you can get around the need to convert matchdict parameters.\n\nPyramid's [URL\nDispatch](https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/urldispatch.html#custom-route-predicates)\ndocumentation page shows the following example for URL matchdict conversion:\n\n```python\ndef integers(*segment_names):\n def predicate(info, request):\n match = info['match']\n for segment_name in segment_names:\n try:\n match[segment_name] = int(match[segment_name])\n except (TypeError, ValueError):\n pass\n return True\n return predicate\n\nymd_to_int = integers('year', 'month', 'day')\n\nconfig.add_route('ymd', '/{year}/{month}/{day}',\n custom_predicates=(ymd_to_int,))\n```\n\nThis code ensures both that the route will not match unless predicate executes\nsuccesfully (returns `True`) and that the view will see integer values for keys\n`year`, `month` and `day` in `request.matchdict`. While this is very useful it\nis unfortunately deprecated functionality. Sice pyramid-1.5 you will get a\ndeprecation warnin when using `custom_predicates` in routes or views.\n\nTo replace this functionality with supported mechanisms we've implemented a\ngeneric new-style route predicate class. To use this class in your routes you\nfirst have to configure it as described in [How to use](#how-to-use). Then in\nthe example in the previous section the view configs for route `parent_pk`\nshould change as follows:\n\n```python\n@view_config(route_name='parent_pk', request_method='GET', attr='get',\n convert_matchdict=(int, 'id'))\n@view_config(route_name='parent_pk', request_method='POST', attr='update',\n convert_matchdict=(int, 'id'))\n@view_config(route_name='parent_pk', request_method='DELETE', attr='delete',\n convert_matchdict=(int, 'id'))\n```\n\nPlease note that while in the old `custom_predicates` method the conversion of\nthe matchdict parameters was done at route level, the new-style route predicates\ndo not have access to the matchdict. Therefore we have to use view predicates to\nachieve the same.\n\nAfter these changes you no longer need the `int()` cast in the\n`identity_filter()` method. You'll also avoid the need to catch the `ValueError`\nexception.\n\n## CatchallPredicate\n\nThis is a supporting predicate to be used with [CatchallView](#catchallview). It\nassumes the route contains a fizzle parameter of the form `{catchall:.*}` (NOT\n`*catchall`}, since the star format creates an array of string values from the\nmatch) that is then parsed internally and converted to values better suited for the [CatchallView](#catchallview) class.\n\n## CatchallView\n\nThis is an extension of the [CrudView](#crudview) class that adds support for a\nfar richer route format based on internal parsing done by the [CatchallPredicate](#catchallpredicate) and has the ability to:\n- expose multiple entity types in a single place\n- offer arbitrary eager loading depth, as specified in the route's loading hints\n- drill into both dynamic and static relationships\n- offer slice syntax for easier pagination\n\nTo use this class:\n\n```python\n# setup route\nconfig.add_route(\"catchall\", '{catchall:.*}')\n\n# declare the class\n\n@view_defaults(renderer='json', catchall={\n 'parent': Parent,\n 'child': Child\n})\n@view_config(route_name=\"catchall\", attr='process')\nclass MyCatchallView(CatchallView):\n pass\n```\n\nThis code is enough to expose routes such as: \n- `GET /parent` or `GET /child` to list all parents or children\n- `GET /parent@1` or `GET /child@1` to get parent with id=1 or child with id=1\n- `POST /parent` or `POST /child` to add a new parent\n- `POST /parent@1` to update properties for parent with id=1\n- `DELETE /parent@1`, `DELETE /child@1` to delete parent with id=1 or child with\n id=1\n\nIn other words, both entity types `Parent` and `Child` are accessible from a\nsingle point. \n\n### Hints syntax\n\nHowever from your application's perspective alllowing access to\n`Child` at the root level might not be something useful, in other words you\nmight want your API to regard `Child` as tightly bound to `Parent`. CatchallView\nallows you to get a parent entity and all children attached in one go using `GET\n/parent@1:*children`. The CatchallView will see the portion of the route coming\nafter the column character as a list of loading hints for `Parent` entity. In\nthis case, it attaches a `selectinload(Parent.children)` option to the query.\n\nThe hints will also allow you to hide properties that might be too large, by\ndeferring them. I.e. if you added a `blob` property on `Parent` and the caller\nmight want to avoid retrieving it, they could call `GET /parent@1:-blob`.\nConversely, if the `blob` property is mared as a deferred column in the model\ndeclaration but the caller would want it included in the response they can\nundefer it by calling `GET /parent@1:+blob`.\n\nIf we also added a `blob` column for the `Child` entity (let's assume it's a\ndeferred column in the code), the caller can get a parent with all children\nincluding the blob for each by calling `GET /parent@1:*children(+blob)`.\nMultiple hints can be provided by comma separating them. This is also the case\nfor relationship hints: \n- `GET /parent@1:-blob,*children` means \"load `Parent` with all `children`\n included and defer loading the column `Parent.blob`\".\n- `GET /parent@1:-blob,*children(+blob,-blob2,*second_parent)` means \"load `Parent`\n with all `children` included, defer column `Parent.blob` and `Child.blob2` and\n undefer column `Child.blob`. For each child also load the relationship `Child.second_parent`.\n\nThe hints can have arbitrary depth. Each relationship hint can have hints\nreferring to the entities of that relationship.\n\nHints are also applicable to listing requests: `GET /parent:*children` will\neffectively retrieve all parents and all associated children.\n\nPlease note: dynamic relationship properties cannot be the target of a\nrelationship hint.\n\n### Drilldown support\n\nIf the caller wanted to retrieve just the children of a parent of known id they\ncould call `GET /parent@1/children`. The last bit of the route is not a hint,\nit's a drilldown specifier. This constructs a query that retrieves all children\nfor parent with id=1, by reading the foreign key constraint of relationship\n`Parent.children`.\n\nThe drilldown supports both normal relationship properties as well as dynamic\nrelationship properties. It automatically determines if the target property is a\nlist or a single entity (i.e. `GET /child@1/parent` also works). All hints\nprovided must come after the drilldown specifier and they will refer to the\nentities in the relationship being drilled down into. For example in the request\n`GET /parent@1/children:+blob` the hint will defer loading of column\n`Child.blob`.\n\nIf the property being drilled into is a collection all [Filtering, sorting and\npagination](#filtering-sorting-pagination) considerations apply. \n\n### Single element from collection\n\nIf the request either refers to a collection property via\n[Drilldown](#drilldown-support) or refers to a collection of entities because it\ndoes not contain a primary key specifier the caller can select a single item\nfrom the list by using subscript notation. For example, `GET\n/parent@1/children[0]` will retrieve the first child of the `Parent.children`\ncollection. [Filtering and sorting](#filtering-sorting-pagination) are applied\nfirst.\n\n### Filtering, sorting, pagination\n\nFiltering, sorting and pagination are applied as described in the\n[CRUDView](#crudview) section. Only `auto_filters` and `auto_order` are used.\nSupport for custom expressions is upcoming.\n\nPagination as supported by [CRUDView](#crudview) is also supported however the\nsame subscript notation as described in the previous section can be used for\nslicing: `GET /parent[0:10]?order_by=data+desc` retrieves the first 10 `Parent`\nentities in descending `data` order.\n\n## JsonGuardProvider\n\nFor security considerations the flexibility offered by this library can be\ndetrimental. Model classes can contain references to entities that need to be\nprotected from the API, both in terms of reading them (when using\n[CatchallView](#catchallview)) and in terms of updating them (concerns [any\ninsert/update method](#monkeypatch-objapplychanges)).\n\nThe `JsonGuardProvider` interface allows you to add security fencing for four\nareas:\n- method `guardSerialize` allows you to control how much information gets\n serialized to JSON\n- method `guardUpdate` allows you to control what can be written into the\n entities whenever `obj.apply_changes()` get called\n- method `guardHints` allows you to control what [CatchallView\n hints](#hints-syntax) are permitted\n- method `guardDrilldown` allows you to control what properties can be \n- [drilled down](#drilldown-support) into via `CatchallView`\n\nTo use a `JsonGuardProvider` implement this interface in a Pyramid\n[context](https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/urldispatch.html#route-factories)\nand attach it to the route and view using `add_route`'s `factory`.\n\nFor example:\n\n```python\nfrom py_liant.interfaces import JsonGuardProvider\n\nclass MyContext(JsonGuardProvider):\n request = None\n\n # provide some ACLs, for use with ACLAutorizationPolicy\n def __acl__(self):\n # let's assume any authenticated user should have read access\n if self.request.method == 'GET':\n return [(Allow, Authenticated, \"process\")]\n # if request verb is POST or DELETE, require admin role\n return [(Allow, \"role:admin\", \"process\")]\n\n def __init__(self, request):\n # we need to look at the request in the implementation\n self.request = request\n\n def guardSerialize(self, obj, value):\n # always hide Child.second_parent\n if isinstance(obj, Child) and 'second_parent' in value:\n # do NOT modify obj, just change value (JsonObject)\n del value.second_parent\n\n def guardUpdate(self, obj, data, for_update=True):\n # apply custom changes to the input data, for example encrypt passwords\n if isinstance(obj, User) and 'password' in data:\n # for example passwords can be encrypted\n data.password = hash(data.password)\n\n # or prevent certain properties being written into by the update\n if isinstance(obj, Parent):\n if 'property' in data:\n del data.property\n\n # or apply mandatory changes to certain objects\n # TrackedInstanceMixin could be a mixin that adds 'added' and \n # 'last_updated' columns to entities\n if isinstance(obj, TrackedInstanceMixin):\n # for_update is set to true when obj is newly instantiated\n if not for_update:\n obj.added = datetime.now(timezone.utc)\n obj.last_updated = datetime.now(timezone.utc)\n\n # if returning falsey value processing for this entity and all \n # descendants is prevented\n return True\n\n def guardHints(self, cls, hints):\n # maniupate the hints provided by the caller\n\n # e.g. remove any hint for Parent.data\n if cls is Parent and Parent.data in hints:\n del hints[Parent.data]\n\n # or add default hints for certain classes\n if cls is Child and Child.data not in hints:\n hints[Child.data] = ('-', None)\n\n if cls is Child and Child.parent not in hints:\n hints[Child.parent] = ('*', [('+', Parent.blob)])\n\n def guardDrilldown(self, prop) -> bool:\n if prop is Parent.children:\n return False\n return True\n\n# change the rotue definition to include context factory\nconfig.add_route(\"catchall\", '{catchall:.*}', factory=MyContext)\n```\n\n## SearchPathSetter\n\nThis is a PostgreSQL specific addition that can be used to set up the schema\nsearch path for all newly created database connection. It's implemented as a\nSQLAlchemy `PoolListener` (deprecated since version 0.7). A replacement that\nuses the modern events API is currently in the works.\n\nIt is very unlikely you will need to use this class in your project unless you\nneed to use multi-tenant databases with configurable schemas.\n\n## EnumAttrs and PythonEnum\n\n`PythonEnum` is a custom implementation of `sqlalchemy.types.Enum` that is\nuseful in PostgreSQL for declaring named enum types.\n\nUsage:\n\n```python\nfrom enum import Enum\nfrom py_liant.enum import EnumAttrs, PythonEnum\n\n# in PostgreSQL this will generate:\n# CREATE TYPE user_type AS ENUM ('admin', 'operator', 'user')\n@EnumAttrs('user_type')\nclass user_type(Enum):\n admin = 'admin'\n operator = 'oeprator'\n user = 'user'\n\nclass User(Base):\n __tablename__ = 'parent'\n id = Column(Integer, primary_key=True)\n name = Column(Text)\n user_type = Column(PythonEnum(user_type))\n```\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/georgebarbarosie/py-liant", "keywords": "", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "py-liant", "package_url": "https://pypi.org/project/py-liant/", "platform": "", "project_url": "https://pypi.org/project/py-liant/", "project_urls": { "Homepage": "https://github.com/georgebarbarosie/py-liant" }, "release_url": "https://pypi.org/project/py-liant/0.6.0/", "requires_dist": [ "pyramid", "SQLAlchemy", "simplejson", "isodate", "python-dateutil", "transaction", "pyparsing" ], "requires_python": ">=3.4.0", "summary": "Glue together pyramid, sqlalchemy, simplejson to provide a read-write, object-graph-aware JSON API", "version": "0.6.0" }, "last_serial": 5687835, "releases": { "0.6.0": [ { "comment_text": "", "digests": { "md5": "469bed59c0aed15a75b46c61b0975e0b", "sha256": "a38c0f481cb3edf8aca830a51468a25a5305ff8debe9f23c21b2b6790e3bcb65" }, "downloads": -1, "filename": "py_liant-0.6.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "469bed59c0aed15a75b46c61b0975e0b", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": ">=3.4.0", "size": 25532, "upload_time": "2019-08-16T14:03:43", "url": "https://files.pythonhosted.org/packages/35/4e/b3eddb012062c92075e97dd16bb661f54e81e33bef490d1fe0ced286c5e9/py_liant-0.6.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "8a3468842440bde9c24bd52d9529f180", "sha256": "87164bd271a45ef39926e78a85c8c86bb48f87ed6f1c774fabecb578619ee822" }, "downloads": -1, "filename": "py_liant-0.6.0.tar.gz", "has_sig": false, "md5_digest": "8a3468842440bde9c24bd52d9529f180", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.4.0", "size": 37489, "upload_time": "2019-08-16T14:03:45", "url": "https://files.pythonhosted.org/packages/40/d8/34998d092104ab04791dfcd5d1f9be61a9a524b64c18a11d43871f0612d7/py_liant-0.6.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "469bed59c0aed15a75b46c61b0975e0b", "sha256": "a38c0f481cb3edf8aca830a51468a25a5305ff8debe9f23c21b2b6790e3bcb65" }, "downloads": -1, "filename": "py_liant-0.6.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "469bed59c0aed15a75b46c61b0975e0b", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": ">=3.4.0", "size": 25532, "upload_time": "2019-08-16T14:03:43", "url": "https://files.pythonhosted.org/packages/35/4e/b3eddb012062c92075e97dd16bb661f54e81e33bef490d1fe0ced286c5e9/py_liant-0.6.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "8a3468842440bde9c24bd52d9529f180", "sha256": "87164bd271a45ef39926e78a85c8c86bb48f87ed6f1c774fabecb578619ee822" }, "downloads": -1, "filename": "py_liant-0.6.0.tar.gz", "has_sig": false, "md5_digest": "8a3468842440bde9c24bd52d9529f180", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.4.0", "size": 37489, "upload_time": "2019-08-16T14:03:45", "url": "https://files.pythonhosted.org/packages/40/d8/34998d092104ab04791dfcd5d1f9be61a9a524b64c18a11d43871f0612d7/py_liant-0.6.0.tar.gz" } ] }