├── MANIFEST.in ├── .gitignore ├── neoapi ├── __init__.py ├── http_error_codes.py ├── errors.py ├── application_codes.py └── serializable_structured_node.py ├── setup.cfg ├── Changelog ├── Overview.md ├── README.md ├── LICENSE ├── setup.py └── Examples.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include Changelog 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | doc/build 3 | dist 4 | *.egg-info 5 | *.swp 6 | *.swn 7 | *.swo 8 | development.env 9 | *.pyc 10 | .idea 11 | .python-version 12 | .ropeproject -------------------------------------------------------------------------------- /neoapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .serializable_structured_node import * 2 | 3 | __author__ = 'Max Buck' 4 | __email__ = 'maxbuckdeveloper@gmail.com' 5 | __license__ = 'MIT' 6 | __package__ = 'neoapi' 7 | __version__ = '2.0.1' -------------------------------------------------------------------------------- /neoapi/http_error_codes.py: -------------------------------------------------------------------------------- 1 | OK = 200 2 | CREATED = 201 3 | NO_CONTENT = 204 4 | BAD_REQUEST = 400 5 | UNAUTHORIZED = 401 6 | FORBIDDEN = 403 7 | NOT_FOUND = 404 8 | NOT_ALLOWED = 405 9 | CONFLICT = 409 10 | INTERNAL_SERVER_ERROR = 500 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 -------------------------------------------------------------------------------- /neoapi/errors.py: -------------------------------------------------------------------------------- 1 | class WrongTypeError(Exception): 2 | 3 | def __init__(self, value='wrong type error'): 4 | self.value = value 5 | 6 | def __str__(self): 7 | return repr(self.value) 8 | 9 | 10 | class ParameterNotSupported(Exception): 11 | def __init__(self, value='wrong type error'): 12 | self.value = value 13 | 14 | def __str__(self): 15 | return repr(self.value) 16 | 17 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | Version 2.0.1 2015-08-17 2 | * Small bug fix. When hashed values are part of a model, but not included in json representation errors were occurring 3 | 4 | Version 2.0.0 2015-09-27 5 | * Some Changes that cause old model format to not work 6 | * This version is significantly more compliant and functional than the last 7 | 8 | Version 1.0.0 2015-09-15 9 | * This is the first stable version 10 | 11 | Version 0.1.4 2015-09-15 12 | * fixed bad syntax in import statements 13 | 14 | Version 0.1.3 2015-09-15 15 | * fixed bad syntax in import statements 16 | 17 | Version 0.1.2 2015-09-15 18 | * fixed bad syntax in import statments 19 | 20 | Version 0.1.1 2015-09-15 21 | * removed requirement for hashlib because python 2.5+ has it built in 22 | 23 | Version 0.1.0 2015-09-15 24 | * first version -------------------------------------------------------------------------------- /Overview.md: -------------------------------------------------------------------------------- 1 | # Overview of Public Methods 2 | 3 | This serves as a quick overview of the public methods specific to SerializableStructuredNode for the latest version. All 4 | NeoModel methods are still present. To create a 'magic' api, simply make the correct models -- see the 5 | [sample project](https://github.com/buckmaxwell/sample-neo-api), and then create an endpoint that returns model.method() 6 | for the correct HTTP verb. It's that easy. 7 | 8 | 9 | ## Resource Methods 10 | *deprecated methods not listed* 11 | 12 | #### CRUD 13 | * create_resource 14 | * POST 15 | * update_resource 16 | * PATCH 17 | * deactivate_resource 18 | * DELETE 19 | 20 | #### Fetching 21 | * get_resource 22 | * get_collection 23 | 24 | ## Relationship methods 25 | *deprecated methods not listed* 26 | 27 | #### CRUD 28 | * create_relationship -- *Many* 29 | * POST 30 | * update_relationship -- *One or Many* 31 | * PATCH 32 | * disconnect_relationship -- *Many* 33 | * DELETE 34 | 35 | #### Fetching 36 | * get_relationship -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NeoAPI - the perfect tool for building python APIs with neo4j 2 | 3 | 4 | What you can do: 5 | 6 | * Create powerful APIs that conform to the json api specification in minutes 7 | * Leverage the power of the neomodel OGM and py2neo to create beautiful models with great functionality. 8 | 9 | 10 | ## Installation 11 | 12 | ```sh 13 | $ pip install neoapi 14 | ``` 15 | Thats all! 16 | 17 | ## Getting Started 18 | 19 | To get started with NeoAPI it makes sense to familiarize yourself with 20 | [NeoModel](http://neomodel.readthedocs.org/en/latest/) and [the json api specification](http://jsonapi.org/). These are the two technologies NeoAPI is built on. 21 | 22 | Chances are though, you want to get started right now!! If that's the case, please check out our [sample project](https://github.com/buckmaxwell/sample-neo-api), Overview.md, and Examples.md. Together those resources should be the jetpack you need. 23 | 24 | If you get stuck, do the reading I suggested above, or drop me an email at maxbuckdeveloper@gmail.com, I'm always happy 25 | to help. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2015 Max Buck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /neoapi/application_codes.py: -------------------------------------------------------------------------------- 1 | from http_error_codes import (OK, CREATED, BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, 2 | CONFLICT, INTERNAL_SERVER_ERROR, NOT_ALLOWED) 3 | from flask import jsonify, make_response 4 | 5 | BAD_FORMAT_VIOLATION = '4000', 'there is a problem with the request format', BAD_REQUEST 6 | UNIQUE_KEY_VIOLATION = '4001', 'a uniqueness constraint is violated by the request', BAD_REQUEST 7 | WRONG_TYPE_VIOLATION = '4002', 'the type key does not match the resource requested', BAD_REQUEST 8 | PARAMETER_NOT_SUPPORTED_VIOLATION = '4003', 'a query parameter you tried to use is not supported for this endpoint', BAD_REQUEST 9 | ENUMERATED_TYPE_VIOLATION = '4004', 'a value given for an enumerated type was unsupported', BAD_REQUEST 10 | BAD_PARAMETER_VIOLATION = '4005', 'one or more of the attributes in your request is not part of the model', BAD_REQUEST 11 | ATTEMPTED_CARDINALITY_VIOLATION = '4006', 'you tried to do something that would violate a cardinality constraint', BAD_REQUEST 12 | 13 | RESOURCE_NOT_FOUND = '4040', 'the requested resource was not found on the server', NOT_FOUND 14 | 15 | METHOD_NOT_ALLOWED = '4050', 'the http method you tried is not a legal operation on this resource', NOT_ALLOWED 16 | FORBIDDEN_VIOLATION = '4030', 'the http method you tried is forbidden.', FORBIDDEN 17 | UNAUTHORIZED_VIOLATION = '4010', 'missing or bad authorization', UNAUTHORIZED 18 | BAD_AUTHENTICATION = '4011', 'bad authorization', UNAUTHORIZED 19 | NO_AUTHENTICATION = '4012', 'no authorization header provided', UNAUTHORIZED 20 | 21 | INTERNAL_SERVER_ERROR_VIOLATION = '5000', 'internal server error', INTERNAL_SERVER_ERROR 22 | MULTIPLE_NODES_WITH_ID_VIOLATION = '5001', 'multiple nodes with the same id', INTERNAL_SERVER_ERROR_VIOLATION 23 | 24 | 25 | def error_response(array_of_application_code_tuples): 26 | errors = list() 27 | for app_code_tuple in array_of_application_code_tuples: 28 | error = dict() 29 | error['status'] = app_code_tuple[2] 30 | error['code'] = app_code_tuple[0] 31 | error['title'] = app_code_tuple[1] 32 | errors.append(error) 33 | 34 | r = make_response(jsonify({'errors': errors})) 35 | r.status_code = app_code_tuple[2] 36 | r.headers['Content-Type'] = "application/vnd.api+json; charset=utf-8" 37 | 38 | return r 39 | 40 | 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Always prefer setuptools over distutils 2 | from setuptools import setup, find_packages 3 | 4 | 5 | setup( 6 | name='neoapi', 7 | 8 | # Versions should comply with PEP440. For a discussion on single-sourcing 9 | # the version across setup.py and the project code, see 10 | # https://packaging.python.org/en/latest/single_source_version.html 11 | version='2.0.1', 12 | 13 | description='A package for serializing json api compliant responses from neomodel StructuredNodes', 14 | 15 | # The project's main homepage. 16 | url='https://github.com/buckmaxwell/neoapi', 17 | 18 | # Author details 19 | author='Max Buck', 20 | author_email='maxbuckdeveloper@gmail.com', 21 | 22 | # Choose your license 23 | license='MIT', 24 | 25 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 26 | classifiers=[ 27 | # How mature is this project? Common values are 28 | # 3 - Alpha 29 | # 4 - Beta 30 | # 5 - Production/Stable 31 | 'Development Status :: 3 - Alpha', 32 | 33 | # Indicate who your project is intended for 34 | 'Intended Audience :: Developers', 35 | 'Topic :: Software Development :: Build Tools', 36 | 37 | # Pick your license as you wish (should match "license" above) 38 | 'License :: OSI Approved :: MIT License', 39 | 40 | # Specify the Python versions you support here. In particular, ensure 41 | # that you indicate whether you support Python 2, Python 3 or both. 42 | 'Programming Language :: Python :: 2', 43 | 'Programming Language :: Python :: 2.6', 44 | 'Programming Language :: Python :: 2.7', 45 | 'Programming Language :: Python :: 3', 46 | 'Programming Language :: Python :: 3.2', 47 | 'Programming Language :: Python :: 3.3', 48 | 'Programming Language :: Python :: 3.4', 49 | ], 50 | 51 | # What does your project relate to? 52 | keywords='json api specification neomodel neo4j', 53 | 54 | # You can just specify the packages manually here if your project is 55 | # simple. Or you can use find_packages(). 56 | packages=find_packages(exclude=['contrib', 'docs', 'tests*']), 57 | 58 | # List run-time dependencies here. These will be installed by pip when 59 | # your project is installed. For an analysis of "install_requires" vs pip's 60 | # requirements files see: 61 | # https://packaging.python.org/en/latest/requirements.html 62 | install_requires=['neomodel', 'py2neo', 'flask'], 63 | 64 | # If there are data files included in your packages that need to be 65 | # installed, specify them here. If using Python 2.6 or less, then these 66 | # have to be included in MANIFEST.in as well. 67 | include_package_data=True 68 | 69 | ) -------------------------------------------------------------------------------- /Examples.md: -------------------------------------------------------------------------------- 1 | ## Resource Methods 2 | 3 | ### POST /users 4 | 5 | Add a new resource. 6 | 7 | URL: http://localhost:10200/users 8 | 9 | Request: 10 | 11 | ```http 12 | Accept: application/vnd.api+json 13 | ``` 14 | 15 | ```json 16 | { 17 | "data": { 18 | "type": "users", 19 | "attributes": { 20 | "email": "ryan@gmail.com", 21 | "password": "ryan", 22 | "gender": "m" 23 | }, 24 | "relationships": { 25 | "mom": { 26 | "data": { 27 | "type": "users", 28 | "id": "sarah@gmail.com" 29 | } 30 | } 31 | } 32 | } 33 | } 34 | ``` 35 | 36 | Response: 37 | ##### 201 CREATED 38 | ```http 39 | Content-Type: application/vnd.api+json; charset=utf-8 40 | Content-Length: 992 41 | Location: http://localhost:10200/v1/users/maxbuckdeveloper@gmail.com 42 | Date: Tue, 15 Sep 2015 04:31:28 GMT 43 | ``` 44 | 45 | ```json 46 | { 47 | "data": { 48 | "attributes": { 49 | "active": true, 50 | "created": "Sun, 27 Sep 2015 16:54:19 GMT", 51 | "email": "ryan@gmail.com", 52 | "gender": "m", 53 | "id": "ryan@gmail.com", 54 | "type": "users", 55 | "updated": "Sun, 27 Sep 2015 16:54:19 GMT" 56 | }, 57 | "id": "ryan@gmail.com", 58 | "relationships": { 59 | "friends": { 60 | "data": [], 61 | "links": { 62 | "related": "http://localhost:10200/v1/users/ryan@gmail.com/friends", 63 | "self": "http://localhost:10200/v1/users/ryan@gmail.com/relationships/friends" 64 | } 65 | }, 66 | "mom": { 67 | "data": { 68 | "id": "sarah@gmail.com", 69 | "type": "users" 70 | }, 71 | "links": { 72 | "related": "http://localhost:10200/v1/users/ryan@gmail.com/mom", 73 | "self": "http://localhost:10200/v1/users/ryan@gmail.com/relationships/mom" 74 | } 75 | } 76 | }, 77 | "type": "users" 78 | }, 79 | "links": { 80 | "self": "http://localhost:10200/v1/users/ryan@gmail.com" 81 | } 82 | } 83 | ``` 84 | 85 | ### PATCH /users/< id > 86 | 87 | Modify an existing resource. 88 | 89 | URL: http://localhost:10200/users/ryan@gmail.com 90 | 91 | Request: 92 | 93 | ```http 94 | Accept: application/vnd.api+json 95 | ``` 96 | ```json 97 | { 98 | "data": { 99 | "type": "users", 100 | "attributes": { 101 | "password": "muchHarderPasswordToGuess" 102 | } 103 | } 104 | } 105 | ``` 106 | 107 | Response: 108 | ##### 200 OK 109 | ```http 110 | Content-Type: application/vnd.api+json; charset=utf-8 111 | Content-Length: 992 112 | Location: http://localhost:10200/v1/users/maxbuckdeveloper@gmail.com 113 | Date: Tue, 15 Sep 2015 04:31:28 GMT 114 | ``` 115 | 116 | ```json 117 | { 118 | "data": { 119 | "attributes": { 120 | "active": true, 121 | "created": "Sun, 27 Sep 2015 16:54:19 GMT", 122 | "email": "ryan@gmail.com", 123 | "gender": "m", 124 | "id": "ryan@gmail.com", 125 | "type": "users", 126 | "updated": "Sun, 27 Sep 2015 17:18:38 GMT" 127 | }, 128 | "id": "ryan@gmail.com", 129 | "relationships": { 130 | "friends": { 131 | "data": [], 132 | "links": { 133 | "related": "http://localhost:10200/v1/users/ryan@gmail.com/friends", 134 | "self": "http://localhost:10200/v1/users/ryan@gmail.com/relationships/friends" 135 | } 136 | }, 137 | "mom": { 138 | "data": { 139 | "id": "sarah@gmail.com", 140 | "type": "users" 141 | }, 142 | "links": { 143 | "related": "http://localhost:10200/v1/users/ryan@gmail.com/mom", 144 | "self": "http://localhost:10200/v1/users/ryan@gmail.com/relationships/mom" 145 | } 146 | } 147 | }, 148 | "type": "users" 149 | }, 150 | "links": { 151 | "self": "http://localhost:10200/v1/users/ryan@gmail.com" 152 | } 153 | } 154 | ``` 155 | 156 | ### DELETE /users/< id > 157 | 158 | Deactivate an existing resource. 159 | 160 | URL: http://localhost:10200/users/maxbuckdeveloper@gmail.com 161 | 162 | Request: 163 | 164 | ```http 165 | Accept: application/vnd.api+json 166 | ``` 167 | 168 | Response: 169 | ##### 204 NO CONTENT 170 | ```http 171 | Content-Type: application/vnd.api+json; charset=utf-8 172 | Content-Length: 0 173 | Date: Tue, 15 Sep 2015 04:31:28 GMT 174 | ``` 175 | 176 | ### GET /users/\ 177 | 178 | Fetch an existing resource. 179 | 180 | URL: http://localhost:10200/users/ryan@gmail.com?include=mom* 181 | 182 | **More info on the **include** parameter can be found [here](http://jsonapi.org/format/#fetching-includes)* 183 | 184 | Request: 185 | 186 | ```http 187 | Accept: application/vnd.api+json 188 | ``` 189 | 190 | Response: 191 | ##### 200 OK 192 | ```http 193 | Content-Type: application/vnd.api+json; charset=utf-8 194 | Content-Length: 1050 195 | Date: Tue, 15 Sep 2015 04:31:28 GMT 196 | ``` 197 | ```json 198 | { 199 | "data": { 200 | "attributes": { 201 | "active": true, 202 | "created": "Sun, 27 Sep 2015 16:54:19 GMT", 203 | "email": "ryan@gmail.com", 204 | "gender": "m", 205 | "id": "ryan@gmail.com", 206 | "type": "users", 207 | "updated": "Sun, 27 Sep 2015 17:18:38 GMT" 208 | }, 209 | "id": "ryan@gmail.com", 210 | "relationships": { 211 | "friends": { 212 | "data": [], 213 | "links": { 214 | "related": "http://localhost:10200/v1/users/ryan@gmail.com/friends", 215 | "self": "http://localhost:10200/v1/users/ryan@gmail.com/relationships/friends" 216 | } 217 | }, 218 | "mom": { 219 | "data": { 220 | "id": "sarah@gmail.com", 221 | "type": "users" 222 | }, 223 | "links": { 224 | "related": "http://localhost:10200/v1/users/ryan@gmail.com/mom", 225 | "self": "http://localhost:10200/v1/users/ryan@gmail.com/relationships/mom" 226 | } 227 | } 228 | }, 229 | "type": "users" 230 | }, 231 | "included": [ 232 | { 233 | "attributes": { 234 | "active": true, 235 | "created": "Sun, 27 Sep 2015 16:54:19 GMT", 236 | "email": "sarah@gmail.com", 237 | "gender": "f", 238 | "id": "sarah@gmail.com", 239 | "type": "users", 240 | "updated": "Sun, 27 Sep 2015 16:54:19 GMT" 241 | }, 242 | "id": "sarah@gmail.com", 243 | "relationships": { 244 | "friends": { 245 | "data": [], 246 | "links": { 247 | "related": "http://localhost:10200/v1/users/sarah@gmail.com/friends", 248 | "self": "http://localhost:10200/v1/users/sarah@gmail.com/relationships/friends" 249 | } 250 | }, 251 | "mom": { 252 | "data": null, 253 | "links": { 254 | "related": "http://localhost:10200/v1/users/sarah@gmail.com/mom", 255 | "self": "http://localhost:10200/v1/users/sarah@gmail.com/relationships/mom" 256 | } 257 | } 258 | }, 259 | "type": "users" 260 | } 261 | ], 262 | "links": { 263 | "self": "http://localhost:10200/v1/users/ryan@gmail.com" 264 | } 265 | } 266 | ``` 267 | 268 | ### GET /users 269 | 270 | Fetch a resource collection 271 | 272 | URL: http://localhost:10200/users?page[offset]=0&page[limit]=1* 273 | 274 | **More info on **pagination** can be found [here](http://jsonapi.org/format/#fetching-pagination)* 275 | 276 | Request: 277 | 278 | ```http 279 | Accept: application/vnd.api+json 280 | ``` 281 | 282 | Response: 283 | ##### 200 OK 284 | ```http 285 | Content-Type: application/vnd.api+json; charset=utf-8 286 | Content-Length: 1061 287 | Date: Tue, 15 Sep 2015 04:31:28 GMT 288 | ``` 289 | ```json 290 | { 291 | "data": [ 292 | { 293 | "attributes": { 294 | "active": true, 295 | "created": "Sun, 27 Sep 2015 16:54:19 GMT", 296 | "email": "max@gmail.com", 297 | "gender": "m", 298 | "id": "max@gmail.com", 299 | "type": "users", 300 | "updated": "Sun, 27 Sep 2015 16:54:19 GMT" 301 | }, 302 | "id": "max@gmail.com", 303 | "relationships": { 304 | "friends": { 305 | "data": [], 306 | "links": { 307 | "related": "http://localhost:10200/v1/users/max@gmail.com/friends", 308 | "self": "http://localhost:10200/v1/users/max@gmail.com/relationships/friends" 309 | } 310 | }, 311 | "mom": { 312 | "data": null, 313 | "links": { 314 | "related": "http://localhost:10200/v1/users/max@gmail.com/mom", 315 | "self": "http://localhost:10200/v1/users/max@gmail.com/relationships/mom" 316 | } 317 | } 318 | }, 319 | "type": "users" 320 | } 321 | ], 322 | "links": { 323 | "first": "http://localhost:10200/v1/users?page[offset]=0&page[limit]=1", 324 | "last": "http://localhost:10200/v1/users?page[offset]=2&page[limit]=1", 325 | "next": "http://localhost:10200/v1/users?page[offset]=1&page[limit]=1", 326 | "self": "http://localhost:10200/v1/users?page[offset]=0&page[limit]=1" 327 | } 328 | } 329 | ``` 330 | ## Relationship Methods 331 | 332 | ### POST /users/\/relationships/friends* 333 | 334 | Add an item to a relationship collection. 335 | 336 | ****Note:** use POST for collections with MANY items, otherwise use PATCH* 337 | 338 | 339 | URL: http://localhost:10200/users/ryan@gmail.com/relationships/friends 340 | 341 | Request: 342 | 343 | ```http 344 | Accept: application/vnd.api+json 345 | ``` 346 | 347 | ```json 348 | { 349 | "data": [ 350 | { 351 | "type": "users", 352 | "id": "max@gmail.com", 353 | "meta": { 354 | "met": "space camp" 355 | } 356 | } 357 | ] 358 | } 359 | ``` 360 | Response: 361 | ##### 204 NO CONTENT 362 | ```http 363 | Content-Type: application/vnd.api+json; charset=utf-8 364 | Content-Length: 0 365 | Date: Tue, 15 Sep 2015 04:31:28 GMT 366 | ``` 367 | 368 | ### PATCH /users/\/relationships/mom 369 | 370 | This method does a total replace on a relationship. It should therefore be used to create relationships with cardinality <= 1. It can also be used to delete all relationships. 371 | 372 | 373 | URL: http://localhost:10200/v1/users/ryan@gmail.com/relationships/mom 374 | 375 | Request to create: 376 | ```http 377 | Accept: application/vnd.api+json 378 | ``` 379 | 380 | ```json 381 | { 382 | "data":{"type":"users", "id":"sarah@gmail.com"} 383 | } 384 | ``` 385 | 386 | Request to delete: 387 | ```http 388 | Accept: application/vnd.api+json 389 | ``` 390 | ```json 391 | { 392 | "data":null 393 | } 394 | ``` 395 | 396 | Response: 397 | ##### 204 NO CONTENT 398 | ```http 399 | Content-Type: application/vnd.api+json; charset=utf-8 400 | Content-Length: 0 401 | Date: Tue, 15 Sep 2015 04:31:28 GMT 402 | ``` 403 | 404 | ### DELETE /users/\/relationships/friends 405 | 406 | Use this method to delete certain relationships contained in the body. Only the relationships in the body will be deleted. This method is only for collections with cardinality > 1. 407 | 408 | URL: http://localhost:10200/v1/users/ryan@gmail.com/relationships/friends 409 | 410 | Request: 411 | ```http 412 | Accept: application/vnd.api+json 413 | ``` 414 | 415 | ```json 416 | { 417 | "data": [ 418 | { 419 | "type": "users", 420 | "id": "max@gmail.com" 421 | } 422 | ] 423 | } 424 | ``` 425 | 426 | Response: 427 | ##### 204 NO CONTENT 428 | ```http 429 | Content-Type: application/vnd.api+json; charset=utf-8 430 | Content-Length: 0 431 | Date: Tue, 15 Sep 2015 04:31:28 GMT 432 | ``` 433 | 434 | ### PATCH /users/\/relationships/friends 435 | 436 | On collections with cardinality > 1 use this method to delete all existing items in that collection. 437 | 438 | URL: http://localhost:10200/v1/users/ryan@gmail.com/relationships/friends 439 | 440 | Request: 441 | ```http 442 | Accept: application/vnd.api+json 443 | ``` 444 | ```json 445 | { 446 | "data": [] 447 | } 448 | ``` 449 | 450 | Response: 451 | ##### 204 NO CONTENT 452 | ```http 453 | Content-Type: application/vnd.api+json; charset=utf-8 454 | Content-Length: 0 455 | Date: Tue, 15 Sep 2015 04:31:28 GMT 456 | ``` 457 | 458 | ### GET /users/\/relationships/friends 459 | 460 | Fetch a relationship. 461 | 462 | URL: http://localhost:10200/v1/users/max@gmail.com/relationships/friends 463 | 464 | Request: 465 | ```http 466 | Accept: application/vnd.api+json 467 | ``` 468 | 469 | ```json 470 | { 471 | "data": [ 472 | { 473 | "id": "ben@gmail.com", 474 | "meta": { 475 | "created": "Sun, 27 Sep 2015 18:16:47 GMT", 476 | "met": "space camp", 477 | "type": "friend", 478 | "updated": "Sun, 27 Sep 2015 18:16:47 GMT" 479 | }, 480 | "type": "users" 481 | }, 482 | { 483 | "id": "erik@gmail.com", 484 | "meta": { 485 | "created": "Sun, 27 Sep 2015 18:16:47 GMT", 486 | "met": "taco tuesday party", 487 | "type": "friend", 488 | "updated": "Sun, 27 Sep 2015 18:16:47 GMT" 489 | }, 490 | "type": "users" 491 | }, 492 | { 493 | "id": "ryan@gmail.com", 494 | "meta": { 495 | "created": "Sun, 27 Sep 2015 18:16:47 GMT", 496 | "met": "at atheist revival", 497 | "type": "friend", 498 | "updated": "Sun, 27 Sep 2015 18:16:47 GMT" 499 | }, 500 | "type": "users" 501 | } 502 | ], 503 | "included": [ 504 | { 505 | "attributes": { 506 | "active": true, 507 | "created": "Sun, 27 Sep 2015 18:16:47 GMT", 508 | "email": "ben@gmail.com", 509 | "gender": "m", 510 | "id": "ben@gmail.com", 511 | "type": "users", 512 | "updated": "Sun, 27 Sep 2015 18:16:47 GMT" 513 | }, 514 | "id": "ben@gmail.com", 515 | "relationships": { 516 | "friends": { 517 | "data": [ 518 | { 519 | "id": "max@gmail.com", 520 | "type": "users" 521 | } 522 | ], 523 | "links": { 524 | "related": "http://localhost:10200/v1/users/ben@gmail.com/friends", 525 | "self": "http://localhost:10200/v1/users/ben@gmail.com/relationships/friends" 526 | } 527 | }, 528 | "mom": { 529 | "data": null, 530 | "links": { 531 | "related": "http://localhost:10200/v1/users/ben@gmail.com/mom", 532 | "self": "http://localhost:10200/v1/users/ben@gmail.com/relationships/mom" 533 | } 534 | } 535 | }, 536 | "type": "users" 537 | }, 538 | { 539 | "attributes": { 540 | "active": true, 541 | "created": "Sun, 27 Sep 2015 18:16:47 GMT", 542 | "email": "erik@gmail.com", 543 | "gender": "m", 544 | "id": "erik@gmail.com", 545 | "type": "users", 546 | "updated": "Sun, 27 Sep 2015 18:16:47 GMT" 547 | }, 548 | "id": "erik@gmail.com", 549 | "relationships": { 550 | "friends": { 551 | "data": [ 552 | { 553 | "id": "max@gmail.com", 554 | "type": "users" 555 | } 556 | ], 557 | "links": { 558 | "related": "http://localhost:10200/v1/users/erik@gmail.com/friends", 559 | "self": "http://localhost:10200/v1/users/erik@gmail.com/relationships/friends" 560 | } 561 | }, 562 | "mom": { 563 | "data": null, 564 | "links": { 565 | "related": "http://localhost:10200/v1/users/erik@gmail.com/mom", 566 | "self": "http://localhost:10200/v1/users/erik@gmail.com/relationships/mom" 567 | } 568 | } 569 | }, 570 | "type": "users" 571 | }, 572 | { 573 | "attributes": { 574 | "active": true, 575 | "created": "Sun, 27 Sep 2015 16:54:19 GMT", 576 | "email": "ryan@gmail.com", 577 | "gender": "m", 578 | "id": "ryan@gmail.com", 579 | "type": "users", 580 | "updated": "Sun, 27 Sep 2015 17:18:38 GMT" 581 | }, 582 | "id": "ryan@gmail.com", 583 | "relationships": { 584 | "friends": { 585 | "data": [ 586 | { 587 | "id": "max@gmail.com", 588 | "type": "users" 589 | } 590 | ], 591 | "links": { 592 | "related": "http://localhost:10200/v1/users/ryan@gmail.com/friends", 593 | "self": "http://localhost:10200/v1/users/ryan@gmail.com/relationships/friends" 594 | } 595 | }, 596 | "mom": { 597 | "data": null, 598 | "links": { 599 | "related": "http://localhost:10200/v1/users/ryan@gmail.com/mom", 600 | "self": "http://localhost:10200/v1/users/ryan@gmail.com/relationships/mom" 601 | } 602 | } 603 | }, 604 | "type": "users" 605 | } 606 | ], 607 | "links": { 608 | "first": "http://localhost:10200/v1/users/max@gmail.com/relationships/friends?page[offset]=0&page[limit]=20", 609 | "last": "http://localhost:10200/v1/users/max@gmail.com/relationships/friends?page[offset]=0&page[limit]=20", 610 | "related": "http://localhost:10200/v1/users/max@gmail.com/friends", 611 | "self": "http://localhost:10200/v1/users/max@gmail.com/relationships/friends?page[offset]=0&page[limit]=20" 612 | } 613 | } 614 | ``` 615 | -------------------------------------------------------------------------------- /neoapi/serializable_structured_node.py: -------------------------------------------------------------------------------- 1 | from neomodel import (Property, StructuredNode, StringProperty, DateProperty, AliasProperty, UniqueProperty, 2 | DateTimeProperty, RelationshipFrom, BooleanProperty, Relationship, DoesNotExist, ZeroOrOne, 3 | DeflateError, One, ZeroOrMore, OneOrMore, AttemptedCardinalityViolation, MultipleNodesReturned, 4 | StructuredRel) 5 | from py2neo.cypher.error.statement import ParameterMissing 6 | import os 7 | import http_error_codes 8 | from flask import jsonify, make_response 9 | from neomodel import db 10 | import application_codes 11 | from .errors import WrongTypeError, ParameterNotSupported 12 | from datetime import datetime 13 | import hashlib 14 | 15 | base_url = os.environ.get('BASE_API_URL', 'http://localhost:10200/v1') 16 | CONTENT_TYPE = "application/vnd.api+json; charset=utf-8" 17 | 18 | 19 | class SerializableStructuredNode(StructuredNode): 20 | """ 21 | This class extends NeoModel's StructuredNode class. It adds a series of functions in order to allow for \ 22 | creation of json responses that conform to the jsonapi specification found at http://jsonapi.org/ 23 | """ 24 | hashed = [] 25 | secret = [] 26 | dates = [] 27 | enums = dict() 28 | updated = DateTimeProperty(default=datetime.now()) 29 | created = DateTimeProperty(default=datetime.now()) 30 | active = BooleanProperty(default=True) 31 | type = StringProperty(default='serializable_structured_nodes') 32 | id = StringProperty(required=True, unique_index=True) 33 | 34 | def get_self_link(self): 35 | return '{base_url}/{type}/{id}'.format(base_url=base_url, type=self.type, id=self.id) 36 | 37 | @classmethod 38 | def get_class_link(cls): 39 | return '{base_url}/{type}'.format(base_url=base_url, type=cls.__type__) 40 | 41 | @classmethod 42 | def resource_collection_response(cls, offset=0, limit=20): 43 | """ 44 | This method is deprecated for version 1.1.0. Please use get_collection 45 | """ 46 | request_args = {'page[offset]': offset, 'page[limit]': limit} 47 | return cls.get_collection(request_args) 48 | 49 | def individual_resource_response(self, included=[]): 50 | data = dict() 51 | data['data'] = self.get_resource_object() 52 | data['links'] = {'self': self.get_self_link()} 53 | data['included'] = self.get_included_from_list(included) 54 | r = make_response(jsonify(data)) 55 | r.status_code = http_error_codes.OK 56 | r.headers['Content-Type'] = CONTENT_TYPE 57 | return r 58 | 59 | def get_path_resources(self, path): 60 | response = list() 61 | if path: 62 | nodes = eval('self.{part}.all()'.format(part=path[0])) 63 | for n in nodes: 64 | if n.get_resource_object() not in response: 65 | response.append(n.get_resource_object()) 66 | response += n.get_path_resources(path[1:]) 67 | 68 | return response 69 | 70 | def get_included_from_list(self, included): 71 | response = list() 72 | props = self.defined_properties() 73 | included = [x.split('.') for x in included] 74 | for attr_name in props.keys(): 75 | if not isinstance(props[attr_name], Property): # is attribute 76 | for path in included: 77 | if attr_name == path[0]: 78 | response += self.get_path_resources(path) 79 | 80 | return response 81 | 82 | def get_resource_object(self): 83 | response = dict() 84 | response['id'] = self.id 85 | response['type'] = self.type 86 | response['attributes'] = dict() 87 | response['relationships'] = dict() 88 | props = self.defined_properties() 89 | for attr_name in props.keys(): 90 | if isinstance(props[attr_name], Property): # is attribute 91 | if attr_name not in self.secret: 92 | response['attributes'][attr_name] = getattr(self, attr_name) 93 | 94 | else: # is relationship 95 | response['relationships'][attr_name] = dict() 96 | 97 | # links 98 | response['relationships'][attr_name]['links'] = { 99 | 'self': '{base_url}/{type}/{id}/relationships/{attr_name}'.format( 100 | base_url=base_url, 101 | type=self.type, 102 | id=self.id, 103 | attr_name=attr_name), 104 | 'related': '{base_url}/{type}/{id}/{attr_name}'.format( 105 | base_url=base_url, 106 | type=self.type, 107 | id=self.id, 108 | attr_name=attr_name) 109 | } 110 | 111 | # data 112 | related_node_or_nodes = eval('self.{attr_name}.all()'.format(attr_name=attr_name)) 113 | 114 | 115 | if not eval("type(self.{related_collection_type})".format(related_collection_type=attr_name)) == ZeroOrOne: 116 | response['relationships'][attr_name]['data'] = list() 117 | for the_node in related_node_or_nodes: 118 | if the_node.active: 119 | # TODO: Decide whether or not to include relationship meta info 120 | # x = getattr(self, attr_name) 121 | # rsrc_identifier = x.relationship(the_node).get_resource_identifier_object(the_node) 122 | rsrc_identifier = {'id': the_node.id, 'type': the_node.type} 123 | response['relationships'][attr_name]['data'].append(rsrc_identifier) 124 | elif related_node_or_nodes: 125 | the_node = related_node_or_nodes[0] 126 | # x = getattr(self, attr_name) 127 | # rsrc_identifier = x.relationship(the_node).get_resource_identifier_object(the_node) 128 | rsrc_identifier = {'type': the_node.type, 'id': the_node.id} 129 | response['relationships'][attr_name]['data'] = rsrc_identifier 130 | else: 131 | response['relationships'][attr_name]['data'] = None 132 | 133 | return response 134 | 135 | def relationship_collection_response(self, related_collection_type, offset=0, limit=20): 136 | try: 137 | response = dict() 138 | response['included'] = list() 139 | total_length = eval('len(self.{related_collection_type})'.format( 140 | related_collection_type=related_collection_type) 141 | ) 142 | response['links'] = { 143 | 'self': '{base_url}/{type}/{id}/relationships/{related_collection_type}?page[offset]={offset}&page[limit]={limit}'.format( 144 | base_url=base_url, 145 | type=self.type, 146 | id=self.id, 147 | related_collection_type=related_collection_type, 148 | offset=offset, 149 | limit=limit 150 | ), 151 | 'related': '{base_url}/{type}/{id}/{related_collection_type}'.format( 152 | base_url=base_url, 153 | type=self.type, 154 | id=self.id, 155 | related_collection_type=related_collection_type), 156 | 'first': '{base_url}/{type}/{id}/relationships/{related_collection_type}?page[offset]={offset}&page[limit]={limit}'.format( 157 | base_url=base_url, 158 | type=self.type, 159 | id=self.id, 160 | related_collection_type=related_collection_type, 161 | offset=0, 162 | limit=limit 163 | ), 164 | 'last': "{base_url}/{type}/{id}/relationships/{related_collection_type}?page[offset]={offset}&page[limit]={limit}".format( 165 | base_url=base_url, 166 | type=self.type, 167 | id=self.id, 168 | related_collection_type=related_collection_type, 169 | offset=total_length - (total_length % int(limit)), 170 | limit=limit 171 | ) 172 | 173 | } 174 | 175 | if int(offset) - int(limit) > 0: 176 | response['links']['prev'] = "{base_url}/{type}/{id}/relationships/{related_collection_type}?page[offset]={offset}&page[limit]={limit}".format( 177 | base_url=base_url, 178 | type=self.type, 179 | id=self.id, 180 | related_collection_type=related_collection_type, 181 | offset=int(offset) - int(limit), 182 | limit=limit 183 | ) 184 | 185 | if total_length > int(offset) + int(limit): 186 | response['links']['next'] = "{base_url}/{type}/{id}/relationships/{related_collection_type}?page[offset]={offset}&page[limit]={limit}".format( 187 | base_url=base_url, 188 | type=self.type, 189 | id=self.id, 190 | related_collection_type=related_collection_type, 191 | offset=int(offset) + int(limit), 192 | limit=limit 193 | ) 194 | 195 | # data 196 | relation_type = eval('self.{related_collection_type}.definition'.format( 197 | related_collection_type=related_collection_type)).get('relation_type') 198 | 199 | results, columns = self.cypher( 200 | "START a=node({self}) MATCH a-[rel:{relation_type}]-(end_node) RETURN rel,end_node SKIP {offset} LIMIT {limit}".format( 201 | self=self._id, relation_type=relation_type, offset=offset, limit=limit 202 | ) 203 | ) 204 | 205 | # TODO: For line below SerializableStructuredRel must be set to specific rel model type 206 | 207 | relclass = SerializableStructuredRel.get_relclass_from_type(results[0][0]['type']) 208 | relationships = [relclass.inflate(row["rel"]) for row in results] 209 | related_node_or_nodes = [self.inflate(row["end_node"]) for row in results] 210 | 211 | if not type(getattr(self, related_collection_type)) == ZeroOrOne: 212 | response['data'] = list() 213 | for i, the_node in enumerate(related_node_or_nodes): 214 | if the_node.active: 215 | response['data'].append(relationships[i].get_resource_identifier_object(the_node)) 216 | response['included'].append(the_node.get_resource_object()) 217 | elif related_node_or_nodes: # The collection contains 1 item 218 | the_node = related_node_or_nodes[0] 219 | response['data'] = {'type': the_node.type, 'id': the_node.id} 220 | response['data'] = relationships[0].get_resource_identifier_object(the_node) 221 | response['included'].append(the_node.get_resource_object()) 222 | else: # The collection is has Cardinality ZeroOrOne and is zero, so null 223 | response['data'] = None 224 | 225 | r = make_response(jsonify(response)) 226 | r.status_code = http_error_codes.OK 227 | r.headers['Content-Type'] = CONTENT_TYPE 228 | except AttributeError: 229 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 230 | return r 231 | 232 | def set_related_resources_collection_inactive(self, related_collection_type): 233 | try: 234 | # data 235 | relation_type = eval('self.{related_collection_type}.definition'.format( 236 | related_collection_type=related_collection_type)).get('relation_type') 237 | 238 | results, columns = self.cypher( 239 | "START a=node({self}) MATCH a-[:{relation_type}]-(b) RETURN b".format( 240 | self=self._id, relation_type=relation_type 241 | ) 242 | ) 243 | related_node_or_nodes = [self.inflate(row[0]) for row in results] 244 | 245 | for n in related_node_or_nodes: 246 | n.deactivate() 247 | 248 | r = make_response('') 249 | r.status_code = http_error_codes.NO_CONTENT 250 | r.headers['Content-Type'] = CONTENT_TYPE 251 | except AttributeError: 252 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 253 | return r 254 | 255 | def set_individual_related_resource_inactive(self, related_collection_type, related_resource): 256 | # data 257 | related_node_or_nodes = eval('self.{related_collection_type}.search(id=related_resource)'.format(related_collection_type=related_collection_type), ) 258 | 259 | if len(related_node_or_nodes) == 1: 260 | the_node = related_node_or_nodes[0] 261 | the_node.deactivate() 262 | r = make_response('') 263 | r.status_code = http_error_codes.NO_CONTENT 264 | r.headers['Content-Type'] = CONTENT_TYPE 265 | else: 266 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 267 | return r 268 | 269 | def related_resources_collection_response(self, related_collection_type, included, offset=0, limit=20): 270 | try: 271 | response = dict() 272 | response['included'] = list() 273 | total_length = eval('len(self.{related_collection_type})'.format( 274 | related_collection_type=related_collection_type) 275 | ) 276 | response['links'] = { 277 | 'self': '{base_url}/{type}/{id}/{related_collection_type}?page[offset]={offset}&page[limit]={limit}'.format( 278 | base_url=base_url, 279 | type=self.type, 280 | id=self.id, 281 | related_collection_type=related_collection_type, 282 | offset=offset, 283 | limit=limit 284 | ), 285 | 'first': '{base_url}/{type}/{id}/{related_collection_type}?page[offset]={offset}&page[limit]={limit}'.format( 286 | base_url=base_url, 287 | type=self.type, 288 | id=self.id, 289 | related_collection_type=related_collection_type, 290 | offset=0, 291 | limit=limit 292 | ), 293 | 'last': "{base_url}/{type}/{id}/{related_collection_type}?page[offset]={offset}&page[limit]={limit}".format( 294 | base_url=base_url, 295 | type=self.type, 296 | id=self.id, 297 | related_collection_type=related_collection_type, 298 | offset=total_length - (total_length % int(limit)), 299 | limit=limit 300 | ) 301 | 302 | } 303 | 304 | if int(offset) - int(limit) > 0: 305 | response['links']['prev'] = "{base_url}/{type}/{id}/{related_collection_type}?page[offset]={offset}&page[limit]={limit}".format( 306 | base_url=base_url, 307 | type=self.type, 308 | id=self.id, 309 | related_collection_type=related_collection_type, 310 | offset=int(offset) - int(limit), 311 | limit=limit 312 | ) 313 | 314 | if total_length > int(offset) + int(limit): 315 | response['links']['next'] = "{base_url}/{type}/{id}/{related_collection_type}?page[offset]={offset}&page[limit]={limit}".format( 316 | base_url=base_url, 317 | type=self.type, 318 | id=self.id, 319 | related_collection_type=related_collection_type, 320 | offset=int(offset) + int(limit), 321 | limit=limit 322 | ) 323 | 324 | # data 325 | relation_type = eval('self.{related_collection_type}.definition'.format( 326 | related_collection_type=related_collection_type)).get('relation_type') 327 | 328 | results, columns = self.cypher( 329 | "START a=node({self}) MATCH a-[:{relation_type}]-(b) RETURN b SKIP {offset} LIMIT {limit}".format( 330 | self=self._id, relation_type=relation_type, offset=offset, limit=limit 331 | ) 332 | ) 333 | related_node_or_nodes = [self.inflate(row[0]) for row in results] 334 | 335 | if not eval("type(self.{related_collection_type})".format(related_collection_type=related_collection_type)) == ZeroOrOne: 336 | response['data'] = list() 337 | for the_node in related_node_or_nodes: 338 | if the_node.active: 339 | response['data'].append(the_node.get_resource_object()) 340 | for n in the_node.get_included_from_list(included): 341 | if n not in response['included']: 342 | response['included'].append(n) 343 | elif related_node_or_nodes: 344 | the_node = related_node_or_nodes[0] 345 | response['data'].append(the_node.get_resource_object()) 346 | else: 347 | response['data'] = None 348 | 349 | r = make_response(jsonify(response)) 350 | r.status_code = http_error_codes.OK 351 | r.headers['Content-Type'] = CONTENT_TYPE 352 | except AttributeError: 353 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 354 | except SyntaxError: 355 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 356 | return r 357 | 358 | def related_resources_individual_response(self, related_collection_type, related_resource, included=[]): 359 | response = dict() 360 | response['links'] = { 361 | 'self': '{base_url}/{type}/{id}/{related_collection_type}/{related_resource}'.format( 362 | base_url=base_url, 363 | type=self.type, 364 | id=self.id, 365 | related_collection_type=related_collection_type, 366 | related_resource=related_resource), 367 | } 368 | 369 | # data 370 | related_node_or_nodes = eval('self.{related_collection_type}.search(id=related_resource)'.format(related_collection_type=related_collection_type), ) 371 | 372 | if len(related_node_or_nodes) == 1: 373 | the_node = related_node_or_nodes[0] 374 | response['data'] = the_node.get_resource_object() 375 | response['included'] = the_node.get_included_from_list(included) 376 | r = make_response(jsonify(response)) 377 | r.status_code = http_error_codes.OK 378 | r.headers['Content-Type'] = CONTENT_TYPE 379 | else: 380 | response['data'] = None 381 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 382 | 383 | return r 384 | 385 | def delete_relationship_collection(self, related_collection_type): 386 | related_node_or_nodes = eval('self.{related_collection_type}.all()'.format( 387 | related_collection_type=related_collection_type 388 | )) 389 | for node in related_node_or_nodes: 390 | eval('self.{related_collection_type}.disconnect(node)'.format( 391 | related_collection_type=related_collection_type 392 | )) 393 | r = make_response('') 394 | r.status_code = http_error_codes.NO_CONTENT 395 | r.headers['Content-Type'] = CONTENT_TYPE 396 | 397 | return r 398 | 399 | def delete_individual_relationship(self, related_collection_type, related_resource): 400 | related_node_or_nodes = eval('self.{related_collection_type}.search(id=related_resource)'.format( 401 | related_collection_type=related_collection_type 402 | )) 403 | for node in related_node_or_nodes: 404 | eval('self.{related_collection_type}.disconnect(node)'.format( 405 | related_collection_type=related_collection_type 406 | )) 407 | r = make_response('') 408 | r.status_code = http_error_codes.NO_CONTENT 409 | r.headers['Content-Type'] = CONTENT_TYPE 410 | 411 | return r 412 | 413 | def individual_relationship_response(self, related_collection_type, related_resource, included=[]): 414 | try: 415 | response = dict() 416 | response['data'] = dict() 417 | response['included'] = list() 418 | response['links'] = { 419 | 'self': '{base_url}/{type}/{id}/relationships/{related_collection_type}/{related_resource}'.format( 420 | base_url=base_url, 421 | type=self.type, 422 | id=self.id, 423 | related_collection_type=related_collection_type, 424 | related_resource=related_resource), 425 | 'related': '{base_url}/{type}/{id}/{related_collection_type}/{related_resource}'.format( 426 | base_url=base_url, 427 | type=self.type, 428 | id=self.id, 429 | related_collection_type=related_collection_type, 430 | related_resource=related_resource) 431 | } 432 | 433 | # data 434 | relation_type = eval('self.{related_collection_type}.definition'.format( 435 | related_collection_type=related_collection_type)).get('relation_type') 436 | 437 | results, columns = self.cypher( 438 | "START a=node({self}) MATCH a-[rel:{relation_type}]-(end_node) RETURN rel, end_node".format( 439 | self=self._id, relation_type=relation_type 440 | ) 441 | ) 442 | if len(results) == 1: 443 | relationship = results[0]["rel"] 444 | the_node = self.inflate(results[0]["end_node"]) 445 | if the_node.active: 446 | response['data'] = relationship.get_resource_identifier_object(the_node) 447 | response['included'].append(the_node.get_resource_object()) 448 | else: 449 | raise DoesNotExist 450 | r = make_response(jsonify(response)) 451 | r.status_code = http_error_codes.OK 452 | r.headers['Content-Type'] = CONTENT_TYPE 453 | except (AttributeError, DoesNotExist): 454 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 455 | return r 456 | 457 | def deactivate(self): 458 | self.active = False 459 | self.save() 460 | 461 | @classmethod 462 | def get_collection(cls, request_args): 463 | r""" 464 | Used to fetch a collection of resource object of type 'cls' in response to a GET request\ 465 | . get_resource_or_collection should only be invoked on a resource when the client specifies a GET request. 466 | 467 | :param request_args: The query parameters supplied with the request. currently supports page[offset], and \ 468 | page[limit]. Pagination only applies to collection requests. See http://jsonapi.org/format/#fetching-pagination. 469 | :return: An HTTP response object in accordance with the specification at \ 470 | http://jsonapi.org/format/#fetching-resources 471 | """ 472 | try: 473 | if request_args.get('include'): 474 | raise ParameterNotSupported 475 | 476 | offset = request_args.get('page[offset]', 0) 477 | limit = request_args.get('page[limit]', 20) 478 | 479 | query = "MATCH (n) WHERE n:{label} AND n.active RETURN n ORDER BY n.id SKIP {offset} LIMIT {limit}".format( 480 | label=cls.__name__, 481 | offset=offset, 482 | limit=limit) 483 | 484 | results, meta = db.cypher_query(query) 485 | data = dict() 486 | data['data'] = list() 487 | data['links'] = dict() 488 | 489 | data['links']['self'] = "{class_link}?page[offset]={offset}&page[limit]={limit}".format( 490 | class_link=cls.get_class_link(), 491 | offset=offset, 492 | limit=limit 493 | ) 494 | 495 | data['links']['first'] = "{class_link}?page[offset]={offset}&page[limit]={limit}".format( 496 | class_link=cls.get_class_link(), 497 | offset=0, 498 | limit=limit 499 | ) 500 | 501 | if int(offset) - int(limit) > 0: 502 | data['links']['prev'] = "{class_link}?page[offset]={offset}&page[limit]={limit}".format( 503 | class_link=cls.get_class_link(), 504 | offset=int(offset)-int(limit), 505 | limit=limit 506 | ) 507 | 508 | if len(cls.nodes) > int(offset) + int(limit): 509 | data['links']['next'] = "{class_link}?page[offset]={offset}&page[limit]={limit}".format( 510 | class_link=cls.get_class_link(), 511 | offset=int(offset)+int(limit), 512 | limit=limit 513 | ) 514 | 515 | data['links']['last'] = "{class_link}?page[offset]={offset}&page[limit]={limit}".format( 516 | class_link=cls.get_class_link(), 517 | offset=len(cls.nodes.filter(active=True)) - (len(cls.nodes.filter(active=True)) % int(limit))-1, 518 | limit=limit 519 | ) 520 | 521 | list_of_nodes = [cls.inflate(row[0]) for row in results] 522 | for this_node in list_of_nodes: 523 | data['data'].append(this_node.get_resource_object()) 524 | r = make_response(jsonify(data)) 525 | r.status_code = http_error_codes.OK 526 | r.headers['Content-Type'] = CONTENT_TYPE 527 | return r 528 | except ParameterNotSupported: 529 | return application_codes.error_response([application_codes.PARAMETER_NOT_SUPPORTED_VIOLATION]) 530 | 531 | @classmethod 532 | def get_resource(cls, request_args, id): 533 | r""" 534 | Used to fetch a single resource object with the given id in response to a GET request.\ 535 | get_resource should only be invoked on a resource when the client specifies a GET request. 536 | 537 | :param request_args: 538 | :return: The query parameters supplied with the request. currently supports include. See \ 539 | http://jsonapi.org/format/#fetching-includes 540 | """ 541 | try: 542 | this_resource = cls.nodes.get(id=id, active=True) 543 | 544 | try: 545 | included = request_args.get('include').split(',') 546 | except AttributeError: 547 | included = [] 548 | 549 | r = this_resource.individual_resource_response(included) 550 | except DoesNotExist: 551 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 552 | 553 | return r 554 | 555 | @classmethod 556 | def get_resource_or_collection(cls, request_args, id=None): 557 | r""" 558 | Deprecated for version 1.1.0. Please use get_resource or get_collection. 559 | 560 | This function has multiple behaviors. 561 | 562 | With id specified: Used to fetch a single resource object with the given id in response to a GET request.\ 563 | get_resource_or_collection should only be invoked on a resource when the client specifies a GET request. 564 | 565 | With id not specified: Used to fetch a collection of resource object of type 'cls' in response to a GET request\ 566 | . get_resource_or_collection should only be invoked on a resource when the client specifies a GET request. 567 | 568 | :param request_args: The query parameters supplied with the request. currently supports include, page[offset], \ 569 | and page[limit]. Pagination only applies to collection requests. See http://jsonapi.org/format/#fetching-pagination and \ 570 | http://jsonapi.org/format/#fetching-includes 571 | :param id: The 'id' field of the node to fetch in the database. The id field must be set in the model -- it \ 572 | is not the same as the node id. If the id is not supplied the full collection will be returned. 573 | :return: An HTTP response object in accordance with the specification at \ 574 | http://jsonapi.org/format/#fetching-resources 575 | """ 576 | if id: 577 | try: 578 | r = cls.get_resource(request_args) 579 | except DoesNotExist: 580 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 581 | else: 582 | try: 583 | r = cls.get_collection(request_args) 584 | except Exception as e: 585 | r = application_codes.error_response([application_codes.BAD_FORMAT_VIOLATION]) 586 | return r 587 | 588 | @classmethod 589 | def create_resource(cls, request_json): 590 | r""" 591 | Used to create a node in the database of type 'cls' in response to a POST request. create_resource should only \ 592 | be invoked on a resource when the client specifies a POST request. 593 | 594 | :param request_json: a dictionary formatted according to the specification at \ 595 | http://jsonapi.org/format/#crud-creating 596 | :return: An HTTP response object in accordance with the same specification 597 | """ 598 | response = dict() 599 | new_resource, location = None, None 600 | try: 601 | data = request_json['data'] 602 | if data['type'] != cls.__type__: 603 | raise WrongTypeError('type must match the type of the resource being created.') 604 | 605 | attributes = data.get('attributes') 606 | if attributes: 607 | for x in attributes.keys(): 608 | if x in cls.dates: 609 | dt = datetime.strptime(attributes[x], '%Y-%m-%d') 610 | attributes[x] = dt 611 | 612 | new_resource = cls(**attributes) 613 | new_resource.save() 614 | 615 | enum_keys = new_resource.enums.keys() 616 | for key in attributes.keys(): 617 | if key in enum_keys: 618 | if attributes[key] in new_resource.enums[key]: 619 | setattr(new_resource, key, attributes[key]) 620 | else: 621 | raise EnumeratedTypeError 622 | else: 623 | setattr(new_resource, key, attributes[key]) 624 | new_resource.save() 625 | 626 | for r in new_resource.hashed: 627 | unhashed = getattr(new_resource, r) 628 | if unhashed: 629 | setattr(new_resource, r, hashlib.sha256(unhashed).hexdigest()) 630 | new_resource.save() 631 | 632 | relationships = data.get('relationships') 633 | if relationships: 634 | for relation_name in relationships.keys(): 635 | relations = relationships.get(relation_name) 636 | if relations: 637 | relations = relations['data'] 638 | if isinstance(relations, list): 639 | for relation in relations: 640 | the_type = relation['type'] # must translate type to cls 641 | the_id = relation['id'] 642 | the_class = cls.get_class_from_type(the_type) 643 | new_resources_relation = the_class.nodes.get(id=the_id, active=True) 644 | meta = relation.get('meta') 645 | eval('new_resource.{relation_name}.connect(new_resources_relation, meta)'.format( 646 | relation_name=relation_name) 647 | ) 648 | new_resource.save() 649 | else: 650 | relation = relations 651 | the_type = relation['type'] 652 | the_id = relation['id'] 653 | the_class = cls.get_class_from_type(the_type) 654 | new_resources_relation = the_class.nodes.get(id=the_id, active=True) 655 | meta = relation.get('meta') 656 | eval('new_resource.{relation_name}.connect(new_resources_relation, meta)'.format( 657 | relation_name=relation_name) 658 | ) 659 | new_resource.save() 660 | 661 | response['data'] = new_resource.get_resource_object() 662 | response['links'] = {'self': new_resource.get_self_link()} 663 | status_code = http_error_codes.CREATED 664 | location = new_resource.get_self_link() 665 | 666 | r = make_response(jsonify(response)) 667 | r.headers['Content-Type'] = "application/vnd.api+json; charset=utf-8" 668 | if location and new_resource: 669 | r.headers['Location'] = location 670 | 671 | r.status_code = status_code 672 | 673 | except UniqueProperty: 674 | r = application_codes.error_response([application_codes.UNIQUE_KEY_VIOLATION]) 675 | try: 676 | new_resource.delete() 677 | except: 678 | pass 679 | 680 | except DoesNotExist: 681 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 682 | try: 683 | new_resource.delete() 684 | except: 685 | pass 686 | 687 | except WrongTypeError as e: 688 | r = application_codes.error_response([application_codes.WRONG_TYPE_VIOLATION]) 689 | try: 690 | new_resource.delete() 691 | except: 692 | pass 693 | 694 | except KeyError as e: 695 | r = application_codes.error_response([application_codes.BAD_FORMAT_VIOLATION]) 696 | print e 697 | try: 698 | new_resource.delete() 699 | except: 700 | pass 701 | 702 | except EnumeratedTypeError: 703 | r = application_codes.error_response([application_codes.ENUMERATED_TYPE_VIOLATION]) 704 | try: 705 | new_resource.delete() 706 | except: 707 | pass 708 | 709 | except ParameterMissing: 710 | r = application_codes.error_response([application_codes.BAD_PARAMETER_VIOLATION]) 711 | try: 712 | new_resource.delete() 713 | except: 714 | pass 715 | 716 | return r 717 | 718 | @classmethod 719 | def update_resource(cls, request_json, id): 720 | r""" 721 | Used to update a node in the database of type 'cls' in response to a PATCH request. update_resource should only \ 722 | be invoked on a resource when the client specifies a PATCH request. 723 | 724 | :param request_json: a dictionary formatted according to the specification at \ 725 | http://jsonapi.org/format/#crud-updating 726 | :param id: The 'id' field of the node to update in the database. The id field must be set in the model -- it \ 727 | is not the same as the node id 728 | :return: An HTTP response object in accordance with the same specification 729 | """ 730 | response = dict() 731 | try: 732 | this_resource = cls.nodes.get(id=id, active=True) 733 | data = request_json['data'] 734 | if data['type'] != cls.__type__: 735 | raise WrongTypeError('type must match the type of the resource being updated.') 736 | 737 | attributes = data.get('attributes') 738 | if attributes: 739 | 740 | for x in attributes.keys(): 741 | if x in cls.dates: 742 | dt = datetime.strptime(attributes[x], '%Y-%m-%d') 743 | attributes[x] = dt 744 | 745 | this_resource.updated = datetime.now() 746 | 747 | this_resource.save() 748 | 749 | enum_keys = this_resource.enums.keys() 750 | for key in attributes.keys(): 751 | if key in enum_keys: 752 | if attributes[key] in this_resource.enums[key]: 753 | setattr(this_resource, key, attributes[key]) 754 | else: 755 | raise EnumeratedTypeError 756 | else: 757 | setattr(this_resource, key, attributes[key]) 758 | this_resource.save() 759 | 760 | for r in this_resource.hashed: 761 | unhashed = getattr(this_resource, r) 762 | setattr(this_resource, r, hashlib.sha256(unhashed).hexdigest()) 763 | this_resource.save() 764 | 765 | relationships = data.get('relationships') 766 | if relationships: 767 | for relation_name in relationships.keys(): 768 | relations = relationships.get(relation_name) 769 | 770 | for related_resource in eval('this_resource.{relation_name}.all()'.format(relation_name=relation_name)): 771 | eval('this_resource.{relation_name}.disconnect(related_resource)'. 772 | format(relation_name=relation_name)) 773 | 774 | if relations: 775 | relations = relations['data'] 776 | if isinstance(relations, list): 777 | for relation in relations: 778 | the_type = relation['type'] 779 | the_id = relation['id'] 780 | the_class = cls.get_class_from_type(the_type) 781 | new_resources_relation = the_class.nodes.get(id=the_id, active=True) 782 | meta = relation.get('meta') 783 | the_rel = eval( 784 | 'this_resource.{relation_name}.connect(new_resources_relation, meta)'.format( 785 | relation_name=relation_name 786 | ) 787 | ) 788 | else: 789 | relation = relations 790 | meta = relation.get('meta') 791 | the_type = relation['type'] 792 | the_id = relation['id'] 793 | the_class = cls.get_class_from_type(the_type) 794 | new_resources_relation = the_class.nodes.get(id=the_id, active=True) 795 | eval('this_resource.{relation_name}.connect(new_resources_relation, meta)'.format( 796 | relation_name=relation_name) 797 | ) 798 | this_resource.updated = datetime.now() 799 | this_resource.save() 800 | 801 | response['data'] = this_resource.get_resource_object() 802 | response['links'] = {'self': this_resource.get_self_link()} 803 | status_code = http_error_codes.OK 804 | location = this_resource.get_self_link() 805 | r = make_response(jsonify(response)) 806 | r.headers['Content-Type'] = "application/vnd.api+json; charset=utf-8" 807 | if location and this_resource: 808 | r.headers['Location'] = location 809 | r.status_code = status_code 810 | 811 | except UniqueProperty as e: 812 | print str(e) 813 | r = application_codes.error_response([application_codes.UNIQUE_KEY_VIOLATION]) 814 | 815 | except DoesNotExist: 816 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 817 | 818 | except WrongTypeError as e: 819 | r = application_codes.error_response([application_codes.WRONG_TYPE_VIOLATION]) 820 | 821 | except KeyError as e: 822 | r = application_codes.error_response([application_codes.BAD_FORMAT_VIOLATION]) 823 | 824 | except EnumeratedTypeError: 825 | r = application_codes.error_response([application_codes.ENUMERATED_TYPE_VIOLATION]) 826 | 827 | except ParameterMissing: 828 | r = application_codes.error_response([application_codes.BAD_PARAMETER_VIOLATION]) 829 | 830 | return r 831 | 832 | @classmethod 833 | def set_resource_inactive(cls, id): 834 | """This method is deprecated for version 1.1.0. Please use deactivate_resource""" 835 | return cls.deactivate_resource(cls, id) 836 | 837 | @classmethod 838 | def deactivate_resource(cls, id): 839 | r""" 840 | Used to deactivate a node of type 'cls' in response to a DELETE request. deactivate_resource should only \ 841 | be invoked on a resource when the client specifies a DELETE request. 842 | 843 | :param id: The 'id' field of the node to update in the database. The id field must be set in the model -- it \ 844 | is not the same as the node id 845 | :return: An HTTP response object in accordance with the specification at \ 846 | http://jsonapi.org/format/#crud-deleting 847 | """ 848 | try: 849 | this_resource = cls.nodes.get(id=id, active=True) 850 | this_resource.deactivate() 851 | r = make_response('') 852 | r.headers['Content-Type'] = "application/vnd.api+json; charset=utf-8" 853 | r.status_code = http_error_codes.NO_CONTENT 854 | except DoesNotExist: 855 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 856 | 857 | return r 858 | 859 | @classmethod 860 | def get_relationship(cls, request_args, id, related_collection_name, related_resource=None): 861 | """ 862 | Get a relationship 863 | :param request_args: 864 | :param id: The 'id' field of the node on the left side of the relationship in the database. The id field must \ 865 | be set in the model -- it is not the same as the node id 866 | :param related_collection_name: The name of the relationship 867 | :param related_resource: Deprecated for version 1.1.0 868 | :return: A response according to the specification at http://jsonapi.org/format/#fetching-relationships 869 | """ 870 | try: 871 | included = request_args.get('include').split(',') 872 | except (SyntaxError, AttributeError): 873 | included = [] 874 | try: 875 | offset = request_args.get('page[offset]', 0) 876 | limit = request_args.get('page[limit]', 20) 877 | this_resource = cls.nodes.get(id=id, active=True) 878 | if not related_resource: 879 | if request_args.get('include'): 880 | r = application_codes.error_response([application_codes.PARAMETER_NOT_SUPPORTED_VIOLATION]) 881 | else: 882 | r = this_resource.relationship_collection_response(related_collection_name, offset, limit) 883 | else: # deprecated for version 1.1.0 884 | r = this_resource.individual_relationship_response(related_collection_name, related_resource, included) 885 | 886 | except DoesNotExist: 887 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 888 | return r 889 | 890 | @classmethod 891 | def create_relationships(cls, id, related_collection_name, request_json): 892 | r""" 893 | Used to create relationship(s) between the id node and the nodes identified in the included resource \ 894 | identifier objects. 895 | 896 | :param id: The 'id' field of the node on the left side of the relationship in the database. The id field must \ 897 | be set in the model -- it is not the same as the node id 898 | :param related_collection_name: The name of the relationship 899 | :param request_json: request_json: a dictionary formatted according to the specification at \ 900 | http://jsonapi.org/format/#crud-updating-relationships 901 | :return: A response according to the same specification 902 | """ 903 | try: 904 | this_resource = cls.nodes.get(id=id, active=True) 905 | related_collection = getattr(this_resource, related_collection_name) 906 | if type(related_collection) in (One, ZeroOrOne): # Cardinality <= 1 so update_relationship should be used 907 | r = application_codes.error_response([application_codes.FORBIDDEN_VIOLATION]) 908 | else: 909 | data = request_json['data'] 910 | for rsrc_identifier in data: 911 | the_new_node = cls.get_class_from_type(rsrc_identifier['type']).nodes.get(id=rsrc_identifier['id']) 912 | rel_attrs = rsrc_identifier.get('meta') 913 | if not rel_attrs or isinstance(rel_attrs, dict): 914 | related_collection.connect(the_new_node, rel_attrs) 915 | else: 916 | raise WrongTypeError 917 | #r = this_resource.relationship_collection_response(related_collection_name) 918 | r = make_response('') 919 | r.status_code = http_error_codes.NO_CONTENT 920 | r.headers['Content-Type'] = CONTENT_TYPE 921 | 922 | except DoesNotExist: 923 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 924 | except (KeyError, TypeError, WrongTypeError): 925 | r = application_codes.error_response([application_codes.BAD_FORMAT_VIOLATION]) 926 | except AttemptedCardinalityViolation: 927 | r = application_codes.error_response([application_codes.ATTEMPTED_CARDINALITY_VIOLATION]) 928 | except MultipleNodesReturned: 929 | r = application_codes.error_response([application_codes.MULTIPLE_NODES_WITH_ID_VIOLATION]) 930 | return r 931 | 932 | @classmethod 933 | def disconnect_relationship(cls, id, related_collection_name, request_json): 934 | """ 935 | Disconnect one or more relationship in a collection with cardinality 'Many'. 936 | 937 | :param id: The 'id' field of the node on the left side of the relationship in the database. The id field must \ 938 | be set in the model -- it is not the same as the node id 939 | :param related_collection_name: The name of the relationship 940 | :param request_json: a dictionary formatted according to the specification at \ 941 | http://jsonapi.org/format/#crud-updating-relationships 942 | :return: A response according to the same specification 943 | """ 944 | try: 945 | this_resource = cls.nodes.get(id=id, active=True) 946 | related_collection = getattr(this_resource, related_collection_name) 947 | rsrc_identifier_list = request_json['data'] 948 | if not isinstance(rsrc_identifier_list, list): 949 | raise WrongTypeError 950 | 951 | for rsrc_identifier in rsrc_identifier_list: 952 | connected_resource = cls.get_class_from_type(rsrc_identifier['type']).nodes.get( 953 | id=rsrc_identifier['id'] 954 | ) 955 | related_collection.disconnect(connected_resource) 956 | r = make_response('') 957 | r.status_code = http_error_codes.NO_CONTENT 958 | r.headers['Content-Type'] = CONTENT_TYPE 959 | except DoesNotExist: 960 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 961 | except (KeyError, WrongTypeError): 962 | r = application_codes.error_response([application_codes.BAD_FORMAT_VIOLATION]) 963 | return r 964 | 965 | @classmethod 966 | def delete_relationship(cls, id, related_collection_name, related_resource=None): 967 | """ 968 | Deprecated for version 1.1.0. Please use update_relationship 969 | """ 970 | try: 971 | this_resource = cls.nodes.get(id=id, active=True) 972 | if not related_resource: 973 | r = this_resource.delete_relationship_collection(related_collection_name) 974 | else: 975 | r = this_resource.delete_individual_relationship(related_collection_name, related_resource) 976 | except DoesNotExist: 977 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 978 | return r 979 | 980 | @classmethod 981 | def update_relationship(cls, id, related_collection_name, request_json): 982 | r""" 983 | Used to completely replace all the existing relationships with new ones. 984 | 985 | :param id: The 'id' field of the node on the left side of the relationship in the database. The id field must \ 986 | be set in the model -- it is not the same as the node id 987 | :param related_collection_name: The name of the relationship 988 | :param request_json: a dictionary formatted according to the specification at \ 989 | http://jsonapi.org/format/#crud-updating-relationships 990 | :return: A response according to the same specification 991 | """ 992 | try: 993 | this_resource = cls.nodes.get(id=id, active=True) 994 | related_collection = getattr(this_resource, related_collection_name) 995 | data = request_json['data'] 996 | 997 | if type(related_collection) in (One, ZeroOrOne): # Cardinality <= 1 so is a single obj 998 | if not data and related_collection.single(): # disconnect the resource 999 | related_collection.disconnect(related_collection.single()) 1000 | elif not data: 1001 | pass # There is already no connected resource 1002 | else: 1003 | the_new_node = cls.get_class_from_type(data['type']).nodes.get(id=data['id']) 1004 | if related_collection.single(): # update the relationship 1005 | related_collection.reconnect(related_collection.single(), the_new_node) 1006 | the_rel = eval('related_collection.relationship(the_new_node)'.format( 1007 | start_node=this_resource, relname=related_collection_name) 1008 | ) 1009 | meta = data.get('meta') 1010 | if meta: 1011 | for k in meta.keys(): 1012 | setattr(the_rel, k, meta[k]) 1013 | the_rel.save() 1014 | 1015 | else: # create the relationship 1016 | related_collection.connect(the_new_node, data.get('meta')) 1017 | 1018 | else: # Cardinality > 1 so this is a collection of objects 1019 | old_nodes = related_collection.all() 1020 | for item in old_nodes: # removes all old connections 1021 | related_collection.disconnect(item) 1022 | for identifier in data: # adds all new connections 1023 | the_new_node = cls.get_class_from_type(identifier['type']).nodes.get(id=identifier['id']) 1024 | the_rel = related_collection.connect(the_new_node) 1025 | meta = identifier.get('meta') 1026 | if meta: 1027 | for k in meta.keys(): 1028 | setattr(the_rel, k, meta[k]) 1029 | the_rel.save() 1030 | 1031 | r = make_response('') 1032 | r.status_code = http_error_codes.NO_CONTENT 1033 | r.headers['Content-Type'] = CONTENT_TYPE 1034 | 1035 | except DoesNotExist: 1036 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 1037 | except (KeyError, TypeError): 1038 | r = application_codes.error_response([application_codes.BAD_FORMAT_VIOLATION]) 1039 | except AttemptedCardinalityViolation: 1040 | r = application_codes.error_response([application_codes.ATTEMPTED_CARDINALITY_VIOLATION]) 1041 | except MultipleNodesReturned: 1042 | r = application_codes.error_response([application_codes.MULTIPLE_NODES_WITH_ID_VIOLATION]) 1043 | return r 1044 | 1045 | @classmethod 1046 | def get_related_resources(cls, request_args, id, related_collection_name, related_resource=None): 1047 | try: 1048 | included = request_args.get('include').split(',') 1049 | except: 1050 | included = [] 1051 | try: 1052 | this_resource = cls.nodes.get(id=id, active=True) 1053 | if not related_resource: 1054 | offset = request_args.get('page[offset]', 0) 1055 | limit = request_args.get('page[limit]', 20) 1056 | r = this_resource.related_resources_collection_response(related_collection_name, included, offset, limit) 1057 | else: 1058 | r = this_resource.related_resources_individual_response(related_collection_name, related_resource, included) 1059 | 1060 | except DoesNotExist: 1061 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 1062 | 1063 | return r 1064 | 1065 | @classmethod 1066 | def set_related_resources_inactive(cls, id, related_collection_name, related_resource=None): 1067 | try: 1068 | this_resource = cls.nodes.get(id=id, active=True) 1069 | if not related_resource: 1070 | r = this_resource.set_related_resources_collection_inactive(related_collection_name) 1071 | else: 1072 | r = this_resource.set_individual_related_resource_inactive(related_collection_name, related_resource) 1073 | 1074 | except DoesNotExist: 1075 | r = application_codes.error_response([application_codes.RESOURCE_NOT_FOUND]) 1076 | 1077 | return r 1078 | 1079 | @classmethod 1080 | def get_class_from_type(cls, the_type): 1081 | for the_cls in cls.__base__.__subclasses__(): 1082 | if the_cls.__type__ == the_type: 1083 | return the_cls 1084 | return None 1085 | 1086 | 1087 | class EnumeratedTypeError(Exception): 1088 | pass 1089 | 1090 | 1091 | class SerializableStructuredRel(StructuredRel): 1092 | r""" 1093 | The Base Relationship that all Structured Relationships must inherit from. All relationships should be structured \ 1094 | starting version 1.1.0 -- okay to use model=SerializableStructuredRel 1095 | """ 1096 | secret = [] 1097 | updated = DateTimeProperty(default=datetime.now()) 1098 | created = DateTimeProperty(default=datetime.now()) 1099 | type = StringProperty(default="serializable_structured_rel") 1100 | 1101 | def get_resource_identifier_object(self, end_node): 1102 | try: 1103 | response = dict() 1104 | response['id'] = end_node.id 1105 | response['type'] = end_node.type 1106 | response['meta'] = dict() 1107 | 1108 | props = self.defined_properties() 1109 | print self.__class__ 1110 | for attr_name in props.keys(): 1111 | print attr_name 1112 | if attr_name not in self.secret: 1113 | response['meta'][attr_name] = getattr(self, attr_name) 1114 | 1115 | return response 1116 | except Exception as e: 1117 | print type(e), e 1118 | raise 1119 | 1120 | @classmethod 1121 | def get_relclass_from_type(cls, the_type): 1122 | for the_cls in cls.__subclasses__(): 1123 | if the_cls.__type__ == the_type: 1124 | return the_cls 1125 | return None 1126 | 1127 | 1128 | 1129 | 1130 | --------------------------------------------------------------------------------