├── .gitignore ├── README.md ├── __init__.py ├── client.py ├── request.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # files to ignore 2 | *.pyc 3 | x.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Compass 2 | ============= 3 | Compass is a simple Python REST interface for the OrientDB graph document store. Later iterations of compass will allow you to choose between the REST or BINARY protocols while using the same compass interface. 4 | 5 | Requirements 6 | ------------- 7 | * Python 2.6 8 | * [OrientDB Rest Server](http://www.orientechnologies.com/) 9 | 10 | Classes 11 | ------------- 12 | Compass provides seven classes that represent OrientDB objects. 13 | 14 | * **CompassException** -- the exception object that is returned when things go wrong. 15 | * **BaseObject** -- all other objects extend this one. It provides a simple interface for save and delete and adds some dict-like interactions via member getters, setters, and iterations 16 | * **Server** -- the root object for all Compass interactions. Allows creation and retrieval of databases (Database). 17 | * **Database** -- most actions are taken directly against the Database object. Allows creation and retrieval of classes (Klass) and documents (Document). 18 | * **Klass** -- this object is though of as a table, like documents (Document) can be easily grouped by class. A class has a schema and Klass allows for creation and removal of properties in that schema (KlassProperty) and documents (Document) for that class. 19 | * **Cluster** -- all of the physical or logical cluster information 20 | * **KlassProperty** -- holds all of the information about a class (Klass) property. 21 | * **Document** -- this object represents a single record in the database (Database) or class (Klass) 22 | 23 | Usage 24 | ------------- 25 | Using compass is simple, all failures will raise a CompassException. 26 | 27 | **Imports** 28 | 29 | import compass 30 | import json 31 | 32 | **Exceptions** 33 | All actions throw a CompassException if it happens to fail. Good practice would be to wrap all code in a try except construct to capture any errors. 34 | 35 | try: 36 | server = client.server(url='http://localhost:2480') 37 | except CompassException, e: 38 | print e #should return a 401 with an error message 39 | 40 | 41 | **Server Connection** 42 | To connect to the server you need to locate the user defined in the orientdb-server-config.xml file. Once you start the server for the first time, the user is root and the password is automatically generated. 43 | 44 | user = 'root' 45 | password = 'EDEBF121682BF71D0DEDB82459E6B772CDD291F3C0765D7C6B104420604F269C' 46 | url = 'http://localhost:2480' 47 | server = client.Server(url=url, username=username, password=password) 48 | 49 | **Database Interactions** 50 | Connect to or retrieve an existing database. Upon database creation, three users are created; admin, writer, and reader. Those are defined as a simple tuple so if you create a custom user, store the credentials as such. 51 | 52 | #create a demo database 53 | demo_db = server.database(name='client_demo', create=True, 54 | credentials=client.ADMIN) 55 | 56 | #add a new class to the database 57 | address_class = demo_db.klass(name='Address', create=True) 58 | 59 | #add a document directly to that class 60 | apple_doc = address_class.document(address1='1 Infinite Loop', 61 | city='Cupertino', state='CA', zip='95014') 62 | 63 | #add another document to the database object, but give it the class 'Address' 64 | microsoft_doc = demo_db.document(class_name='Address', address1='One Microsoft Way'...) 65 | 66 | #query documents this returns a Klass object with its data member made up of Document objects. 67 | #The key is the record_id (@rid) and value is the Document object 68 | result_klass = demo_db.query('SELECT * FROM Address WHERE city = "Cupertino"') 69 | 70 | **Klass Object** 71 | The class object will eventually allow you to define a schema for all documents that belong to it. The latest build of OrientDB's rest server does not allow for creation of properties that define certain attributes (required, max, min length, etc), it will in a later release 72 | 73 | #retrieve a class 74 | addresses = demo_db.klass(name='Address') 75 | 76 | #add a property to the schema 77 | addresses.property(name='country') 78 | 79 | #display the schema 80 | print addresses.schema # [dict] key is the property name, value is a KlassProperty object 81 | 82 | #modify a property -- this will matter in a future release, the save interface is not defined yet 83 | country_prop = addresses.schema['country'] 84 | country_prop['required'] = True 85 | 86 | #remove a property 87 | addresses.schema['country'].delete() 88 | 89 | **Document** 90 | Documents should be created from a Database or Klass object, it is possible to create a document using the Document constructor, but a Database or Klass needs to be passed in as an argument. 91 | 92 | #update a document 93 | apple_doc['country'] = 'USA' 94 | 95 | #save 96 | apple_doc.save() 97 | 98 | #delete 99 | apple_doc.delete() 100 | 101 | #connect a document to another one 102 | #multiple will store the connections as a list 103 | other_document = demo_db.document(field='Value') 104 | apple_doc.relate(field='connections', document=other_document, multiple=True) -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from client import * 2 | -------------------------------------------------------------------------------- /client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """Python client for OrientDB REST serivce""" 4 | 5 | import urllib 6 | import json 7 | 8 | from request import Request 9 | 10 | RECORD_ID = '@rid' 11 | ADMIN = ('admin', 'admin') 12 | READER = ('reader', 'reader') 13 | WRITER = ('writer', 'writer') 14 | 15 | 16 | class CompassException(Exception): 17 | """General purpose Compass Exception Object""" 18 | pass 19 | 20 | 21 | class BaseObject(object): 22 | """This is the base object for all Compass Objects""" 23 | data = {} 24 | rid = None 25 | _immutable = [] 26 | 27 | def __init__(self, data): 28 | if data is None: 29 | data = {} 30 | 31 | self.data = data 32 | 33 | def save(self): 34 | """ 35 | If an extending object has a save action, 36 | it will overwrite this method 37 | """ 38 | pass 39 | 40 | def delete(self): 41 | """ 42 | If an extending object has a delete action, 43 | it will overwrite this method 44 | """ 45 | pass 46 | 47 | def __len__(self): 48 | return len(self.data) 49 | 50 | def __iter__(self): 51 | return iter(self.data) 52 | 53 | def __contains__(self, key): 54 | return key in self.data 55 | 56 | def __getitem__(self, key): 57 | return self.data[key] 58 | 59 | def __setitem__(self, key, value): 60 | if key in self._immutable: 61 | raise KeyError('%s is not editable' % (key)) 62 | 63 | self.data[key] = value 64 | 65 | def __delitem__(self, key): 66 | if key in self._immutable: 67 | raise KeyError('%s is not editable' % (key)) 68 | 69 | del self.data[key] 70 | 71 | 72 | class Server(BaseObject): 73 | """This Object handles all Server-based interactions""" 74 | action = { 75 | 'info': '%s/server', 76 | 'disconnect': '%s/disconnect' 77 | } 78 | 79 | def __init__(self, url, username, password): 80 | super(Server, self).__init__(data=None) 81 | 82 | self.url = url 83 | self.username = username 84 | self.password = password 85 | self.request = Request(self.username, self.password) 86 | 87 | def info(self): 88 | """ 89 | This method queries the server for all of the 90 | information related to it 91 | """ 92 | url = self.action['info'] % (self.url) 93 | response, content = self.request.get(url=url) 94 | 95 | if response.status == 200: 96 | self.data = json.loads(content) 97 | return 98 | elif response.status == 204: 99 | return 100 | else: 101 | raise CompassException(content) 102 | 103 | return self 104 | 105 | def disconnect(self): 106 | """Sends a disconnect request to the server""" 107 | url = self.action['disconnect'] % (self.url) 108 | self.request.get(url) 109 | 110 | def database(self, name, credentials=ADMIN, create=False, storage='memory'): 111 | """Sends a request the server for a database 112 | will both add new and query existing databases. 113 | 114 | Returns a Datase object 115 | """ 116 | if create: 117 | url = Database.action['post'] % (self.url, name, storage) 118 | response, content = self.request.post(url=url, data=None) 119 | 120 | if response.status == 200: 121 | return self.database(name=name, credentials=ADMIN) 122 | else: 123 | raise CompassException(content) 124 | else: 125 | url = Database.action['get'] % (self.url, name) 126 | user, password = credentials 127 | request = Request(user, password) 128 | response, content = request.get(url=url) 129 | 130 | if response.status == 200: 131 | data = json.loads(content) 132 | return Database(self.url, name=name, 133 | credentials=credentials, data=data).connect() 134 | else: 135 | raise CompassException(content) 136 | 137 | 138 | class Database(BaseObject): 139 | """Database object""" 140 | action = { 141 | 'connect': '%s/connect/%s', 142 | 'get': '%s/database/%s', 143 | 'post': '%s/database/%s/%s', 144 | 'query': '%s/command/%s/%s/%s', 145 | 'cluster': '%s/cluster/%s/%s' 146 | } 147 | 148 | def __init__(self, url, name, credentials, lang='sql', data=None): 149 | super(Database, self).__init__(data=None) 150 | 151 | self.url = url 152 | self.lang = lang 153 | self.name = name 154 | self.credentials = credentials 155 | self.request = Request(username=credentials[0], 156 | password=credentials[1]) 157 | 158 | if data is None: 159 | self.connect() 160 | else: 161 | self.data = data 162 | 163 | def reload(self, callback=None): 164 | """This method will repopulate the 165 | data property of for the Database object 166 | """ 167 | url = self.action['get'] % (self.url, self.name) 168 | response, content = self.request.get(url) 169 | 170 | if response.status == 200: 171 | self.data = json.loads(content) 172 | 173 | if callback is not None and hasattr(callback, '__call__'): 174 | callback() 175 | else: 176 | raise CompassException(content) 177 | 178 | def connect(self): 179 | """Creats a connection to the database""" 180 | url = self.action['connect'] % (self.url, self.name) 181 | response, content = self.request.get(url) 182 | 183 | if response.status == 200: 184 | self.data = json.loads(content) 185 | elif response.status == 204: 186 | pass 187 | else: 188 | raise CompassException(content) 189 | 190 | return self 191 | 192 | def cluster(self, class_name): 193 | """Queries the server for a Cluster object""" 194 | url = Cluster.action['get'] % (self.url, self.name, class_name) 195 | response, content = self.request.get(url) 196 | 197 | if response.status == 200: 198 | return Cluster(database=self, data=json.loads(content)) 199 | elif response.status == 204: 200 | pass 201 | else: 202 | raise CompassException(content) 203 | 204 | def klass(self, name, limit=20, create=False): 205 | """Sends a request the server for class information 206 | will both add new and query existing classes. 207 | 208 | Returns a Klass object 209 | """ 210 | if create: 211 | url = Klass.action['post'] % (self.url, self.name, name) 212 | response, content = self.request.post(url, data={}) 213 | 214 | if response.status == 201: 215 | return self.klass(name=name) 216 | else: 217 | raise CompassException(content) 218 | else: 219 | url = Klass.action['get'] % (self.url, self.name, name, limit) 220 | response, content = self.request.get(url) 221 | 222 | if response.status == 200: 223 | data = json.loads(content) 224 | 225 | if data.has_key('result'): 226 | data = data['result'] 227 | else: 228 | data = None; 229 | 230 | return Klass(database=self, name=name, 231 | documents=data) 232 | else: 233 | raise CompassException(content) 234 | 235 | def query(self, query, klass=None): 236 | """Sends a reqeust to the server based on a query""" 237 | query = urllib.quote(query) 238 | url = self.action['query'] % (self.url, self.name, self.lang, query) 239 | response, content = self.request.post(url, data={}) 240 | 241 | if response.status == 200: 242 | result = json.loads(content) 243 | 244 | if isinstance(klass, Klass): 245 | klass.define_documents(result['result'], reset=True) 246 | else: 247 | return Klass(database=self, documents=result['result']) 248 | else: 249 | raise CompassException(content) 250 | 251 | def document(self, rid=None, class_name=None, **data): 252 | if rid: 253 | rid = rid.replace("#", "", 1) 254 | url = Document.action['get'] % (self.url, self.name, rid) 255 | response, content = self.request.get(url) 256 | 257 | if response.status == 200: 258 | return Document(rid, json.loads(content), database=self) 259 | else: 260 | raise CompassException(content) 261 | else: 262 | if class_name is not None: 263 | data['@class'] = class_name 264 | 265 | url = Document.action['post'] % (self.url, self.name) 266 | response, content = self.request.post(url, data=data) 267 | content = json.loads(content) 268 | rid = content[RECORD_ID] 269 | rid = rid.replace("#", "", 1) 270 | if response.status == 201: 271 | return self.document(rid=rid, database=self) 272 | else: 273 | raise CompassException(content) 274 | 275 | 276 | class Cluster(BaseObject): 277 | action = { 278 | 'get': '%s/cluster/%s/%s' 279 | } 280 | 281 | def __init__(self, database, data): 282 | super(Cluster, self).__init__(data) 283 | self.database = database 284 | 285 | 286 | class Klass(BaseObject): 287 | action = { 288 | 'get': '%s/class/%s/%s/%s', 289 | 'post': '%s/class/%s/%s' 290 | } 291 | 292 | def __init__(self, database, name=None, schema=None, documents=None): 293 | super(Klass, self).__init__(data=None) 294 | 295 | self.name = name 296 | self.database = database 297 | 298 | if schema is None: 299 | schema = {} 300 | 301 | self.schema = schema 302 | 303 | self.define_documents(documents) 304 | 305 | def define_documents(self, documents=None, reset=False): 306 | if documents is None: 307 | documents = {} 308 | 309 | if reset: 310 | self.data = {} 311 | 312 | keys = self.data.keys() 313 | 314 | for data in documents: 315 | if data[RECORD_ID] not in keys: 316 | self.data[data[RECORD_ID]] = Document(data[RECORD_ID], 317 | data, klass=self) 318 | 319 | def query(self, query): 320 | return self.database.query(query=query, klass=self) 321 | 322 | def document(self, rid=None, **data): 323 | return self.database.document(rid=rid, class_name=self.name, **data) 324 | 325 | def property(self, name, create=True): 326 | if create and name not in self.schema: 327 | try: 328 | prop = KlassProperty(name=name, klass=self, 329 | callback=self._define_schema) 330 | except CompassException: 331 | raise 332 | 333 | elif name in self.schema: 334 | prop = self.schema[name] 335 | 336 | prop.delete() 337 | del self.schema[name] 338 | 339 | def _define_schema(self): 340 | for klass in self.database.data['classes']: 341 | if klass['name'] == self.name: 342 | for prop in klass['properties']: 343 | if klass['name'] not in self.schema: 344 | self.schema[klass['name']] = KlassProperty( 345 | name=prop['name'], 346 | klass=self, data=prop) 347 | 348 | 349 | class KlassProperty(BaseObject): 350 | action = { 351 | 'post': '%s/property/%s/%s/%s', 352 | 'delete': '%s/property/%s/%s/%s' 353 | } 354 | 355 | def __init__(self, name, klass, data=None, callback=None): 356 | super(KlassProperty, self).__init__(data=None) 357 | 358 | self.name = name 359 | self.klass = klass 360 | 361 | if callback is not None: 362 | self._create(callback=callback) 363 | 364 | def delete(self): 365 | url = self.action['delete'] % (self.klass.database.url, 366 | self.klass.database.name, 367 | self.klass.name, self.name) 368 | response, content = self.klass.database.request.delete(url) 369 | 370 | if response.status != 204: 371 | raise CompassException(content) 372 | 373 | def _create(self, callback): 374 | url = self.action['post'] % (self.klass.database.url, 375 | self.klass.database.name, 376 | self.klass.name, self.name) 377 | response, content = self.klass.database.request.post(url, data={}) 378 | 379 | if response.status == 201: 380 | self.klass.database.reload(callback=callback) 381 | else: 382 | raise CompassException(content) 383 | 384 | 385 | class Document(BaseObject): 386 | action = { 387 | 'get': '%s/document/%s/%s', 388 | 'post': '%s/document/%s', 389 | 'put': '%s/document/%s', 390 | 'delete': '%s/document/%s/%s' 391 | } 392 | _immutable = ['@rid'] 393 | 394 | def __init__(self, rid, data, database=None, klass=None): 395 | super(Document, self).__init__(data) 396 | 397 | rid = rid.replace("#", "", 1) 398 | 399 | self.rid = rid 400 | self.database = database 401 | self.klass = klass 402 | 403 | if self.database is not None: 404 | self.db_url = self.database.url 405 | self.db_name = self.database.name 406 | self.db_request = self.database.request 407 | else: 408 | self.db_url = self.klass.database.url 409 | self.db_name = self.klass.database.name 410 | self.db_request = self.klass.database.request 411 | 412 | def save(self): 413 | url = self.action['put'] % (self.db_url, self.db_name) 414 | response, content = self.db_request.put(url=url, data=self.data) 415 | 416 | if response.status == 200: 417 | return self 418 | else: 419 | raise CompassException(content) 420 | 421 | def relate(self, field, document=None, rid=None, multiple=False): 422 | if document is not None: 423 | rid = document[RECORD_ID] 424 | 425 | if multiple: 426 | if isinstance(self[field], list): 427 | ids = self[field] 428 | 429 | if rid not in ids: 430 | ids.append(rid) 431 | 432 | self[field] = ids 433 | else: 434 | self[field] = rid 435 | 436 | return self 437 | 438 | def delete(self): 439 | url = self.action['delete'] % (self.db_url, self.db_name, self.rid) 440 | response, content = self.db_request.delete(url=url) 441 | 442 | if response.status != 204: 443 | raise CompassException(content) 444 | -------------------------------------------------------------------------------- /request.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import base64 4 | import datetime 5 | import decimal 6 | import httplib2 7 | import re 8 | import time 9 | from urlparse import urlsplit 10 | 11 | try: 12 | import json 13 | except ImportError: 14 | import simplejson as json 15 | 16 | __author__ = "Javier de la Rosa, and Diego Muñoz Escalante" 17 | __credits__ = ["Javier de la Rosa", "Diego Muñoz Escalante"] 18 | __license__ = "GPLv3" 19 | __version__ = "0.1.0" 20 | __email__ = "versae [at] gmail [dot] com" 21 | __status__ = "Development" 22 | 23 | CACHE = False 24 | DEBUG = False 25 | 26 | 27 | class StatusException(Exception): 28 | """ 29 | Create an Error Response. 30 | """ 31 | 32 | def __init__(self, value, result=None): 33 | self.value = value 34 | self.responses = { 35 | 100: ('Continue', 'Request received, please continue'), 36 | 101: ('Switching Protocols', 37 | 'Switching to new protocol; obey Upgrade header'), 38 | 200: ('OK', 'Request fulfilled, document follows'), 39 | 201: ('Created', 'Document created, URL follows'), 40 | 202: ('Accepted', 41 | 'Request accepted, processing continues off-line'), 42 | 203: ('Non-Authoritative Information', 'Request fulfilled from cache'), 43 | 204: ('No Content', 'Request fulfilled, nothing follows'), 44 | 205: ('Reset Content', 'Clear input form for further input.'), 45 | 206: ('Partial Content', 'Partial content follows.'), 46 | 300: ('Multiple Choices', 47 | 'Object has several resources -- see URI list'), 48 | 301: ('Moved Permanently', 'Object moved permanently -- see URI list'), 49 | 302: ('Found', 'Object moved temporarily -- see URI list'), 50 | 303: ('See Other', 'Object moved -- see Method and URL list'), 51 | 304: ('Not Modified', 52 | 'Document has not changed since given time'), 53 | 305: ('Use Proxy', 54 | 'You must use proxy specified in Location to access this ' 55 | 'resource.'), 56 | 307: ('Temporary Redirect', 57 | 'Object moved temporarily -- see URI list'), 58 | 400: ('Bad Request', 59 | 'Bad request syntax or unsupported method'), 60 | 401: ('Unauthorized', 61 | 'No permission -- see authorization schemes'), 62 | 402: ('Payment Required', 63 | 'No payment -- see charging schemes'), 64 | 403: ('Forbidden', 65 | 'Request forbidden -- authorization will not help'), 66 | 404: ('Not Found', 'Nothing matches the given URI'), 67 | 405: ('Method Not Allowed', 68 | 'Specified method is invalid for this server.'), 69 | 406: ('Not Acceptable', 'URI not available in preferred format.'), 70 | 407: ('Proxy Authentication Required', 'You must authenticate with ' 71 | 'this proxy before proceeding.'), 72 | 408: ('Request Timeout', 'Request timed out; try again later.'), 73 | 409: ('Conflict', 'Request conflict.'), 74 | 410: ('Gone', 75 | 'URI no longer exists and has been permanently removed.'), 76 | 411: ('Length Required', 'Client must specify Content-Length.'), 77 | 412: ('Precondition Failed', 'Precondition in headers is false.'), 78 | 413: ('Request Entity Too Large', 'Entity is too large.'), 79 | 414: ('Request-URI Too Long', 'URI is too long.'), 80 | 415: ('Unsupported Media Type', 'Entity body in unsupported format.'), 81 | 416: ('Requested Range Not Satisfiable', 82 | 'Cannot satisfy request range.'), 83 | 417: ('Expectation Failed', 84 | 'Expect condition could not be satisfied.'), 85 | 418: ('I\'m a teapot', 'Is the server running?'), 86 | 500: ('Internal Server Error', 'Server got itself in trouble'), 87 | 501: ('Not Implemented', 88 | 'Server does not support this operation'), 89 | 502: ('Bad Gateway', 'Invalid responses from another server/proxy.'), 90 | 503: ('Service Unavailable', 91 | 'The server cannot process the request due to a high load'), 92 | 504: ('Gateway Timeout', 93 | 'The gateway server did not receive a timely response'), 94 | 505: ('HTTP Version Not Supported', 'Cannot fulfill request.'), 95 | } 96 | if result: 97 | self.result = "\n%s" % result 98 | 99 | def __str__(self): 100 | return u"Error [%s]: %s. %s.%s" % (self.value, 101 | self.responses[self.value][0], 102 | self.responses[self.value][1], 103 | self.result) 104 | 105 | def __unicode__(self): 106 | return self.__str__() 107 | 108 | 109 | class NotFoundError(StatusException): 110 | 111 | def __init__(self, value=None, result=None): 112 | if not value: 113 | value = 404 114 | if not result: 115 | result = "Node, relationship or property not found" 116 | super(NotFoundError, self).__init__(value, result) 117 | 118 | 119 | class Request(object): 120 | """ 121 | Create an HTTP request object for HTTP 122 | verbs GET, POST, PUT and DELETE. 123 | """ 124 | 125 | def __init__(self, username=None, password=None, key_file=None, 126 | cert_file=None): 127 | self.username = username 128 | self.password = password 129 | self.key_file = key_file 130 | self.cert_file = cert_file 131 | self._illegal_s = re.compile(r"((^|[^%])(%%)*%s)") 132 | 133 | def get(self, url, headers=None): 134 | """ 135 | Perform an HTTP GET request for a given URL. 136 | Returns the response object. 137 | """ 138 | return self._request('GET', url, headers=headers) 139 | 140 | def post(self, url, data, headers=None): 141 | """ 142 | Perform an HTTP POST request for a given url. 143 | Returns the response object. 144 | """ 145 | return self._request('POST', url, data, headers=headers) 146 | 147 | def put(self, url, data, headers=None): 148 | """ 149 | Perform an HTTP PUT request for a given url. 150 | Returns the response object. 151 | """ 152 | return self._request('PUT', url, data, headers=headers) 153 | 154 | def delete(self, url, headers=None): 155 | """ 156 | Perform an HTTP DELETE request for a given url. 157 | Returns the response object. 158 | """ 159 | return self._request('DELETE', url, headers=headers) 160 | 161 | # Proleptic Gregorian dates and strftime before 1900 « Python recipes 162 | # ActiveState Code: http://bit.ly/9t0JKb via @addthis 163 | 164 | def _findall(self, text, substr): 165 | # Also finds overlaps 166 | sites = [] 167 | i = 0 168 | while 1: 169 | j = text.find(substr, i) 170 | if j == -1: 171 | break 172 | sites.append(j) 173 | i = j + 1 174 | return sites 175 | 176 | # Every 28 years the calendar repeats, except through century leap 177 | # years where it's 6 years. But only if you're using the Gregorian 178 | # calendar. ;) 179 | 180 | def _strftime(self, dt, fmt): 181 | if self._illegal_s.search(fmt): 182 | raise TypeError("This strftime implementation does not handle %s") 183 | if dt.year > 1900: 184 | return dt.strftime(fmt) 185 | year = dt.year 186 | # For every non-leap year century, advance by 187 | # 6 years to get into the 28-year repeat cycle 188 | delta = 2000 - year 189 | off = 6 * (delta // 100 + delta // 400) 190 | year = year + off 191 | # Move to around the year 2000 192 | year = year + ((2000 - year) // 28) * 28 193 | timetuple = dt.timetuple() 194 | s1 = time.strftime(fmt, (year,) + timetuple[1:]) 195 | sites1 = self._findall(s1, str(year)) 196 | s2 = time.strftime(fmt, (year + 28,) + timetuple[1:]) 197 | sites2 = self._findall(s2, str(year + 28)) 198 | sites = [] 199 | for site in sites1: 200 | if site in sites2: 201 | sites.append(site) 202 | s = s1 203 | syear = "%4d" % (dt.year, ) 204 | for site in sites: 205 | s = s[:site] + syear + s[site + 4:] 206 | return s 207 | 208 | def _json_encode(self, data, ensure_ascii=False): 209 | 210 | def _any(data): 211 | DATE_FORMAT = "%Y-%m-%d" 212 | TIME_FORMAT = "%H:%M:%S" 213 | ret = None 214 | if isinstance(data, (list, tuple)): 215 | ret = _list(data) 216 | elif isinstance(data, dict): 217 | ret = _dict(data) 218 | elif isinstance(data, decimal.Decimal): 219 | ret = str(data) 220 | elif isinstance(data, datetime.datetime): 221 | ret = self._strftime(data, 222 | "%s %s" % (DATE_FORMAT, TIME_FORMAT)) 223 | elif isinstance(data, datetime.date): 224 | ret = self._strftime(data, DATE_FORMAT) 225 | elif isinstance(data, datetime.time): 226 | ret = data.strftime(TIME_FORMAT) 227 | else: 228 | ret = data 229 | return ret 230 | 231 | def _list(data): 232 | ret = [] 233 | for v in data: 234 | ret.append(_any(v)) 235 | return ret 236 | 237 | def _dict(data): 238 | ret = {} 239 | for k, v in data.items(): 240 | # Neo4j doesn't allow 'null' properties 241 | if v: 242 | ret[k] = _any(v) 243 | return ret 244 | ret = _any(data) 245 | return json.dumps(ret, ensure_ascii=ensure_ascii) 246 | 247 | def _request(self, method, url, data={}, headers={}): 248 | global CACHE, DEBUG 249 | splits = urlsplit(url) 250 | scheme = splits.scheme 251 | # Not used, it makes pyflakes happy 252 | # hostname = splits.hostname 253 | # port = splits.port 254 | username = splits.username or self.username 255 | password = splits.password or self.password 256 | headers = headers or {} 257 | if DEBUG: 258 | httplib2.debuglevel = 1 259 | else: 260 | httplib2.debuglevel = 0 261 | if CACHE: 262 | headers['Cache-Control'] = 'no-cache' 263 | http = httplib2.Http(".cache") 264 | else: 265 | http = httplib2.Http() 266 | if scheme.lower() == 'https': 267 | http.add_certificate(self.key_file, self.cert_file, self.url) 268 | headers['Accept'] = 'application/json' 269 | headers['Accept-Encoding'] = '*' 270 | headers['Accept-Charset'] = 'ISO-8859-1,utf-8;q=0.7,*;q=0.7' 271 | # I'm not sure on the right policy about cache with Neo4j REST Server 272 | # headers['Cache-Control'] = 'no-cache' 273 | # TODO: Handle all requests with the same Http object 274 | headers['Connection'] = 'close' 275 | 276 | if username and password: 277 | credentials = "%s:%s" % (username, password) 278 | base64_credentials = base64.encodestring(credentials) 279 | authorization = "Basic %s" % base64_credentials[:-1] 280 | headers['Authorization'] = authorization 281 | headers['Remote-User'] = username 282 | if method in ("POST", "PUT"): 283 | headers['Content-Type'] = 'application/json' 284 | # Thanks to Yashh: http://bit.ly/cWsnZG 285 | # Don't JSON encode body when it starts with "http://" to set inde 286 | if isinstance(data, (str, unicode)) and data.startswith('http://'): 287 | body = data 288 | else: 289 | ## OrientDB reset Socket if body is '{}' 290 | if data is not None and len(data) > 0 : 291 | body = self._json_encode(data, ensure_ascii=True) 292 | else: 293 | body = ''; 294 | try: 295 | response, content = http.request(url, method, headers=headers, 296 | body=body) 297 | return response, content 298 | except AttributeError: 299 | raise Exception("Unknown error. Is the server running?") -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | import unittest 5 | from client import * 6 | import random 7 | 8 | username = 'root' 9 | password = 'AFE1D46E7A5FC722CBD5821644B03BD57CC782B7A1585D438A0414CE0B8DE73D' 10 | url = 'http://localhost:2480' 11 | server = Server(url=url, username=username, password=password) 12 | 13 | #dont fail me! 14 | def randomName(prefix, loop=2): 15 | def randomNumber(): 16 | return random.randrange(random.randint(1, 10000), random.randint(10001, 99999), 4) 17 | 18 | name = [prefix] 19 | 20 | while loop > -1: 21 | name.append(str(randomNumber())) 22 | loop -= 1 23 | 24 | return '_'.join(name) 25 | 26 | 27 | db_name = randomName('test_DB_', 4) 28 | class_db_name = randomName('database_', 2) 29 | doc_db_name = randomName('doc_db', 1) 30 | doc_name = randomName('document_', 3) 31 | doc_class_name = randomName('doc_class', 2) 32 | class_name = randomName('class_name_', 1) 33 | document_rid = '' 34 | 35 | class ServerTests(unittest.TestCase): 36 | def test_can_retrieve_server_info(self): 37 | try: 38 | info = server.info() 39 | result = True 40 | 41 | if 'storages' not in server: 42 | result = False 43 | except CompassException, e: 44 | result = False 45 | 46 | self.assertTrue(result) 47 | 48 | 49 | class CreationTests(unittest.TestCase): 50 | def setUp(self): 51 | self.db_name = db_name 52 | 53 | def test_can_create_database(self): 54 | try: 55 | server.database(name=self.db_name, create=True) 56 | result = True 57 | except CompassException, e: 58 | result = False 59 | 60 | self.assertTrue(result) 61 | 62 | def test_can_create_class(self): 63 | try: 64 | database = server.database(name=class_db_name, create=True) 65 | database.klass(name=class_name, create=True) 66 | 67 | result = True 68 | except CompassException, e: 69 | result = False 70 | 71 | self.assertTrue(result) 72 | 73 | def test_can_create_document_on_database(self): 74 | try: 75 | global document_rid 76 | 77 | database = server.database(name=doc_db_name, create=True) 78 | document = database.document(name=doc_name, create=True, first_name='mark') 79 | document_rid= document.rid 80 | result = True 81 | except CompassException, e: 82 | result = False 83 | 84 | self.assertTrue(result) 85 | 86 | def test_can_create_document_on_class(self): 87 | try: 88 | database = server.database(name=randomName('doc_class_db', 3), create=True) 89 | klass = database.klass(name=class_name, create=True) 90 | document = klass.document(name=doc_name, create=True, first_name='MARK') 91 | result = True 92 | except CompassException, e: 93 | result = False 94 | 95 | self.assertTrue(result) 96 | 97 | def test_can_create_property_on_class(self): 98 | try: 99 | database = server.database(name=randomName('doc_class_db', 3), create=True) 100 | klass = database.klass(name=randomName('random_class', 2), create=True) 101 | klass.property(name=randomName('property', 1)) 102 | 103 | if len(klass.schema) > 0: 104 | result = True 105 | else: 106 | reslut = False 107 | except CompassException, e: 108 | result = False; 109 | 110 | self.assertTrue(result) 111 | 112 | 113 | class RetrievalTests(unittest.TestCase): 114 | def test_can_retrieve_database(self): 115 | try: 116 | server.database(name=db_name) 117 | result = True 118 | except CompassException, e: 119 | result = False 120 | 121 | self.assertTrue(result) 122 | 123 | 124 | def test_can_retrieve_class(self): 125 | try: 126 | database = server.database(name=class_db_name) 127 | database.klass(name=class_name) 128 | 129 | result = True 130 | except CompassException, e: 131 | result = False 132 | 133 | self.assertTrue(result) 134 | 135 | 136 | class DatabaseTests(unittest.TestCase): 137 | def test_can_connect_to_database(self): 138 | try: 139 | database = server.database(name=db_name) 140 | 141 | database.connect() 142 | 143 | result = True 144 | except CompassException, e: 145 | result = False 146 | 147 | self.assertTrue(result) 148 | 149 | def test_can_query_database(self): 150 | try: 151 | query = 'select * from %s' % (class_name) 152 | database = server.database(name=class_db_name) 153 | klass = database.query(query=query) 154 | 155 | if isinstance(klass, Klass): 156 | result = True 157 | else: 158 | reslut = False 159 | except CompassException, e: 160 | result = False 161 | 162 | self.assertTrue(result) 163 | 164 | 165 | class DocumentTests(unittest.TestCase): 166 | def setUp(self): 167 | self.database = server.database(name=doc_db_name) 168 | 169 | def test_can_retrieve_document_value(self): 170 | try: 171 | global document_rid 172 | 173 | document = self.database.document(rid=document_rid) 174 | result = True 175 | except CompassException, e: 176 | result = False 177 | 178 | self.assertTrue(result) 179 | 180 | def test_can_set_document_value(self): 181 | try: 182 | global document_rid 183 | 184 | document = self.database.document(rid=document_rid) 185 | result = True 186 | 187 | document[u'last name'] = 'ahhhhh' 188 | except CompassException, e: 189 | result = False 190 | 191 | self.assertTrue(result) 192 | 193 | def test_can_connect_document_by_rid(self): 194 | try: 195 | global document_rid 196 | 197 | document = self.database.document(rid=document_rid) 198 | document2 = self.database.document(create=True, data={}) 199 | id2 = document2.rid 200 | 201 | document.relate(field='knows', rid=id2) 202 | 203 | result = True 204 | except CompassException, e: 205 | result = False 206 | 207 | self.assertTrue(result) 208 | 209 | def test_can_connect_document_by_document(self): 210 | try: 211 | global document_rid 212 | 213 | document = self.database.document(rid=document_rid) 214 | document2 = self.database.document(create=True, data={}) 215 | 216 | document.relate(field='knows', document=document2) 217 | 218 | result = True 219 | except CompassException, e: 220 | result = False 221 | 222 | self.assertTrue(result) 223 | 224 | def test_can_save_docuemnt(self): 225 | try: 226 | global document_rid 227 | 228 | document = self.database.document(rid=document_rid) 229 | 230 | document['who'] = 'me' 231 | 232 | document.save() 233 | 234 | result = True 235 | except CompassException, e: 236 | result = False 237 | 238 | self.assertTrue(result) 239 | 240 | def test_can_delete_document(self): 241 | try: 242 | global document_rid 243 | 244 | document = self.database.document(name='test to be deleted', create=True) 245 | 246 | document.delete() 247 | 248 | result = True 249 | except CompassException, e: 250 | result = False 251 | 252 | self.assertTrue(result) 253 | 254 | 255 | class KlassTests(unittest.TestCase): 256 | def setUp(self): 257 | self.database = server.database(name=randomName('class_test_db', 3), create=True) 258 | self.klass = self.database.klass(name=randomName('random_class', 2), create=True) 259 | 260 | def test_can_create_property(self): 261 | try: 262 | original_len = len(self.klass.schema) 263 | 264 | self.klass.property(name=randomName('prop_', 3)) 265 | 266 | if len(self.klass.schema) > original_len: 267 | result = True 268 | else: 269 | result = False 270 | except CompassException, e: 271 | result = False; 272 | 273 | self.assertTrue(result) 274 | 275 | def test_can_delete_property(self): 276 | try: 277 | self.klass.property(name=randomName('prop1_', 3)) 278 | self.klass.property(name=randomName('prop2_', 3)) 279 | 280 | original_len = len(self.klass.schema) 281 | keys = self.klass.schema.keys() 282 | 283 | del self.klass.schema[keys[0]] 284 | 285 | if len(self.klass.schema) < original_len: 286 | result = True 287 | else: 288 | result = False 289 | except CompassException, e: 290 | result = False; 291 | 292 | self.assertTrue(result) 293 | 294 | def test_can_run_query_and_get_results(self): 295 | try: 296 | class_name = randomName('new_class', 2) 297 | new_class = self.database.klass(name=class_name, create=True) 298 | doc1 = new_class.document(name=randomName('random_doc', 3)) 299 | doc2 = new_class.document(name=randomName('random_doc', 3)) 300 | query = 'select * from %s' % (class_name) 301 | new_class.query(query=query) 302 | 303 | if len(new_class) == 2: 304 | result = True 305 | else: 306 | result = False 307 | except CompassException, e: 308 | result = False 309 | 310 | self.assertTrue(result) 311 | 312 | if __name__ == '__main__': 313 | server_suite = unittest.TestLoader().loadTestsFromTestCase(ServerTests) 314 | unittest.TextTestRunner(verbosity=3).run(server_suite) 315 | 316 | creation_suite = unittest.TestLoader().loadTestsFromTestCase(CreationTests) 317 | unittest.TextTestRunner(verbosity=3).run(creation_suite) 318 | 319 | retrieval_suite = unittest.TestLoader().loadTestsFromTestCase(RetrievalTests) 320 | unittest.TextTestRunner(verbosity=3).run(retrieval_suite) 321 | 322 | database_suite = unittest.TestLoader().loadTestsFromTestCase(DatabaseTests) 323 | unittest.TextTestRunner(verbosity=3).run(database_suite) 324 | 325 | document_suite = unittest.TestLoader().loadTestsFromTestCase(DocumentTests) 326 | unittest.TextTestRunner(verbosity=3).run(document_suite) 327 | 328 | klass_suite = unittest.TestLoader().loadTestsFromTestCase(KlassTests) 329 | unittest.TextTestRunner(verbosity=3).run(klass_suite) --------------------------------------------------------------------------------