├── .gitignore ├── LICENSE ├── MANIFEST ├── README.md ├── datomic ├── __init__.py ├── datomic.py ├── datomic_test.py └── schema.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | *.swp 39 | *.swo 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.py 3 | datomic/__init__.py 4 | datomic/datomic.py 5 | datomic/datomic_test.py 6 | datomic/schema.py 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | datomic-py 2 | ========== 3 | 4 | A Python library for the Datomic REST API. 5 | 6 | While nothing will ever match the speed or power of Datomic's Clojure interface, this library will work well for the basics. 7 | 8 | 9 | ```shell 10 | pip install datomic 11 | ``` 12 | 13 | Connect 14 | ======= 15 | 16 | To connect to database "test" in store "mem" on localhost at port 8888: 17 | 18 | ```python 19 | from datomic import * 20 | 21 | db = DB('localhost', 8888, 'mem', 'test') 22 | 23 | # create the database 24 | db.create() 25 | True 26 | 27 | # get the state 28 | db.info() 29 | {'basis-t': 62, 'db/alias': 'mem/test'} 30 | ``` 31 | 32 | 33 | Schema 34 | ====== 35 | 36 | You do not need to define a schema. If you want to, it is just a bunch of nested tuples. 37 | 38 | Unless specified otherwise, each attribute is assumed to be a string with a cardinality of one. 39 | 40 | ```python 41 | 42 | S=( 43 | ('person', 44 | ('name', FULL, "A Persons name"), 45 | ('email', FULL, "A Person's email"), 46 | ('age', LONG, "A Person's age"), 47 | ('likes', REF, MANY, ISCOMP, "A Persons likes"), 48 | ), 49 | ('item', 50 | ('name', FULL), 51 | ('sku', UNIQ), 52 | ('active', BOOL), 53 | ('cat', MANY, ENUM('cat','dog','pony','horse','gerbil','sloth')), 54 | ), 55 | ('review', 56 | ('person', REF), 57 | ('item', REF), 58 | ('stars', LONG), 59 | ), 60 | ) 61 | ``` 62 | 63 | You can pass the schema to DB when connecting. 64 | 65 | ```python 66 | 67 | db = DB(host, port, store, dbname, schema=S) 68 | 69 | 70 | # get the schema data 71 | 72 | db.schema.schema 73 | 74 | ['{:db/id #db/id[:db.part/db] 75 | :db/ident :person/name 76 | :db/fulltext true 77 | :db/doc "A Persons name" 78 | :db/valueType :db.type/string 79 | :db/cardinality :db.cardinality/one 80 | :db.install/_attribute :db.part/db}', 81 | '...', 82 | ] 83 | 84 | # transact the schema 85 | 86 | db.tx_schema() 87 | 88 | 89 | ``` 90 | 91 | For a more comprehensive schema example, see [datomic/datomic_test.py](datomic/datomic_test.py) 92 | 93 | 94 | 95 | 96 | Transact 97 | ======== 98 | 99 | ```python 100 | 101 | # send raw edn string to db.tx() to transact 102 | 103 | resp = db.tx('{:db/id #db/id[:db.part/user] :person/name "Bob"}') 104 | 105 | {'db-before': {}, 'db-after': {}, 'tempids': [] } 106 | 107 | 108 | # or, start a new transaction to accumulate many datums by calling db.tx() with no args 109 | 110 | tx = db.tx() 111 | 112 | # `person` will hold a tempid and resolve to an entity after the tx is executed 113 | 114 | person = tx.add("person/", { 115 | 'name': "John Doe" , 116 | 'age': 25, 117 | }) 118 | 119 | # using "ns/" followed by saves some typing 120 | 121 | item = tx.add("item/", { 122 | 'name': 'Item 1', 123 | 'sku': 'item-1-sku', 124 | 'active': True, 125 | 'cat': ['cat','dog'], 126 | }) 127 | 128 | # another new entity, with a ref to our `person` 129 | 130 | review = tx.add("review/", { 131 | 'item': item, 132 | 'stars': 4, 133 | 'person': person, 134 | }) 135 | 136 | # we can nest a new entity in another entity 137 | 138 | review2 = tx.add("review/", { 139 | 'item': item, 140 | 'stars': 5, 141 | 'person': tx.add("person/", { 142 | 'name': 'Nested Person', 143 | 'age': 22, 144 | }), 145 | }) 146 | 147 | 148 | # add another datum to `person` 149 | 150 | tx.add(person, 'person/likes', item) 151 | 152 | # does exactly the same thing as the previous example 153 | 154 | person.add('person/likes', item) 155 | 156 | # see our tempids so far 157 | 158 | print person, item, review, review2 159 | {'db/id': -1} {'db/id': -2} {'db/id': -4} {'db/id': -6} 160 | 161 | # send the tx to datomic 162 | 163 | tx.execute() 164 | 165 | {'db-after': {'basis-t': 1042, 'db/alias': 'mem/test'}, 166 | 'db-before': {'basis-t': 1040, 'db/alias': 'mem/test'}, 167 | 'tx-data': [{'a': 50, 'added': True, 'e': 13194139534354, 'tx': 13194139534354, 'v': datetime.datetime(2013, 11, 9, 18, 55, 56, 657000, tzinfo=)}, {'....'}] 168 | } 169 | 170 | # all entity ids are automatically resolved 171 | 172 | print person, item, review, review2 173 | 174 | {'db/id': 17592186045459} {'db/id': 17592186045460} {'db/id': 17592186045462} {'db/id': 17592186045464} 175 | 176 | # access to the entity ids 177 | 178 | print person.eid, item.eid, review.eid, review2.eid 179 | 180 | 17592186045459 17592186045460 17592186045462 17592186045464 181 | 182 | # edn format 183 | 184 | print unicode(person) 185 | 186 | #db/id[:db.part/user 17592186045459] 187 | 188 | ``` 189 | 190 | 191 | 192 | 193 | 194 | Entity 195 | ====== 196 | 197 | ```python 198 | 199 | # fetch an entity 200 | 201 | db.e(person) 202 | 203 | {'person/age': 25, 'person/likes': ({'item/name': 'Item 1', 'item/sku': 'item-1-sku', 'item/cat': set(['dog', 'cat']), 'item/active': True, 'db/id': 17592186045460},), 'db/id': 17592186045459, 'person/name': 'John Doe'} 204 | 205 | db.e(item.eid) 206 | 207 | {'item/name': 'Item 1', 'item/sku': 'item-1-sku', 'item/cat': set(['dog', 'cat']), 'item/active': True, 'db/id': 17592186045460} 208 | 209 | db.e(17592186045462) 210 | 211 | {'review/person': {'db/id': 17592186045459}, 'review/stars': 4, 'db/id': 17592186045462, 'review/item': {'db/id': 17592186045460}} 212 | 213 | # add datums to an entity 214 | 215 | tx2 = db.tx() 216 | person2 = tx2.add(person, 'person/email', 'jdoe@gmail.com') 217 | tx2.execute() 218 | 219 | person == person2 220 | 221 | False 222 | 223 | db.e(person2) 224 | 225 | {'person/age': 25, 'person/email': 'jdoe@gmail.com', 'db/id': 17592186045440, 'person/name': 'John Doe', 'person/likes': ({'item/name': 'Item 1', 'item/sku': 'item-1-sku', 'item/cat': set(['dog', 'cat']), 'item/active': True, 'db/id': 17592186045441},)} 226 | 227 | 228 | ``` 229 | 230 | 231 | 232 | Query 233 | ===== 234 | 235 | ```python 236 | 237 | # send a edn string to db.q() 238 | 239 | db.q('[...]') 240 | 241 | 242 | # or, use db.find() to build a query in a more pythonic way 243 | 244 | p_name = '?e :person/name ?n' 245 | p_age = '?e :person/age ?a' 246 | p_email = '?e :person/email ?m' 247 | 248 | 249 | # get one 250 | 251 | db.find('?e ?n').where(p_name).one() 252 | 253 | [17592186045457, 'John Doe'] 254 | 255 | 256 | # one to dict 257 | 258 | p = db.find('?e ?n').where(p_name,p_age).hashone() 259 | p.items() 260 | 261 | [('e', 17592186045457), ('n', 'John Doe')] 262 | 263 | 264 | # OR input param 265 | 266 | qa = db.find('?e ?n ?a').where(p_name, p_age)\ 267 | .param('?n', ['Nested Person', 'John Doe']) 268 | qa.all() 269 | 270 | [[17592186045463, 'Nested Person', 22], [17592186045459, 'John Doe', 25]] 271 | 272 | qa.limit(1).all() 273 | 274 | [[17592186045463, 'Nested Person', 22]] 275 | 276 | 277 | # AND input param 278 | 279 | qb = db.find('?e ?n ?a').where(p_name, p_age)\ 280 | .param('?n ?a', ('John Doe', 25)) 281 | qb.all() 282 | 283 | [17592186045459, 'John Doe', 25]] 284 | 285 | 286 | # unify external data 287 | qc = db.find('?e ?n ?external').where(p_name, p_age)\ 288 | .param('?n ?external', 289 | [ ['John Doe', 123.23], ['Nested Person', 456.00]]) 290 | qc.all() 291 | 292 | [[17592186045459, 'John Doe', 123.23], [17592186045463, 'Nested Person', 456.0]] 293 | 294 | ``` 295 | 296 | 297 | 298 | Retract 299 | ======= 300 | 301 | ```python 302 | db.e(review2).get('review/stars') 303 | 304 | 5 305 | 306 | db.retract(review2, 'review/stars', 5) 307 | 308 | db.e(review2) 309 | 310 | {'review/person': {'db/id': 17592186045463}, 'db/id': 17592186045464, 'review/item': {'db/id': 17592186045460}} 311 | 312 | ``` 313 | 314 | 315 | 316 | Datums 317 | ====== 318 | 319 | db.datums() lazily fetches datums in the chunk size you specify. 320 | 321 | ```python 322 | 323 | for r in db.datoms('aevt', a='person/name', limit=100, chunk=100): 324 | print r 325 | 326 | {'a': 62, 'added': True, 'e': 17592186045459, 'tx': 13194139534354, 'v': 'John Doe'} 327 | {'a': 62, 'added': True, 'e': 17592186045463, 'tx': 13194139534354, 'v': 'Nested Person'} 328 | ``` 329 | 330 | ```python 331 | for r in db.datoms('avet', a='item/sku', v='item-1-sku', limit=100): 332 | print r 333 | 334 | {'a': 67, 'added': True, 'e': 17592186045460, 'tx': 13194139534354, 'v': 'item-1-sku'} 335 | ``` 336 | 337 | 338 | 339 | 340 | TODO 341 | ==== 342 | 343 | * A python library for the C++ edn parser is in progress and should be more performant is in the works. 344 | 345 | * More test coverage 346 | 347 | * Better support for traversing the graph 348 | 349 | * Eager loading of entities 350 | 351 | * Materialized Views 352 | -------------------------------------------------------------------------------- /datomic/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __all__ = [ 3 | 'DB', 4 | 'Schema', 5 | 'STRING', 6 | 'KEYWORD', 7 | 'BOOL', 8 | 'LONG', 9 | 'BIGINT', 10 | 'FLOAT', 11 | 'DOUBLE', 12 | 'BIGDEC', 13 | 'REF', 14 | 'INSTANT', 15 | 'UUID', 16 | 'URI', 17 | 'BYTES', 18 | 'ONE', 19 | 'MANY', 20 | 'UNIQ', 21 | 'IDENT', 22 | 'INDEX', 23 | 'FULL', 24 | 'ISCOMP', 25 | 'NOHIST', 26 | 'ENUM', 27 | ] 28 | 29 | from datomic import ( 30 | DB, 31 | ) 32 | 33 | from schema import ( 34 | Schema, 35 | STRING, 36 | KEYWORD, 37 | BOOL, 38 | LONG, 39 | BIGINT, 40 | FLOAT, 41 | DOUBLE, 42 | BIGDEC, 43 | REF, 44 | INSTANT, 45 | UUID, 46 | URI, 47 | BYTES, 48 | ONE, 49 | MANY, 50 | UNIQ, 51 | IDENT, 52 | INDEX, 53 | FULL, 54 | ISCOMP, 55 | NOHIST, 56 | ENUM, 57 | ) 58 | -------------------------------------------------------------------------------- /datomic/datomic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | """ 4 | import datetime 5 | import urllib3 6 | 7 | from pprint import pprint as pp 8 | from termcolor import colored as cl 9 | import logging 10 | 11 | from schema import Schema 12 | 13 | from clj import dumps, loads 14 | import json 15 | from itertools import izip 16 | 17 | 18 | class DB(object): 19 | 20 | def __init__(self, host, port, store, db, schema=None, **kwargs): 21 | """ Assuming the datomic REST service was started this way: 22 | 23 | > bin/rest -p 8888 mem datomic:mem:// 24 | 25 | Then get a connection for database name 'test' like this: 26 | 27 | >>> db = DB("localhost", 8888, "mem", "test", schema=S) 28 | 29 | """ 30 | self.host, self.port, self.store, self.db = host, port, store, db 31 | self.uri_str = "/data/"+ self.store +"/" 32 | self.uri_db = "/data/"+ self.store +"/"+ self.db +"/" 33 | self.uri_q = "/api/query" 34 | self.pool = urllib3.connectionpool.HTTPConnectionPool( 35 | self.host, port=self.port, 36 | timeout=3, maxsize=20, 37 | headers={"Accept":"application/edn", "Connection": "Keep-Alive"}) 38 | "debugging" 39 | for d in ('debug_http','debug_loads'): 40 | setattr(self, d, kwargs.get(d) == True) 41 | "build or use provided Schema" 42 | if isinstance(schema, Schema): 43 | self.schema = schema 44 | elif isinstance(schema, tuple): 45 | self.schema = Schema(schema) 46 | else: 47 | self.schema = None 48 | if schema is None: return 49 | logging.warning("I don't know what to do with schema kwarg of type '%s'" % type(schema)) 50 | 51 | def create(self): 52 | """ Creates the database 53 | >>> db.create() 54 | True 55 | """ 56 | data = data={"db-name":self.db} 57 | self.rest('POST', self.uri_str, status_codes=(200,201), data=data) 58 | return True 59 | 60 | def info(self): 61 | """ Fetch the current db state 62 | >>> db.info() 63 | {:db/alias "store/db", :basis-t ...} 64 | """ 65 | return self.rest('GET', self.uri_db + '-/') 66 | 67 | def tx_schema(self, **kwargs): 68 | """ Builds the data structure edn, and puts it in the db 69 | """ 70 | for s in self.schema.schema: 71 | tx = self.tx(s, **kwargs) 72 | 73 | def tx(self, *args, **kwargs): 74 | """ Executes a raw tx string, or get a new TX object to work with. 75 | 76 | Passing a raw string or list of strings will immedately transact 77 | and return the API response as a dict. 78 | >>> resp = tx('{:db/id #db/id[:db.part/user] :person/name "Bob"}') 79 | {db-before: db-after: tempids: } 80 | 81 | This gets a fresh `TX()` to prepare a transaction with. 82 | >>> tx = db.tx() 83 | 84 | New `E()` object with person/fname and person/lname attributes 85 | >>> person = tx.add('person/', {'fname':'John', 'lname':'Doe'}) 86 | 87 | New state and city objects referencing the state 88 | >>> state = tx.add('loc/state', 'WA') 89 | >>> city = tx.add('loc/city', 'Seattle', 'isin', state) 90 | 91 | Add person/city, person/state, and person/likes refs to the person entity 92 | >>> person.add('person/', {'city': city, 'state': state, 'likes': [city, state]}) 93 | 94 | Excute the transaction 95 | >>> resp = tx.tx() 96 | 97 | The resolved entity ids for our person 98 | >>> person.eid, state.eid, city.eid 99 | 100 | Fetch all attributes, behave like a dict 101 | >>> person.items() 102 | >>> person.iteritems() 103 | 104 | Access attribute as an attribute 105 | >>> person['person/name'] 106 | 107 | See `TX()` for options. 108 | 109 | """ 110 | if 0 == len(args): return TX(self) 111 | ops = [] 112 | for op in args: 113 | if isinstance(op, list): ops += op 114 | elif isinstance(op, (str,unicode)): ops.append(op) 115 | if 'debug' in kwargs: pp(ops) 116 | tx_proc ="[ %s ]" % "".join(ops) 117 | x = self.rest('POST', self.uri_db, data={"tx-data": tx_proc}) 118 | return x 119 | 120 | def e(self, eid): 121 | """Get an Entity 122 | """ 123 | ta = datetime.datetime.now() 124 | rs = self.rest('GET', self.uri_db + '-/entity', data={'e':int(eid)}, parse=True) 125 | tb = datetime.datetime.now() - ta 126 | print cl('<<< fetched entity %s in %sms' % (eid, tb.microseconds/1000.0), 'cyan') 127 | return rs 128 | 129 | def retract(self, e, a, v): 130 | """ redact the value of an attribute 131 | """ 132 | ta = datetime.datetime.now() 133 | ret = u"[:db/retract %i :%s %s]" % (e, a, dump_edn_val(v)) 134 | rs = self.tx(ret) 135 | tb = datetime.datetime.now() - ta 136 | print cl('<<< retracted %s,%s,%s in %sms' % (e,a,v, tb.microseconds/1000.0), 'cyan') 137 | return rs 138 | 139 | 140 | def datoms(self, index='aevt', e='', a='', v='', 141 | limit=0, offset=0, chunk=100, 142 | start='', end='', since='', as_of='', history='', **kwargs): 143 | """ Returns a lazy generator that will only fetch groups of datoms 144 | at the chunk size specified. 145 | 146 | http://docs.datomic.com/clojure/index.html#datomic.api/datoms 147 | """ 148 | assert index in ['aevt','eavt','avet','vaet'], "non-existant index" 149 | data = {'index': index, 150 | 'a': ':{0}'.format(a) if a else '', 151 | 'v': dump_edn_val(v) if v else '', 152 | 'e': int(e) if e else '', 153 | 'offset': offset or 0, 154 | 'start': start, 155 | 'end': end, 156 | 'limit': limit, 157 | 'history': 'true' if history else '', 158 | 'as-of': int(as_of) if as_of else '', 159 | 'since': int(since) if since else '', 160 | } 161 | data['limit'] = offset + chunk 162 | rs = True 163 | while rs and (data['offset'] < (limit or 1000000000)): 164 | ta = datetime.datetime.now() 165 | rs = self.rest('GET', self.uri_db + '-/datoms', data=data, parse=True) 166 | if not len(rs): 167 | rs = False 168 | tb = datetime.datetime.now() - ta 169 | print cl('<<< fetched %i datoms at offset %i in %sms' % ( 170 | len(rs), data['offset'], tb.microseconds/1000.0), 'cyan') 171 | for r in rs: yield r 172 | data['offset'] += chunk 173 | 174 | def rest(self, method, uri, data=None, status_codes=None, parse=True, **kwargs): 175 | """ Rest helpers 176 | """ 177 | r = self.pool.request_encode_body(method, uri, fields=data, encode_multipart=False) 178 | if not r.status in (status_codes if status_codes else (200,201)): 179 | print cl('\n---------\nURI / REQUEST TYPE : %s %s' % (uri, method), 'red') 180 | print cl(data, 'red') 181 | print r.headers 182 | raise Exception, "Invalid status code: %s" % r.status 183 | if not parse: 184 | " return raw urllib3 response" 185 | return r 186 | if not self.debug_loads: 187 | " return parsed edn" 188 | return loads(r.data) 189 | "time edn parse time and return parsed edn" 190 | return self.debug(loads, args=(r_data, ), kwargs={}, 191 | fmt='<<< parsed edn datastruct in {ms}ms', color='green') 192 | 193 | def debug(self, defn, args, kwargs, fmt=None, color='green'): 194 | """ debug timing, colored terminal output 195 | """ 196 | ta = datetime.datetime.now() 197 | rs = defn(*args, **kwargs) 198 | tb = datetime.datetime.now() - ta 199 | fmt = fmt or "processed {defn} in {ms}ms" 200 | logmsg = fmt.format(ms=tb.microseconds/1000.0, defn=defn) 201 | "terminal output" 202 | print cl(logmsg, color) 203 | "logging output" 204 | logging.debug(logmsg) 205 | return rs 206 | 207 | def q(self, q, inputs=None, limit='', offset='', history=False): 208 | """ query 209 | """ 210 | if not q.strip().startswith("["): q = "[ {0} ]".format(q) 211 | args = u'[ {:db/alias "%(store)s/%(db)s" %(hist)s} %(inputs)s ]' % dict( 212 | store = self.store, 213 | db = self.db, 214 | hist = ':history true' if history==True else '', 215 | inputs = " ".join(inputs or [])) 216 | data = {"args": args, 217 | "q": q, 218 | "offset": offset or '', 219 | "limit": limit or '', 220 | } 221 | return self.rest('GET', self.uri_q, data=data, parse=True) 222 | 223 | def find(self, *args, **kwargs): 224 | " new query builder on current db" 225 | return Query(*args, db=self, schema=self.schema) 226 | 227 | 228 | 229 | 230 | 231 | 232 | class Query(object): 233 | """ chainable query builder" 234 | 235 | >>> db.find('?e ?a') # default find 236 | >>> q.where() # with add 237 | >>> q.ins() # in add 238 | """ 239 | 240 | def __init__(self, find, db=None, schema=None): 241 | self.db = db 242 | self.schema = schema 243 | self._find = [] 244 | self._where = [] 245 | self._input = [] 246 | self._limit = None 247 | self._offset = None 248 | self._history = False 249 | self.find(find) 250 | 251 | def __repr__(self): 252 | return " ".join([str(self._find), str(self._in), str(self._where)]) 253 | 254 | def find(self, *args, **kwargs): 255 | " :find " 256 | if args[0] is all: 257 | pass # finds all 258 | else: 259 | [(self._find.append(x)) for x in args] 260 | return self 261 | 262 | def where(self, *args, **kwargs): 263 | " :where " 264 | [(self._where.append(x)) for x in args] 265 | return self 266 | 267 | def fulltext(self, attr, s, q, e, v): 268 | self._where.append("(fulltext $ {0} {1}) [[{2} {3}]]".format(attr, s, e, v)) 269 | self._input.append((s, q)) 270 | 271 | def param(self, *args, **kwargs): 272 | " :in " 273 | for first, second in pairwise(args): 274 | if isinstance(second, list): 275 | if not isinstance(second[0], list): 276 | " add a logical _or_ " 277 | self._input.append(( 278 | u"[{0} ...]".format(first), second)) 279 | else: 280 | " relations, list of list" 281 | self._input.append(( 282 | u"[[{0}]]".format(first), second)) 283 | elif isinstance(second, tuple): 284 | " tuple " 285 | self._input.append(( 286 | u"[{0}]".format(first), list(second))) 287 | else: 288 | " nothing special " 289 | self._input.append((first,second)) 290 | return self 291 | 292 | def limit(self, limit): 293 | self._limit = limit 294 | return self 295 | def offset(self, offset): 296 | self._offset = offset 297 | return self 298 | def history(self, history): 299 | self._offset = history 300 | return self 301 | 302 | def hashone(self): 303 | "execute query, get back" 304 | rs = self.one() 305 | if not rs: 306 | return {} 307 | else: 308 | finds = " ".join(self._find).split(' ') 309 | return dict(zip((x.replace('?','') for x in finds), rs)) 310 | 311 | def one(self): 312 | "execute query, get a single list" 313 | self.limit(1) 314 | rs = self.all() 315 | if not rs: 316 | return None 317 | else: 318 | return rs[0] 319 | 320 | def all(self): 321 | " execute query, get all list of lists" 322 | query,inputs = self._toedn() 323 | return self.db.q(query, 324 | inputs = inputs, 325 | limit = self._limit, 326 | offset = self._offset, 327 | history = self._history) 328 | 329 | def _toedn(self): 330 | """ prepare the query for the rest api 331 | """ 332 | finds = u"" 333 | inputs = u"" 334 | wheres = u"" 335 | args = [] 336 | ": in and args" 337 | for a,b in self._input: 338 | inputs += " {0}".format(a) 339 | args.append(dump_edn_val(b)) 340 | if inputs: 341 | inputs = u":in ${0}".format(inputs) 342 | " :where " 343 | for where in self._where: 344 | if isinstance(where, (str,unicode)): 345 | wheres += u"[{0}]".format(where) 346 | elif isinstance(where, (list)): 347 | wheres += u" ".join([u"[{0}]".format(w) for w in where]) 348 | " find: " 349 | if self._find == []: #find all 350 | fs = set() 351 | for p in wheres.replace('[',' ').replace(']',' ').split(' '): 352 | if p.startswith('?'): 353 | fs.add(p) 354 | self._find = list(fs) 355 | finds = " ".join(self._find) 356 | " all togethr now..." 357 | q = u"""[ :find {0} {1} :where {2} ]""".\ 358 | format( finds, inputs, wheres) 359 | return q,args 360 | 361 | 362 | 363 | 364 | 365 | class E(dict): 366 | """ An entity and its db, optionally a tx. 367 | """ 368 | def __init__(self, e, db=None, tx=None): 369 | """ Represents an entity in the db, 370 | or a tempid in a non-committed state. 371 | 372 | >>> person = E(1, db) 373 | >>> person.eid 374 | 1 375 | 376 | Fetch all attributes, behave like a dict 377 | >>> person.items() 378 | 379 | Iterator just like a dictionary 380 | >>> person.iteritems() 381 | 382 | Access attribute as an attribute 383 | >>> person['person/name'] 384 | >>> person.get('person/name') 385 | 386 | Access ns attribute with dot notation 387 | >>> person.person 388 | 389 | """ 390 | 391 | assert (db is not None or tx is not None),\ 392 | "A DB or TX object is required" 393 | 394 | self._eid = int(e) 395 | self._db = db or tx.db 396 | self._tx = tx 397 | self._txid = -1 if not tx else tx.txid 398 | self._dict = None 399 | 400 | def __repr__(self): 401 | return "{'db/id': %s}" % cl(self._eid, 'magenta') 402 | def __unicode__(self): 403 | return u"#db/id[:db.part/user %s]" % self._eid 404 | def __int__(self): 405 | return self._eid 406 | 407 | """ compare entity id + at-tx 408 | """ 409 | def __eq__(self, obj): 410 | if not isinstance(obj, E): return False 411 | return self._eid == obj._eid and \ 412 | self._txid == obj._txid 413 | def __ne__(self, obj): 414 | if not isinstance(obj, E): return True 415 | return self._eid != obj._eid or \ 416 | self._txid != obj._txid 417 | 418 | """ compare at-tx 419 | """ 420 | def __lt__(self, obj): 421 | return self._txid < obj._txid 422 | def __gt__(self, obj): 423 | return self._txid > obj._txid 424 | def __le__(self, obj): 425 | return self._txid <= obj._txid 426 | def __ge__(self, obj): 427 | return self._txid >= obj._txid 428 | 429 | """ attributes 430 | """ 431 | @property 432 | def __dict__(self): 433 | 'returns a dictionary with last known state in the db' 434 | if isinstance(self._dict, dict): return self._dict 435 | if self._eid < 0: return {} # uncommitted 436 | self._dict = self._db.e(self.eid) # fetch 437 | return self._dict 438 | 439 | def vpar(self, val): 440 | # TODO - check schema for type,cardinality 441 | if not isinstance(val, dict): return val 442 | return E(val.get('db/id'), db=self._db, tx=self._tx) 443 | 444 | def __getitem__(self, attr, default=None): 445 | val = self.__dict__.get(attr, default) 446 | return self.vpar(val) 447 | 448 | def __getattr__(self, attr, default=None): 449 | val = self.__dict__.get(attr, default) 450 | if val: return self.vpar(v) 451 | 452 | rs, ns = {}, '{0}/'.format(attr) 453 | for k,v in self.__dict__.iteritems(): 454 | if k.startswith(ns): 455 | attr = "/".join(k.split('/')[1:]) 456 | vp = self.vpar(v) 457 | if not attr in rs: 458 | rs[attr] = vp 459 | elif isinstance(rs[attr], list): 460 | rs[attr].append(vp) 461 | else: 462 | rs[attr] = list(rs[attr], vp) 463 | return rs 464 | 465 | @property 466 | def items(self): 467 | return self.__dict__.items 468 | @property 469 | def iteritems(self): 470 | return self.__dict__.iteritems 471 | 472 | @property 473 | def eid(self): 474 | return self._eid 475 | 476 | def add(self, *args, **kwargs): 477 | self._tx.add(self, *args, **kwargs) 478 | 479 | def retract(self, a, v): 480 | assert self.eid > 0, "unresolved entity state, cannot issue retractions" 481 | if not a.startswith(':'): 482 | a = u':%s' % v 483 | self._db.tx(u'[:db/retract {0} {1} {2}]'.\ 484 | format(self.eid, a, dump_edn_val(v))) 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | class TX(object): 498 | """ Accumulate, execute, and resolve tempids 499 | """ 500 | def __init__(self, db): 501 | self.db = db 502 | self.tmpents, self.adds, self.ctmpid, self.txid = [], [], -1, -1 503 | self.resp = None 504 | self.realents = [] 505 | 506 | def __repr__(self): 507 | return "" % len(self) 508 | 509 | def __len__(self): 510 | return len(self.adds or []) 511 | 512 | def __int__(self): 513 | return self.txid 514 | 515 | def add(self, *args, **kwargs): 516 | """ Accumulate datums for the transaction 517 | 518 | Start a transaction on an existing db connection 519 | >>> tx = TX(db) 520 | 521 | Get get an entity object with a tempid 522 | >>> ref = add() 523 | >>> ref = add(0) 524 | >>> ref = add(None) 525 | >>> ref = add(False) 526 | 527 | Entity id passed as first argument (int|long) 528 | >>> tx.add(1, 'thing/name', 'value') 529 | 530 | Shorthand form for multiple attributes sharing a root namespace 531 | >>> tx.add(':thing/', {'name':'value', 'tag':'value'}) 532 | 533 | Attributes with a value of None are ignored 534 | >>> tx.add(':thing/ignored', None) 535 | 536 | Add multiple datums for an attribute with carinality:many 537 | >>> tx.add(':thing/color', ['red','white','blue']) 538 | 539 | """ 540 | 541 | assert self.resp is None, "Transaction already committed" 542 | entity, av_pairs, args = None, [], list(args) 543 | if len(args): 544 | if isinstance(args[0], (int, long)): 545 | " first arg is an entity or tempid" 546 | entity = E(args[0], tx=self) 547 | elif isinstance(args[0], E): 548 | " dont resuse entity from another tx" 549 | if args[0]._tx is self: 550 | entity = args[0] 551 | else: 552 | if int(args[0]) > 0: 553 | " use the entity id on a new obj" 554 | entity = E(int(args[0]), tx=self) 555 | args[0] = None 556 | " drop the first arg" 557 | if entity is not None or args[0] in (None, False, 0): 558 | v = args.pop(0) 559 | " auto generate a temp id?" 560 | if entity is None: 561 | entity = E(self.ctmpid, tx=self) 562 | self.ctmpid -= 1 563 | " a,v from kwargs" 564 | if len(args) == 0 and kwargs: 565 | for a,v in kwargs.iteritems(): 566 | self.addeav(entity, a, v) 567 | " a,v from args " 568 | if len(args): 569 | assert len(args) % 2 == 0, "imbalanced a,v in args: " % args 570 | for first, second in pairwise(args): 571 | if not first.startswith(':'): 572 | first = ':' + first 573 | if not first.endswith('/'): 574 | " longhand used: blah/blah " 575 | if isinstance(second, list): 576 | for v in second: 577 | self.addeav(entity, first, v) 578 | else: 579 | self.addeav(entity, first, second) 580 | continue 581 | elif isinstance(second, dict): 582 | " shorthand used: blah/, dict " 583 | for a,v in second.iteritems(): 584 | self.addeav(entity, "%s%s" % (first, a), v) 585 | continue 586 | elif isinstance(second, (list, tuple)): 587 | " shorthand used: blah/, list|tuple " 588 | for a,v in pairwise(second): 589 | self.addeav(entity, "%s%s" % (first, a), v) 590 | continue 591 | else: 592 | raise Exception, "invalid pair: %s : %s" % (first,second) 593 | "pass back the entity so it can be resolved after tx()" 594 | return entity 595 | 596 | def execute(self, **kwargs): 597 | """ commit the current statements from add() 598 | """ 599 | assert self.resp is None, "Transaction already committed" 600 | try: 601 | self.resp = self.db.tx(list(self.edn_iter), **kwargs) 602 | except Exception: 603 | self.resp = False 604 | raise 605 | else: 606 | self.resolve() 607 | self.adds = None 608 | self.tmpents = None 609 | return self.resp # raw dict response 610 | 611 | def resolve(self): 612 | """ Resolve one or more tempids. 613 | Automatically takes place after transaction is executed. 614 | """ 615 | assert isinstance(self.resp, dict), "Transaction in uncommitted or failed state" 616 | rids = [(v) for k,v in self.resp['tempids'].items()] 617 | self.txid = self.resp['tx-data'][0]['tx'] 618 | rids.reverse() 619 | for t in self.tmpents: 620 | pos = self.tmpents.index(t) 621 | t._eid, t._txid = rids[pos], self.txid 622 | for t in self.realents: 623 | t._txid = self.txid 624 | 625 | def addeav(self, e, a, v): 626 | if v is None: return 627 | self.adds.append((e, a, v)) 628 | if int(e) < 0 and e not in self.tmpents: 629 | self.tmpents.append(e) 630 | elif int(e) > 0 and e not in self.realents: 631 | self.realents.append(e) 632 | 633 | @property 634 | def edn_iter(self): 635 | """ yields edns 636 | """ 637 | for e,a,v in self.adds: 638 | yield u"{%(a)s %(v)s :db/id #db/id[:db.part/user %(e)s ]}" % \ 639 | dict(a=a, v=dump_edn_val(v), e=int(e)) 640 | 641 | 642 | 643 | 644 | def dump_edn_val(v): 645 | " edn simple value dump" 646 | if isinstance(v, (str, unicode)): 647 | return json.dumps(v) 648 | elif isinstance(v, E): 649 | return unicode(v) 650 | else: 651 | return dumps(v) 652 | 653 | def pairwise(iterable): 654 | "s -> (s0,s1), (s2,s3), (s4, s5), ..." 655 | a = iter(iterable) 656 | return izip(a, a) 657 | -------------------------------------------------------------------------------- /datomic/datomic_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | """ 3 | """ 4 | 5 | from datomic import * 6 | from schema import * 7 | import datetime 8 | from pprint import pprint as pp 9 | 10 | 11 | S=( 12 | ('person', 13 | ('name', FULL, "A Persons name"), 14 | ('email', FULL, "A Person's email"), 15 | ('thumb', BYTES, "A Person's avatar"), 16 | ('age', LONG, "A Person's age"), 17 | ('passwd', NOHIST, "A Person's password"), 18 | ('likes', REF, MANY, "A Persons likes"), 19 | ('view', REF, MANY, ISCOMP, "A Person's viewed items"), 20 | ), 21 | ('view', 22 | ('when', INSTANT), 23 | ('from', URI), 24 | ('item', REF), 25 | ), 26 | ('order', 27 | ('uid', UUID, INDEX), 28 | ('date', INSTANT, INDEX), 29 | ('user', REF), 30 | ('track', BIGINT), 31 | ('item', REF, MANY, ISCOMP), 32 | ('tax', FLOAT), 33 | ('ship', DOUBLE), 34 | ('total', BIGDEC), 35 | ('sent', BOOL), 36 | ('idx', INDEX), 37 | ), 38 | ('item', 39 | ('name', FULL), 40 | ('desc', FULL), 41 | ('sku', UNIQ), 42 | ('active', BOOL), 43 | ('amt', FLOAT), 44 | ('cat', MANY, ENUM('cat','dog','pony','horse','gerbil','sloth')), 45 | ), 46 | ('review', 47 | ('user', REF), 48 | ('item', REF), 49 | ('strs', LONG), 50 | ('when', INSTANT), 51 | ) 52 | ) 53 | 54 | 55 | HOST = 'localhost' 56 | PORT = 8888 57 | STORE = 'mem' 58 | DBN = 'pytest3' 59 | 60 | db = DB(HOST, PORT, STORE, DBN, S, 61 | debug_http = True, 62 | debug_loads = False) 63 | 64 | 65 | 66 | def test_all(): 67 | 68 | 69 | " db creation " 70 | created = db.create() 71 | 72 | " schema creation " 73 | schemers = db.tx_schema() #debug=True) 74 | 75 | " transact " 76 | tx2 = db.tx() 77 | 78 | people = [] 79 | 80 | person = tx2.add("person/", { 81 | 'name': "A User" , 82 | 'age': 25, 83 | 'passwd': 'password123', 84 | }) 85 | 86 | item1 = tx2.add("item/", { 87 | 'name': 'Cat House', 88 | 'sku': 'sku-1-%s' % datetime.datetime.now(), 89 | 'active': True, 90 | 'amt': 99.99, 91 | 'desc': 'some description', 92 | 'cat': 'cat', 93 | }) 94 | 95 | tx2.add("order/", { 96 | 'user': person, 97 | 'item': item1, 98 | 'idx': 'test-idx', 99 | }) 100 | 101 | tx2.add("review/", { 102 | 'user': person, 103 | 'item': item1, 104 | 'strs': 3, 105 | }) 106 | 107 | item2 = tx2.add("item/", { 108 | 'name': 'Dog House', 109 | 'sku': 'sku-3-%s' % datetime.datetime.now(), 110 | 'active': True, 111 | 'amt': 199.99, 112 | 'desc': 'some description', 113 | 'cat': 'dog', 114 | }) 115 | 116 | rev2 = tx2.add("review/", { 117 | 'item': item2, 118 | 'strs': 5, 119 | 'user': tx2.add("person/", { 120 | 'name': 'Nested Person', 121 | 'age': 22, }), 122 | }) 123 | 124 | rs2 = tx2.execute() #debug=True) 125 | assert rs2, "TX failed" 126 | 127 | tx3 = db.tx() 128 | person2 = tx3.add(person, "person/email", "tony.landis@gmail.com") 129 | person3 = tx3.add(person2, "person/likes", [item1, item2]) 130 | rs3 = tx3.execute() #debug=True) 131 | assert rs3, "TX failed" 132 | 133 | # auto-resolving 134 | assert person.eid > 1 135 | assert int(item1) > 1 136 | 137 | # entity + at-tx comparision operators 138 | assert person == person 139 | assert person != person2 140 | 141 | # at-tx comparision 142 | assert person < person2 143 | assert person2 > person 144 | 145 | assert person >= person 146 | assert person <= person 147 | 148 | assert person2 >= person 149 | assert person <= person2 150 | 151 | # acts like a dict 152 | print person2.items() 153 | print person2 154 | 155 | # more dictish behavior 156 | for k,v in rev2.iteritems(): 157 | print k,v 158 | 159 | # dict access to full attribute 160 | assert rev2['review/strs'] == 5 161 | 162 | # property style access to a root attr namespace 163 | ns = rev2.review 164 | assert ns['strs'] == 5 165 | 166 | # entity conversion 167 | assert isinstance(ns['item'], E) 168 | assert isinstance(ns['user'], E) 169 | 170 | # walking the tree 171 | assert ns['item'].eid == item2.eid 172 | assert ns['user']['person/name'] == 'Nested Person' 173 | assert ns['user']['person/age'] == 22 174 | assert isinstance(ns['user']['db/id'], int) 175 | 176 | # retract 177 | rs = db.retract(rev2, 'review/strs', 5) 178 | assert rs 179 | 180 | p_name = '?e :person/name ?n' 181 | p_age = '?e :person/age ?a' 182 | p_pass = '?e :person/passwd ?p' 183 | 184 | pname = lambda *a: '{0} :person/name {1}'.format(*a) 185 | 186 | # ONE 187 | one = db.find('?e ?n').where(p_name).one() 188 | assert isinstance(one, list) 189 | assert one[0] 190 | assert one[1] 191 | 192 | # HASH ONE 193 | one = db.find('?e ?n').where(p_name).hashone() 194 | assert isinstance(one, dict) 195 | assert one['e'] 196 | assert one['n'] 197 | 198 | # OR input param 199 | qa = db.find('?e ?n ?a').where(p_name, p_age)\ 200 | .param('?n', ['Nested Person', 'A User']) 201 | 202 | # AND input param 203 | qb = db.find('?e ?n ?a ?p').where(p_name, p_age, p_pass)\ 204 | .param('?n ?p ?a', ('A User', 'password123', 25)) 205 | 206 | # EXTERNAL join 207 | qc = db.find('?e ?n ?external').where(p_name, p_age, p_pass)\ 208 | .param('?e', person.eid)\ 209 | .param('?n ?external', 210 | [ ['A User', 123.23], ['Nested Person', 456]]) 211 | 212 | pp(qa.limit(2).all()) 213 | pp(qb.limit(2).all()) 214 | pp(qc.limit(1000).all()) 215 | 216 | # find all notations 217 | qd = db.find(all).where(pname('?e','?r'), p_age, p_pass) 218 | pp(qd.limit(2).all()) 219 | 220 | # fulltext 221 | qf = db.find('?e ?name ?a').where(p_age) 222 | qf.fulltext(':person/name', '?search', 'user', '?e', '?name') 223 | pp(qf.limit(10).all()) 224 | 225 | # datums 226 | for r in db.datoms('aevt', a='person/name', limit=100): 227 | print r 228 | for r in db.datoms('avet', a='order/idx', v='test-idx', limit=100): 229 | print r 230 | 231 | 232 | if __name__ == '__main__': 233 | test_all() 234 | -------------------------------------------------------------------------------- /datomic/schema.py: -------------------------------------------------------------------------------- 1 | """ SCHEMA 2 | 3 | Required schema attributes default to: 4 | 5 | :db/cardinality 6 | 7 | ONE [default] 8 | MANY 9 | 10 | :db/valueType 11 | 12 | STRING [default] 13 | BOOLEAN 14 | LONG 15 | BIGINT 16 | FLOAT 17 | DOUBLE 18 | BIGDEC 19 | REF 20 | INSTANT 21 | UUID 22 | URI 23 | BYTES 24 | 25 | :db/unique 26 | 27 | UNIQ [:db.unqiue/value] 28 | IDENT [:db.unqiue/identity] 29 | 30 | :db/isComponent 31 | 32 | SCOMP 33 | 34 | :db/noHist 35 | 36 | NOHIST 37 | 38 | """ 39 | 40 | VALUETYPE = ':db/valueType' 41 | CARDINALITY = ':db/cardinality' 42 | UNIQUE = ':db/unique' 43 | INDEX = ':db/index' 44 | FULLTEXT = ':db/fulltext' 45 | ISCOMPONENT = ':db/isComponent' 46 | NOHISTORY = ':db/noHistory' 47 | 48 | 49 | STRING = (VALUETYPE, ':db.type/string') 50 | KEYWORD = (VALUETYPE, ':db.type/keyword') 51 | BOOL = (VALUETYPE, ':db.type/boolean') 52 | LONG = (VALUETYPE, ':db.type/long') 53 | BIGINT = (VALUETYPE, ':db.type/bigint') 54 | FLOAT = (VALUETYPE, ':db.type/float') 55 | DOUBLE = (VALUETYPE, ':db.type/double') 56 | BIGDEC = (VALUETYPE, ':db.type/bigdec') 57 | REF = (VALUETYPE, ':db.type/ref') 58 | INSTANT = (VALUETYPE, ':db.type/instant') 59 | UUID = (VALUETYPE, ':db.type/uuid') 60 | URI = (VALUETYPE, ':db.type/uri') 61 | BYTES = (VALUETYPE, ':db.type/bytes') 62 | ONE = (CARDINALITY, ':db.cardinality/one') 63 | MANY = (CARDINALITY, ':db.cardinality/many') 64 | UNIQ = (UNIQUE, ':db.unique/value') 65 | IDENT = (UNIQUE, ':db.unique/identity') 66 | INDEX = (INDEX, 'true') 67 | FULL = (FULLTEXT, 'true') 68 | ISCOMP = (ISCOMPONENT, 'true') 69 | NOHIST = (NOHISTORY, 'true') 70 | ENUM = lambda *x:x 71 | 72 | 73 | 74 | class Schema(object): 75 | """ 76 | An example schema. 77 | """ 78 | 79 | part = 'db' 80 | schema = [] 81 | cache = None 82 | 83 | def __init__(self, struct, part=None): 84 | """ 85 | Pythonic schemas for Datomic 86 | """ 87 | self.cache = {} 88 | if part: self.part = part 89 | self.build_attributes(struct) 90 | 91 | 92 | """ Schema Preparation 93 | """ 94 | def build_attributes(self, struct): 95 | for outer in struct: 96 | for row in outer: 97 | if type(row) in (str,unicode): 98 | ns = row 99 | #setattr(self.tx, ns, Ns(ns, self.tx)) 100 | elif type(row) in (list,tuple): 101 | self.build_attribute(ns, row) 102 | #setattr(getattr(self.tx, ns), row[0], None) 103 | else: 104 | raise Exception, 'Invalid schema definition at row %s' % row 105 | 106 | def build_attribute(self, ns, struct): 107 | attrs, enums = [],[] 108 | attrs.append((':db/id', '#db/id[:db.part/%s]' % self.part)) 109 | attrs.append((':db/ident', ":%s%s%s" % (ns, '/' if (ns and struct[0]) else '', struct[0]) )) 110 | missing = [VALUETYPE, CARDINALITY] 111 | 112 | for it in struct[1::]: 113 | if type(it) == str: 114 | attrs.append((':db/doc', '"%s"' % it)) 115 | elif 2 == len(it): 116 | attrs.append(it) 117 | if it[0] in missing: missing.remove(it[0]) 118 | else: 119 | enums = it 120 | 121 | if ':db/valueType' in missing: attrs.append(STRING) 122 | if ':db/cardinality' in missing: attrs.append(ONE) 123 | 124 | attrs.append((':db.install/_attribute',':db.part/db')) 125 | self.schema.append("{%s}" % "\n ".join(("%s %s" % (k,v)) for k,v in attrs)) 126 | 127 | if enums: 128 | for option in enums: 129 | self.build_enum(ns, struct[0], option) 130 | 131 | def build_enum(self, ns, ident, option): 132 | if ns: 133 | st = " [:db/add #db/id[:db.part/user] :db/ident :%s.%s/%s] " % \ 134 | (ns, ident, option) 135 | else: 136 | st = " [:db/add #db/id[:db.part/user] :db/ident :%s/%s] " % \ 137 | (ident, option) 138 | self.schema.append(st) 139 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name='datomic', 5 | version='0.1.2dev', 6 | description='Interface to the Datomic REST API', 7 | #long_description=open('README.md').read(), 8 | author='Tony Landis', 9 | author_email='tony.landis@gmail.com', 10 | license="Apache 2", 11 | url='https://github.com/tony-landis/datomic-py', 12 | install_requires=[ 13 | 'edn_format', 14 | 'urllib3', 15 | 'termcolor', 16 | ], 17 | packages = ['datomic', ], 18 | ) 19 | --------------------------------------------------------------------------------