├── pydatomic ├── __init__.py ├── schema.py ├── datomic.py └── edn.py ├── tests ├── flake8.rc ├── test_edn.py └── test_datomic.py ├── requirements.txt ├── .gitignore ├── Makefile ├── setup.py └── README.rst /pydatomic/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = '0.1.0dev' 3 | -------------------------------------------------------------------------------- /tests/flake8.rc: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E128,E226,E231,E302 3 | max-line-length = 120 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | coverage==3.7.1 2 | flake8==2.3.0 3 | mock==1.0.1 4 | nose==1.3.4 5 | 6 | -e . 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | *.swp 29 | env 30 | env3 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Make for pydatomic 3 | # 4 | 5 | HIDE ?= @ 6 | VENV ?= env 7 | VENV3 ?= env3 8 | 9 | all: 10 | 11 | test: test-unit test-flake8 12 | 13 | test-unit: 14 | $(HIDE)$(VENV)/bin/nosetests tests 15 | 16 | test-flake8: 17 | $(HIDE)$(VENV)/bin/flake8 --config=tests/flake8.rc pydatomic 18 | $(HIDE)$(VENV3)/bin/flake8 --config=tests/flake8.rc pydatomic 19 | 20 | coverage: 21 | $(HIDE)$(VENV)/bin/nosetests --with-coverage --cover-erase --cover-inclusive --cover-package=pydatomic pydatomic tests 22 | 23 | prepare-venv: 24 | $(HIDE)virtualenv $(VENV) 25 | $(HIDE)$(VENV)/bin/pip install --upgrade -r requirements.txt 26 | $(HIDE)virtualenv --python /usr/bin/python3.4 $(VENV3) 27 | $(HIDE)$(VENV3)/bin/pip install --upgrade -r requirements.txt 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | setup(name="pydatomic", 7 | version="0.1.0", 8 | author="Graham Stratton", 9 | author_email="gns24@beasts.org", 10 | description="Datomic REST API client", 11 | long_description=open('README.rst').read(), 12 | url="https://github.com/gns24/pydatomic", 13 | install_requires=['requests'], 14 | license='mit', 15 | packages=['pydatomic'], 16 | classifiers=['Development Status :: 4 - Beta', 17 | 'Intended Audience :: Developers', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Topic :: Software Development', 20 | 'Programming Language :: Python', 21 | 'Operating System :: OS Independent'] 22 | ) 23 | -------------------------------------------------------------------------------- /pydatomic/schema.py: -------------------------------------------------------------------------------- 1 | ONE = ":db.cardinality/one" 2 | MANY = ":db.cardinality/many" 3 | IDENTITY = ":db.unique/identity" 4 | VALUE = ":db.unique/value" 5 | STRING = ":db.type/string" 6 | BOOLEAN = "db.type/boolean" 7 | 8 | def Attribute(ident, valueType, doc=None, cardinality=ONE, unique=None, 9 | index=False, fulltext=False, noHistory=False): 10 | """ 11 | Arguments which require clojure nil take Python None 12 | """ 13 | parts = [":db/id #db/id[:db.part/db]"] 14 | parts.append(":db/ident %s" % ident) 15 | parts.append(":db/valueType %s" % valueType) 16 | parts.append(":db/cardinality %s" % cardinality) 17 | if doc is not None: 18 | parts.append(":db/doc " + doc) 19 | parts.append(":db/unique %s" % {IDENTITY:IDENTITY, VALUE:VALUE, 20 | None:'nil'}[unique]) 21 | parts.append(":db/index %s" % {False:'false', True:'true'}[index]) 22 | parts.append(":db/fulltext %s" % {False:'false', True:'true'}[fulltext]) 23 | parts.append(":db/noHistory %s" % {False:'false', True:'true'}[noHistory]) 24 | return '{%s}' % ('\n '.join(parts)) 25 | 26 | def Schema(*attributes): 27 | return attributes 28 | 29 | if __name__ == '__main__': 30 | schema = Schema(Attribute(':task/name', STRING, cardinality=ONE), 31 | Attribute(':task/closed', BOOLEAN), 32 | Attribute(':data/user', STRING)) 33 | for a in schema: 34 | print(a) 35 | -------------------------------------------------------------------------------- /pydatomic/datomic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import requests 3 | from urlparse import urljoin 4 | from pydatomic.edn import loads 5 | 6 | 7 | class Database(object): 8 | def __init__(self, name, conn): 9 | self.name = name 10 | self.conn = conn 11 | 12 | def __getattr__(self, name): 13 | def f(*args, **kwargs): 14 | return getattr(self.conn, name)(self.name, *args, **kwargs) 15 | return f 16 | 17 | class Datomic(object): 18 | def __init__(self, location, storage): 19 | self.location = location 20 | self.storage = storage 21 | 22 | def db_url(self, dbname): 23 | return urljoin(self.location, 'data/') + self.storage + '/' + dbname 24 | 25 | def create_database(self, dbname): 26 | r = requests.post(self.db_url(''), data={'db-name':dbname}) 27 | assert r.status_code in (200, 201), r.text 28 | return Database(dbname, self) 29 | 30 | def transact(self, dbname, data): 31 | data = '[%s\n]' % '\n'.join(data) 32 | r = requests.post(self.db_url(dbname)+'/', data={'tx-data':data}, 33 | headers={'Accept':'application/edn'}) 34 | assert r.status_code in (200, 201), (r.status_code, r.text) 35 | return loads(r.content) 36 | 37 | def query(self, dbname, query, extra_args=[], history=False): 38 | args = '[{:db/alias ' + self.storage + '/' + dbname 39 | if history: 40 | args += ' :history true' 41 | args += '} ' + ' '.join(str(a) for a in extra_args) + ']' 42 | r = requests.get(urljoin(self.location, 'api/query'), 43 | params={'args': args, 'q':query}, 44 | headers={'Accept':'application/edn'}) 45 | assert r.status_code == 200, r.text 46 | return loads(r.content) 47 | 48 | def entity(self, dbname, eid): 49 | r = requests.get(self.db_url(dbname) + '/-/entity', params={'e':eid}, 50 | headers={'Accept':'application/edn'}) 51 | assert r.status_code == 200 52 | return loads(r.content) 53 | 54 | if __name__ == '__main__': 55 | q = """[{ 56 | :db/id #db/id[:db.part/db] 57 | :db/ident :person/name 58 | :db/valueType :db.type/string 59 | :db/cardinality :db.cardinality/one 60 | :db/doc "A person's name" 61 | :db.install/_attribute :db.part/db}]""" 62 | 63 | conn = Datomic('http://localhost:3000/', 'tdb') 64 | db = conn.create_database('cms') 65 | db.transact(q) 66 | db.transact('[{:db/id #db/id[:db.part/user] :person/name "Peter"}]') 67 | r = db.query('[:find ?e ?n :where [?e :person/name ?n]]') 68 | print(r) 69 | eid = r[0][0] 70 | print(db.query('[:find ?n :in $ ?e :where [?e :person/name ?n]]', [eid], history=True)) 71 | print(db.entity(eid)) 72 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pydatomic 2 | ========= 3 | 4 | Python library for accessing the datomic DBMS via the `REST API `_. 5 | Includes a reader for `edn `_. 6 | 7 | REST client 8 | ----------- 9 | 10 | Connections are instances of `datomic.Datomic`: 11 | 12 | >>> from pydatomic.datomic import Datomic 13 | >>> conn = Datomic('http://localhost:3000/', 'tdb') 14 | 15 | The method `create_database(name)` returns a database object which can be used for queries. It has the 16 | same methods as the Datomic connection instance, but you don't pass the database name as the first argument. 17 | 18 | >>> db = conn.create_database('cms') 19 | >>> db.transact(["""{ 20 | ... :db/id #db/id[:db.part/db] 21 | ... :db/ident :person/name 22 | ... :db/valueType :db.type/string 23 | ... :db/cardinality :db.cardinality/one 24 | ... :db/doc "A person's name" 25 | ... :db.install/_attribute :db.part/db}"""]) #doctest: +ELLIPSIS 26 | {':db-after':... 27 | >>> db.transact(['{:db/id #db/id[:db.part/user] :person/name "Peter"}']) #doctest: +ELLIPSIS 28 | {':db-after':... 29 | 30 | >>> r = db.query('[:find ?e ?n :where [?e :person/name ?n]]') 31 | >>> print r #doctest: +ELLIPSIS 32 | ((... u'Peter')) 33 | >>> eid = r[0][0] 34 | 35 | The query function optionally takes arguments to apply to the query and has a keyword argument `history` 36 | for querying the history database: 37 | 38 | >>> print db.query('[:find ?n :in $ ?e :where [?e :person/name ?n]]', [eid], history=True) 39 | ((u'Peter',),) 40 | >>> print db.entity(eid) #doctest: +ELLIPSIS 41 | {':person/name': u'Peter', ':db/id': ...} 42 | 43 | 44 | TBD 45 | ~~~ 46 | 47 | - Support for as-of and since 48 | - Support for data-structure queries instead of just textual ones (need to implement an EDN encoder for that). 49 | 50 | edn parser 51 | ---------- 52 | 53 | Includes a parser for most of EDN (https://github.com/edn-format/edn), featuring: 54 | 55 | - Coroutine-based interface for streaming data 56 | - loads() interface for the rest of the time! 57 | - Strings and characters are converted to unicode before passing to application 58 | - Support for tags 59 | - All structures are returned as immutable objects except dicts, as Python still lacks a frozendict type. 60 | - Symbols and keywords are returned as strings (not unicode) 61 | 62 | TBD 63 | ~~~ 64 | 65 | - Encoder! 66 | - Handle invalid input gracefully 67 | - Check validity of strings for keywords/symbols 68 | - Include a frozendict implementation? 69 | - Create a type for symbols and keywords? 70 | - Better API for adding tag handlers (currently you need to modify the global dictionary!) 71 | - Map exact floating point values to Decimal type? 72 | - Don't call tag handlers whilst parsing the element after a discard 73 | 74 | For Developers 75 | -------------- 76 | 77 | Before push your PR, please run the test: 78 | 79 | $ make prepare-venv 80 | 81 | $ make test 82 | 83 | 84 | License 85 | ------- 86 | 87 | Distributed under the MIT license. 88 | -------------------------------------------------------------------------------- /tests/test_edn.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | from datetime import datetime 4 | from uuid import UUID 5 | from pydatomic import edn 6 | 7 | 8 | class EdnParseTest(unittest.TestCase): 9 | 10 | def test_all_data(self): 11 | data = { 12 | '"helloworld"': "helloworld", 13 | "23": 23, 14 | "23.11": 23.11, 15 | "true": True, 16 | "false": False, 17 | "nil": None, 18 | ":hello": ":hello", 19 | r'"string\"ing"': 'string"ing', 20 | '"string\n"': 'string\n', 21 | '[:hello]':(":hello",), 22 | '-10.4':-10.4, 23 | '"你"': u'你', 24 | '\\€': u'€', 25 | "[1 2]": (1, 2), 26 | "#{true \"hello\" 12}": set([True, "hello", 12]), 27 | '#inst "2012-09-10T23:51:55.840-00:00"': datetime(2012, 9, 10, 23, 51, 55, 840000), 28 | "(\\a \\b \\c \\d)": ("a", "b", "c", "d"), 29 | "{:a 1 :b 2 :c 3 :d 4}": {":a":1, ":b":2, ":c":3,":d":4}, 30 | "[1 2 3,4]": (1,2,3,4), 31 | "{:a [1 2 3] :b #{23.1 43.1 33.1}}": {":a":(1, 2, 3), ":b":frozenset([23.1, 43.1, 33.1])}, 32 | "{:a 1 :b [32 32 43] :c 4}": {":a":1, ":b":(32,32,43), ":c":4}, 33 | "\\你": u"你", 34 | '#db/fn{:lang "clojure" :code "(map l)"}': {':lang':u'clojure', ':code':u'(map l)'}, 35 | "#_ {[#{}] #{[]}} [23[34][32][4]]": (23, (34,), (32,), (4,)), 36 | '(:graham/stratton true \n , "A string with \\n \\"s" true #uuid "f81d4fae7dec11d0a76500a0c91e6bf6")': ( 37 | u':graham/stratton', True, u'A string with \n "s', True, UUID('f81d4fae-7dec-11d0-a765-00a0c91e6bf6') 38 | ), 39 | '[\space \\\xE2\x82\xAC [true []] ;true\n[true #inst "2012-09-10T23:39:43.309-00:00" true ""]]': ( 40 | ' ', u'\u20ac', (True, ()), (True, datetime(2012, 9, 10, 23, 39, 43, 309000), True, '') 41 | ), 42 | ' {true false nil [true, ()] 6 {#{nil false} {nil \\newline} }}': { 43 | None: (True, ()), True: False, 6: {frozenset([False, None]): {None: '\n'}} 44 | }, 45 | '[#{6.22e-18, -3.1415, 1} true #graham #{"pie" "chips"} "work"]': ( 46 | frozenset([6.22e-18, -3.1415, 1]), True, u'work' 47 | ), 48 | '(\\a .5)': (u'a', 0.5), 49 | '(List #{[123 456 {}] {a 1 b 2 c ({}, [])}})': ( 50 | u'List', ((123, 456, {}), {u'a': 1, u'c': ({}, ()), u'b': 2}) 51 | ), 52 | } 53 | 54 | for k, v in data.items(): 55 | self.assertEqual(edn.loads(k), v) 56 | 57 | def test_malformed_data(self): 58 | '''Verify ValueError() exception raise on malformed data''' 59 | data = ["[1 2 3", "@EE", "[@nil tee]"] 60 | for d in data: 61 | self.assertRaises(ValueError, edn.loads, d) 62 | 63 | 64 | if __name__ == '__main__': 65 | unittest.main() 66 | -------------------------------------------------------------------------------- /tests/test_datomic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from datetime import datetime 5 | from mock import Mock, call, patch 6 | from pydatomic.datomic import Database, Datomic, requests 7 | 8 | 9 | class DatomicTest(unittest.TestCase): 10 | 11 | def setUp(self): 12 | self.patchers = [ 13 | patch('pydatomic.datomic.requests'), 14 | ] 15 | self.requests = self.patchers[0].start() 16 | 17 | def tearDown(self): 18 | for each in self.patchers: 19 | each.stop() 20 | 21 | def test_create_db(self): 22 | '''Verify create_database()''' 23 | conn = Datomic('http://localhost:3000/', 'tdb') 24 | 25 | self.requests.post.return_value = Mock(status_code=201) 26 | db = conn.create_database('cms') 27 | self.assertEqual( 28 | self.requests.post.mock_calls, 29 | [ call('http://localhost:3000/data/tdb/', data={'db-name': 'cms'}) ] 30 | ) 31 | 32 | def test_transact(self): 33 | '''Verify transact()''' 34 | conn = Datomic('http://localhost:3000/', 'tdb') 35 | db = Database('db', conn) 36 | 37 | self.requests.post.return_value = Mock(status_code=201, content=( 38 | '{:db-before {:basis-t 63, :db/alias "dev/scratch"}, ' 39 | ':db-after {:basis-t 1000, :db/alias "dev/scratch"}, ' 40 | ':tx-data [{:e 13194139534312, :a 50, :v #inst "2014-12-01T15:27:26.632-00:00", ' 41 | ':tx 13194139534312, :added true} {:e 17592186045417, :a 62, ' 42 | ':v "hello REST world", :tx 13194139534312, :added true}], ' 43 | ':tempids {-9223350046623220292 17592186045417}}' 44 | )) 45 | self.assertEqual( 46 | db.transact('[{:db/id #db/id[:db.part/user] :person/name "Peter"}]'), { 47 | ':db-after': {':db/alias': 'dev/scratch', ':basis-t': 1000}, 48 | ':tempids': {-9223350046623220292: 17592186045417}, 49 | ':db-before': {':db/alias': 'dev/scratch', ':basis-t': 63}, 50 | ':tx-data': ({ 51 | ':e': 13194139534312, ':v': datetime(2014, 12, 1, 15, 27, 26, 632000), 52 | ':added': True, ':a': 50, ':tx': 13194139534312 53 | }, { 54 | ':e': 17592186045417, ':v': 'hello REST world', ':added': True, ':a': 62, ':tx': 13194139534312 55 | }) 56 | } 57 | ) 58 | self.assertEqual( 59 | self.requests.post.mock_calls, [call( 60 | 'http://localhost:3000/data/tdb/db/', 61 | headers={'Accept': 'application/edn'}, 62 | data={ 63 | 'tx-data': ( 64 | '[[\n{\n:\nd\nb\n/\ni\nd\n \n#\nd\nb\n/\ni\nd\n[\n:\nd\nb\n.\np\na\nr\n' 65 | 't\n/\nu\ns\ne\nr\n]\n \n:\np\ne\nr\ns\no\nn\n/\nn\na\nm\ne\n \n"\nP\n' 66 | 'e\nt\ne\nr\n"\n}\n]\n]' 67 | ) 68 | } 69 | )] 70 | ) 71 | 72 | def test_query(self): 73 | '''Verify query()''' 74 | conn = Datomic('http://localhost:3000/', 'tdb') 75 | db = Database('db', conn) 76 | self.requests.get.return_value = Mock(status_code=200, content='[[17592186048482]]') 77 | self.assertEqual(db.query('[:find ?e ?n :where [?e :person/name ?n]]'), ((17592186048482,),)) 78 | self.assertEqual(self.requests.get.mock_calls, [ 79 | call( 80 | 'http://localhost:3000/api/query', 81 | headers={'Accept': 'application/edn'}, 82 | params={'q': '[:find ?e ?n :where [?e :person/name ?n]]', 'args': '[{:db/alias tdb/db} ]'} 83 | ) 84 | ]) 85 | 86 | 87 | if __name__ == "__main__": 88 | unittest.main() 89 | -------------------------------------------------------------------------------- /pydatomic/edn.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | from uuid import UUID 4 | 5 | def encode_string(s): 6 | r""" 7 | >>> print encode_string(u'"Hello, world"\\n') 8 | "\"Hello, world\"\\n" 9 | """ 10 | return '"%s"' % s.encode('utf-8').replace('\\', '\\\\').replace('"', '\\"') 11 | 12 | STOP_CHARS = " ,\n\r\t" 13 | 14 | def coroutine(func): 15 | def start(*args,**kwargs): 16 | cr = func(*args,**kwargs) 17 | cr.next() 18 | return cr 19 | return start 20 | 21 | @coroutine 22 | def printer(): 23 | while True: 24 | value = (yield) 25 | print(value) 26 | 27 | @coroutine 28 | def appender(l): 29 | while True: 30 | l.append((yield)) 31 | 32 | def inst_handler(time_string): 33 | return datetime.strptime(time_string[:23], '%Y-%m-%dT%H:%M:%S.%f') 34 | 35 | tag_handlers = {'inst':inst_handler, 36 | 'uuid':UUID, 37 | 'db/fn':lambda x:x} 38 | 39 | @coroutine 40 | def tag_handler(tag_name): 41 | while True: 42 | c = (yield) 43 | if c in STOP_CHARS+'{"[(\\#': 44 | break 45 | tag_name += c 46 | elements = [] 47 | handler = parser(appender(elements)) 48 | handler.send(c) 49 | while not elements: 50 | handler.send((yield)) 51 | if tag_name in tag_handlers: 52 | yield tag_handlers[tag_name](elements[0]), True 53 | else: 54 | print("No tag handler for %s" % tag_name) 55 | yield None, True 56 | 57 | @coroutine 58 | def character_handler(): 59 | r = (yield) 60 | while 1: 61 | c = (yield) 62 | if not c.isalpha(): 63 | if len(r) == 1: 64 | yield r, False 65 | else: 66 | yield {'newline':'\n', 'space':' ', 'tab':'\t'}[r], False 67 | r += c 68 | 69 | def parse_number(s): 70 | s = s.rstrip('MN').upper() 71 | if 'E' not in s and '.' not in s: 72 | return int(s) 73 | return float(s) 74 | 75 | @coroutine 76 | def number_handler(s): 77 | while 1: 78 | c = (yield) 79 | if c in "0123456789+-eEMN.": 80 | s += c 81 | else: 82 | yield parse_number(s), False 83 | 84 | @coroutine 85 | def symbol_handler(s): 86 | while 1: 87 | c = (yield) 88 | if c in '}])' + STOP_CHARS: 89 | yield s, False 90 | else: 91 | s += c 92 | 93 | CHAR_MAP = { 94 | "a": "\a", 95 | "b": "\b", 96 | "f": "\f", 97 | "n": "\n", 98 | "r": "\r", 99 | "t": "\t", 100 | "v": "\v" 101 | } 102 | 103 | @coroutine 104 | def parser(target, stop=None): 105 | handler = None 106 | while True: 107 | c = (yield) 108 | if handler: 109 | v = handler.send(c) 110 | if v is None: 111 | continue 112 | else: 113 | handler = None 114 | v, consumed = v 115 | if v is not None: 116 | target.send(v) 117 | if consumed: 118 | continue 119 | if c == stop: 120 | return 121 | if c in STOP_CHARS: 122 | continue 123 | if c in 'tfn': 124 | expecting = {'t':'rue', 'f':'alse', 'n':'il'}[c] 125 | for char in expecting: 126 | assert (yield) == char 127 | target.send({'t':True, 'f':False, 'n':None}[c]) 128 | elif c == ';': 129 | while (yield) != '\n': 130 | pass 131 | elif c == '"': 132 | chars = [] 133 | while 1: 134 | char = (yield) 135 | if char == '\\': 136 | char = (yield) 137 | char2 = CHAR_MAP.get(char) 138 | if char2 is not None: 139 | chars.append(char2) 140 | else: 141 | chars.append(char) 142 | elif char == '"': 143 | target.send(''.join(chars)) 144 | break 145 | else: 146 | chars.append(char) 147 | elif c == '\\': 148 | handler = character_handler() 149 | elif c in '0123456789': 150 | handler = number_handler(c) 151 | elif c in '-.': 152 | c2 = (yield) 153 | if c2.isdigit(): # .5 should be an error 154 | handler = number_handler(c+c2) 155 | else: 156 | handler = symbol_handler(c+c2) 157 | elif c.isalpha() or c == ':': 158 | handler = symbol_handler(c) 159 | elif c in '[({#': 160 | if c == '#': 161 | c2 = (yield) 162 | if c2 != '{': 163 | handler = tag_handler(c2) 164 | continue 165 | endchar = {'#':'}', '{':'}', '[':']', '(':')'}[c] 166 | l = [] 167 | p = parser(appender(l), endchar) 168 | try: 169 | while 1: 170 | p.send((yield)) 171 | except StopIteration: 172 | pass 173 | if c in '[(': 174 | target.send(tuple(l)) 175 | elif c == '#': 176 | 177 | not_hashable = any(isinstance(each, (dict, list, set)) for each in l) 178 | if not_hashable: 179 | target.send(tuple(l)) 180 | else: 181 | target.send(frozenset(l)) 182 | else: 183 | if len(l) % 2: 184 | raise Exception("Map literal must contain an even number of elements") 185 | target.send(dict(zip(l[::2], l[1::2]))) # No frozendict yet 186 | else: 187 | raise ValueError("Unexpected character in edn", c) 188 | 189 | def loads(s): 190 | l = [] 191 | target = parser(appender(l)) 192 | for c in s.decode('utf-8'): 193 | target.send(c) 194 | target.send(' ') 195 | if len(l) != 1: 196 | raise ValueError("Expected exactly one top-level element in edn string", s) 197 | return l[0] 198 | 199 | if __name__ == '__main__': 200 | print(loads( 201 | b'(:graham/stratton true \n , "A string with \\n \\"s" true #uuid "f81d4fae7dec11d0a76500a0c91e6bf6")')) 202 | print(loads(b'[\space \\\xE2\x82\xAC [true []] ;true\n[true #inst "2012-09-10T23:39:43.309-00:00" true ""]]')) 203 | print(loads(b' {true false nil [true, ()] 6 {#{nil false} {nil \\newline} }}')) 204 | print(loads(b'[#{6.22e-18, -3.1415, 1} true #graham #{"pie" "chips"} "work"]')) 205 | print(loads(b'(\\a .5)')) 206 | --------------------------------------------------------------------------------