{ "info": { "author": "Tozny, LLC", "author_email": "info@tozny.com", "bugtrack_url": null, "classifiers": [], "description": "# Introduction\n\nThe Tozny End-to-End Encrypted Database (E3DB) is a storage platform with powerful sharing and consent management features.\n[Read more on our blog.](https://tozny.com/blog/announcing-project-e3db-the-end-to-end-encrypted-database/)\n\nE3DB provides a familiar JSON-based NoSQL-style API for reading, writing, and querying data stored securely in the cloud.\n\n# Requirements\n\n* Python 3.4+ environment (Version 1.x of the E3DB SDK supports 2.7, but is no longer being supported except in the case of security updates)\n\n# Installation\n\n## With Pip (Preferred)\n\n`pip install e3db`\n\n## Local build\n\n### Build\n\nTo build the package locally:\n\n```bash\npython setup.py bdist_wheel\n```\n\n### Install\n\nThat produces a `.whl` file in the `dist` directory that you can install. This can be installed with:\n\n```bash\npip install --use-wheel --find-links= e3db\n```\n\n# Setup\n\n## Registering a client\n\nRegister an account with [InnoVault](https://innovault.io) to get started. From the Admin Console you can create clients directly (and grab their credentials from the console) or create registration tokens to dynamically create clients with `e3db.Client.register()`. Clients registered from within the console will automatically back their credentials up to your account. Clients created dynamically via the SDK can _optionally_ back their credentials up to your account.\n\nFor a more complete walkthrough, see [`/examples/registration.py`](https://github.com/tozny/e3db-python/blob/master/examples/registration.py).\n\n### Without Credential Backup\n\n```python\nimport e3db\n\ntoken = '...'\nclient_name = '...'\n\npublic_key, private_key = e3db.Client.generate_keypair()\n\nclient_info = e3db.Client.register(token, client_name, public_key)\n\n# Now run operations with the client's details in client_info\n```\n\nThe object returned from the server contains the client's UUID, API key, and API secret (as well as echos back the public key passed during registration). It's your responsibility to store this information locally as it _will not be recoverable_ without credential backup.\n\n### With Credential Backup\n\n```python\nimport e3db\n\ntoken = '...'\nclient_name = '...'\n\npublic_key, private_key = e3db.Client.generate_keypair()\n\nclient_info = e3db.Client.register(token, client_name, public_key, private_key=private_key, backup=True)\n\napi_key_id = client_info.api_key_id\napi_secret = client_info.api_secret\nclient_id = client_info.client_id\n\nconfig = e3db.Config(\n client_id,\n api_key_id,\n api_secret,\n public_key,\n private_key\n)\n\n# Optionally, if you want to save this Configuration to disk, do the following:\nconfig.write()\n\n# Now run operations with the client's details by instantiating a e3db client.\nclient = e3db.Client(config())\n```\n\nThe private key must be passed to the registration handler when backing up credentials as it is used to cryptographically sign the encrypted backup file stored on the server. The private key never leaves the system, and the stored credentials will only be accessible to the newly-registered client itself or the account with which it is registered.\n\n## Loading configuration and creating a client\n\nConfiguration is managed at runtime by instantiating an `e3db.Config` object with your client's credentials.\n\n```python\nimport e3db\nimport os\n\n# Assuming your credentials are stored as defined constants in the\n# application, pass them each into the configuration constructor as\n# follows:\n\nconfig = e3db.Config(\n os.environ[\"client_id\"],\n os.environ[\"api_key_id\"],\n os.environ[\"api_secret\"],\n os.environ[\"public_key\"],\n os.environ[\"private_key\"]\n)\n\n# Pass the configuration when building a new client instance.\n\nclient = e3db.Client(config())\n```\n\n### Options for Loading Credentials\nThere are a number of options for instantiating an `e3db.Config` object. Shown above is the manual creation of the config option, but there are some convenience methods that can make this process easier when dealing with multiple client profiles. \n\nLoading from `.tozny` profiles, takes advantage of the same profiles from the [e3db cli tool](https://github.com/tozny/e3db).\n```python\n# usage\nconfig = e3db.Config.load(profile_name)\n# path searched\npath = `~/.tozny//e3db.json`\n\n# default path for no profile_name\nconfig = e3db.Config.load()\npath = `~/.tozny/e3db.json`\n\n# provided method to save credentials to disk\nconfig.write(profile_name)\n```\n\nLoading from an arbitrary file that matches the credentials format, but may have a custom path.\n```python\ncredentials_path = \"credentials.json\" # your e3db credentialss\nif os.path.exists(credentials_path):\n client = e3db.Client(json.load(open(credentials_path)))\n ...\n```\n\n# Usage\n\n## Writing a record\n\nTo write new records to the database, call the `e3db.Client.write` method with a string describing the type of data to be written, along with an dictionary containing the terms of the record. `e3db.Client.write` returns the newly created record.\n\n```python\nimport e3db\n\nclient = e3db.Client(\n # config\n)\n\nrecord_type = 'contact'\ndata = {\n 'first_name': 'Jon',\n 'last_name': 'Snow',\n 'phone': '555-555-1212'\n}\n\nrecord = client.write(record_type, data)\n\nprint 'Wrote record {0}'.format(record.meta.record_id)\n```\n\n## Searching records\n\nE3DB supports complex search options for finding records based on the terms stored in record metadata. \nThese terms include the properties in the [Meta class](e3db/types/meta.py):\n```\n - record_ids\n - writer_ids\n - user_ids\n - record_types\n - plain\n - keys\n - values\n```\n\nThe values in the plain meta dictionary are expanded into keys and values for two additional query terms:\n\n```\n - keys\n - values\n\nWHERE\n\n plain = {\"key1\":\"value1\", \"key2\":\"value2\"}\n keys = [\"key1\", \"key2\"]\n values = [\"value1\", \"value2\"]\n```\n\nA time range can be provided to limit search results based off one of these terms:\n```\n - created\n - last_modified\n```\n\n#### Search Results\n\nThe results of your search come back in a [SearchResult object](e3db/types/search_result.py):\n\n```python\n# This object contains your records,\n# and additional information about your search.\n\nquery = Search(include_data=True)\nresults = client.search(query)\n\n# Records found in your search\nresults.records \n\n# Number of records returned from search\nlen(results) \n\n# Number of records present in E3db that match your search.\n# If there are more records to be retrieved this number will be greater than len(results), \n# and next_token will not be 0\nresults.total_results \n\n# Token for paginating through queries, \n# this token will be 0 if there are no more records to be returned.\nresults.next_token \n\nresults.search_id # id for bulk searching of many records, currently not available\n```\n\n#### Examples\n\nTo list all records of type `contact` and print a simple report containing names and phone numbers:\n\n```python\n# setup\nimport e3db\nfrom e3db.types import Search\n\nconfig = e3db.Config(\n os.environ[\"client_id\"],\n os.environ[\"api_key_id\"],\n os.environ[\"api_secret\"],\n os.environ[\"public_key\"],\n os.environ[\"private_key\"]\n)\n\nclient = e3db.Client(config())\n\nquery = Search(include_data=True).match(record_types=['contact'])\nresults = client.search(query)\n\nfor record in results:\n full_name = \"{0} --- {1}\".format(record.data['first_name'], record.data['last_name'])\n print \"{0} --- {1}\".format(full_name, record.data['phone'])\n```\n\nThe full list of parameters you can search for are under `e3db.types.Params`. Searching gives you access to chaining more powerful queries such as conditional operators, exclusions, and date filters. \n\nTo search for records of type `season` that have unencrypted metadata values of `summer` and `sunny`, create the following query:\n```python \n# e3db setup...\n\ndata = {\"temp\": \"secret_temp\"}\nplain = {\"name\":\"summer\", \"weather\":\"sunny\"}\nclient.write(\"season\", data, plain)\n\n# {key:values} in the plain JSON field are mapped to keys=[] and values=[] behind the scenes in E3DB.\nquery = Search().match(condition=\"AND\", record_types=[\"season\"], values=[\"summer\", \"sunny\"])\nresults = client.search(query)\n\n# searching on keys instead...\nquery = Search().match(condition=\"AND\", record_types=[\"season\"], keys=[\"name\", \"weather\"])\n```\n\nTo search for records of type `jam` with values of `apricot`, but excluding values of `strawberry`, create the following query:\n```python \n# e3db setup...\n\napricot_data = {\"recipe\": \"encrypted_secret_formula\"}\napricot_plain = {\"flavor\":\"apricot\"}\n\nclient.write(\"jam\", apricot_data, apricot_plain)\n\nstrawberry_data = {\"recipe\": \"encrypted_secret_formula\"}\nstrawberry_plain = {\"flavor\":\"strawberry\"}\nclient.write(\"jam\", strawberry_data, strawberry_plain )\n\nquery = Search().match(record_types=[\"jam\"], values=[\"apricot\"]).exclude(values=[\"strawberry\"])\nresults = client.search(query)\n```\n\n#### Time Filters\n\nThe Search method has the capability to filter by the time records were created or last modified, restricting search results to a specific timeframe. The most basic example, of searching for all records written by a writer in the last 24 hours, is shown below:\n\n```python\n# e3db setup...\nfrom e3db.types import Search\nfrom datetime import datetime, timedelta\nimport time\n\n# Using seconds from unix epoch\nnow = int(time.time())\nstart = now - (60 * 60 * 24) # 60 seconds, 60 minutes, 24 hours\nend = now\n\nwriter_id = \"some_writer_uuid\"\nquery = Search().match(writers=[writer_id])\\\n .range(start=start, end=end) # Filters here\nresults = client.search(query)\n\n# using datetime\nnow = datetime.now().astimezone()\nstart = now - timedelta(days=1)\nend = now\n\nwriter_id = \"some_writer_uuid\"\nquery = Search().match(writers=[writer_id])\\\n .range(start=start, end=end) # Filters here\nresults = client.search(query)\n```\n\nThe Range can be open ended if you want to, for example, see all records written before yesterday:\n```python\nnow = int(time.time())\nend = now - (60 * 60 * 24) # 60 seconds, 60 minutes, 24 hours == 1 day in seconds\n\nquery = Search().match()\\\n .range(end=end)\nresults = client.search(query)\n```\n\nOr see all records written in the last week...\n\n```python\nnow = int(time.time())\nstart = now - (60 * 60 * 24 * 7) # 60 seconds, 60 minutes, 24 hours, 7 days == 1 week in seconds\n\nquery = Search().match()\\\n .range(start=start)\nresults = client.search(query)\n```\n\nThe Search Range accepts Unix Epoch, datetime objects, and datetime timezone-unaware objects, for start and end times. In Python all datetime objects are `timezone naive` or `timezone-unaware` unless you are using a libary like pytz. So the Search Range will assume that you want a timezone of UTC unless otherwise specified.\n\nTo target a specific timezone you have a couple options:\n```python\n# 1. Go zone agnostic by providing a time based off of the Unix Epoch (recommended).\nunix_epoch = int(time.time()) # https://www.epochconverter.com/ for conversions\nquery = Search().match().range(start=unix_epoch-3600, end=unix_epoch) # This will always return the most recent hour of results\n\n# 2. Create timezone aware objects to pass into Search Range.\ntz_unaware = datetime.now()\ntz_aware = tz_unaware.astimezone() # sets timezone to match local machine\n\n# 3. Specify an approriate zone_offset. The hour offset from UTC as int or \"[+|-]HH:MM\" as str\ntz_unaware = datetime.now()\nquery = Search().match().range(start=tz_unaware, zone_offset=-7) # we append UTC-7 to the tz_unaware object\nquery = Search().match().range(start=tz_unaware, zone_offset=\"-07:00\") # we parse and append UTC-7 to the tz_unaware object\n\n# 4. Manually creating the proper time (not recommended).\ntz_unaware = datetime.now()\n# Assuming local tz of Pacific time, PT is UTC-7, and so we add 7 hours to get UTC.\nutc_tz_unaware = tz_unaware + timedelta(hours=7)\nquery = Search().match().range(start=utc_tz_unaware)\n```\n\nTake care when using the python datetime library function `astimezone()`, because it does an implicit conversion behind the scenes using the computer's local timezone if no tzinfo is provided as a parameter. To avoid this you can replace the tzinfo instead:\n```python\nunaware_day = datetime(year=2019, month=3, day=18, hour=0, minute=0, second=0)\nprint(unaware_day.isoformat(\"T\"))\n# Prints: 2019-03-18T00:00:00\n\n# using astimezone()\nas_timezone = unaware_day.astimezone(timezone(timedelta(hours=0))) # attempting to convert to UTC\nprint(as_timezone.isoformat(\"T\"))\n# Prints: 2019-03-18T07:00:00+00:00, which is 7 hours ahead of the anticipated time.\n\n# replacing tzinfo\naware_day = unaware_day.replace(tzinfo=timezone(timedelta(hours=0))) # attempting to convert to UTC\nprint(aware_day.isoformat(\"T\"))\n# Prints: 2019-03-18T00:00:00-07:00\n```\n\n#### Defaults\n\nThe Search method has a number of default parameters when searching, more detail can be found within the inline documentation.\n\nUnder Search there are these defaults:\n```python\n# Optional value that starts search from the first page of results, only required for paginated queries.\nnext_token = 0\n\n# Amount of records to be returned, limiting if more are available. Defaults to 50, and the maximum value allowed is 1000.\ncount = 50\n\n# Include only records written by the client (False), or search other writer ids (True)\ninclude_all_writers = False\n\n# Include data when returning records\ninclude_data = False\n```\n\nUnder Search Params there are these defaults:\n```python\n# Conditional OR when searching upon all terms within this clause (Param object)\ncondition = \"OR\" # options: \"OR\"|\"AND\"\n\n# Exactly matches the search terms provided\nstrategy = \"EXACT\" # options \"EXACT\"|\"FUZZY\"|\"WILDCARD\"|\"REGEXP\"\n```\n\nUnder Search Range there is this default:\n```python\n# Search records based on when they were created or last modified\nkey = \"CREATED\" # options: \"CREATED\"|\"MODIFIED\"\n\n# Zone offset is None by default, but if provided it will override provided datetime objects if they are timezone unaware\n# Accepts \n# - int denoting hours offset from UTC \n# - str in the format \"[+|-]HH:MM\" also denoting the time offset from UTC\n# - for a more comprehensive list see https://en.wikipedia.org/wiki/List_of_UTC_time_offsets\nzone_offset = None\n```\nSince python datetime objects are zone agnostic, provide the proper timezone offset from UTC\nin zone_offset to search properly.\n\n### Boolean Searching\n\nWithin each match or exclude clause the internal search terms can either be joined with AND or OR. \n\nThe data for these examples can be found under [boolean_search](./examples/boolean_search.py)\n\n```python\n# The Search Method has two conditional options for constructing your queries: AND|OR\n# OR is used by default and does not have to be explicitly stated unlike below.\nserver_1_or_server_2 = Search().match(condition=\"OR\", strategy=\"WILDCARD\", plain={\"*server_1*\":\"*\", \"*server_2*\":\"*\"})\nresults = client.search(server_1_or_server_2)\nprint_results(\"getting records with server_1 or server_2 in meta with the plain field\", results)\n\nserver_1_and_server_2 = Search().match(condition=\"AND\", strategy=\"WILDCARD\", plain={\"*server_1*\":\"*\", \"*server_2*\":\"*\"})\nresults = client.search(server_1_and_server_2)\nprint_results(\"getting records with server_1 and server_2 in meta with the plain field (returns nothing)\", results)\n```\n\nSearch terms can be intermingled\n\n```python\n# AND is the logical operator and returns records that match all conditions within the match parameters.\nrecord_and_plain_search = Search().match(condition=\"AND\", strategy=\"WILDCARD\", record_types=[\"flora\"], plain={\"*test*\":\"*dand*\"})\nresults = client.search(record_and_plain_search)\n# The results we see here is all records of record_type `flora`, and key `*test*` and value `*dand*`\n# We get the single record with meta \"{'flora_test_12345':'dandelion'}\"\nprint_results(\"get records by record type 'flora' AND plain containing '*test*':'*dand*' \", results)\n```\n\nCombining match and exclude together will remove the terms in the exclude clause\n```python\n# This means records that equal the match clause AND do not equal the exclude clause are returned.\nmatch_and_exclude = Search().match(record_types=[\"flora\"]).exclude(strategy=\"WILDCARD\", keys=[\"*test*\"])\nresults = client.search(match_and_exclude)\nprint_results(\"get records of record type 'flora' and do not have keys `*test*`\", results)\n```\n\nChaining match clauses with matches, and exclude clauses with excludes are joined by the logical OR. In general, the two basic tenants of chaining these clauses are as follows:\n1. more match clauses expands your search, adding terms that match your results\n1. more exclude clauses constricts your search, removing terms that match your results\n```python\nchain_match = Search().match(condition=\"AND\", record_types=[\"flora\"]).match(condition=\"AND\", record_types=[\"fauna\"])\n# the above search is equivalent to below.\nequivalent_to_chain_match = Search().match(condition=\"OR\", record_types=[\"flora\", \"fauna\"])\n```\n\nNested chaining allows you to specify varying strategies for different terms \n```python\ndiffering_strategies = Search().match(strategy=\"EXACT\", record_types=[\"flora\"])\\\n .match(strategy=\"WILDCARD\", keys=[\"*12345\"])\\\n .exclude(strategy=\"REGEXP\", keys=[\".*test.*\"])\\\n .exclude(strategy=\"REGEXP\", keys=[\".*server_2.*\"])\n# results = (MATCH `flora` OR MATCH `*12345`) AND (EXCLUDE `.*test.*` OR EXCLUDE `.*server_2.*`)\nresults = client.search(differing_strategies)\nprint_results(\"Different matching strategies: this search will return an EXACT match to record_type `flora` OR WILDCARD match to keys `*12345`, and a REGEXP exclude to keys `.*test.*` OR REGEXP exclude to keys `.*server_2.*`\", results)\n```\n\nKeep in mind that this chaining means your previous search object gets altered each time. \n```python\noriginal_search = Search().exclude(record_types=[\"fauna\"])\nmodified_search = original_search.exclude(record_types=[\"flora\"])\nresults = client.search(modified_search)\nprint_results(\"modified_search and original_search will exclude both flora and fauna\", results)\n```\n\n### Advanced Matching Strategies\n\nSearch offers advanced queries that provide more flexibility than the default matching strategy of `EXACT`. These are the four options ordered from fastest to slowest: `EXACT`, `FUZZY`, `WILDCARD`, and `REGEXP`.\n\nTo mirror some of the above queries with these matching strategies we get:\n```python\n# e3db setup...\n\n# fuzzy\n# generates an edit distance and matches terms that are 1-2 edits away from the provided query.\n# summer is 1 edit s-> b away from bummer\nfuzz_query = Search().match(strategy=\"FUZZY\", record_types=[\"season\"], values=[\"bummer\"])\n\n# wildcard\n# supported wildcards are * and ?\n# * matches any character sequence, including the empty sequence.\n# ? matches any single character\nwild_query = Search().match(strategy=\"WILDCARD\", record_types=[\"season\"], values=[\"su??er\"])\n\n# regexp\n# some of the support operators are ^ $ . ? + * | { } [ ] ( ) \\\n# refer to the table below for more information\nregxp_query = Search().match(strategy=\"REGEXP\", record_types=[\"season\"], values=[\"sum.*\"])\n```\n\nGo to [Pattern Search Examples](./examples/pattern_search.py) for more examples.\n\n#### Regexp operators\n```\n^ anchors expression to the start of the matched strings\n$ anchors expression to the end of the matched strings\n. represents any single character\n? used to match the preceding shortest pattern zero or one times\n+ used to match the preceding shortest pattern one or more times\n* used to match the preceding shortest pattern zero or more times\n| acts as an OR operator, matches this OR that\n{ } used to specify the min and max nubmer of times the preceding pattern can repeat. \n - {2} repeat twice, {2,} repeat at least twice, {2,3} repeat 2-3 times\n( ) used to group sub patterns\n - (ab)+ repeat the value within parenthesis 'ab' one or more times\n[ ] used to specify a range of characters\n - [abc] matches 'a' or 'b' or 'c'\n - within the square brackets a ^ negates the class: [^abc] excludes characters 'a', 'b', and 'c'\n\\ is used to escape any of the previous special characters\n```\nFor more information look [here](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html)\n\nSee [the integration tests](e3db/tests/test_search_integration.py) or [examples folder](examples/) for more examples.\n\n### Paging\n\nThe construction of the Search object offers a number of options for paging through your results: namely `next_token` and `count`.\n\nTo page through a large number of results, you can loop like this:\n```python\n# e3db setup...\n# limit number of results returned to 1000 at a time\nquery = Search(count=1000).match(record_types=[\"many_results\"])\nresults = client.search(query)\n\n# Number of results in e3db\ntotal_results = results.total_results\n# Return records from this point onwards in next query\n\nwhile results.next_token:\n query.next_token = results.next_token\n results = client.search(query)\n do_something(results) # process results\n```\n\nThe `next_token` returned from a query will be 0 if there are no more records to return. `total_results` represents the total number of records in e3db that match the executed query. \n\nSee [pagination example](examples/simple_paginate_results.py) for full code example.\n\n#### Search Count Restraints\n\nYou aren't limited to a specific number of searches, however for a single search the maximum page size is 1000. Requesting for a larger page size than 1000 (`count=1001`) will result in a HTTP 400 Bad Request. The pagination example, shown above, can be used to grab more than 1000 records overall.\n\n```\nquery = Search(count=1000)\n```\n\nAdditionally, if your search is too broad you will only be able to retrieve 10,000 results. You can choose to narrow your query by constricting time ranges manually or programatically as shown here with this [sample script](examples/narrow_range_of_large_search.py).\n\n### Large Files\n\nWhen searching or querying for large files, even if you set `include_data=True`, the data field returned will be blank. Instead file meta will be returned under each record's meta `record.meta.file_meta`. To download the file you can use the `e3db.Client.read_file` method like this:\n\n```python\n# e3db setup...\n\n# record_id retrieved from search...\nquery = Search().match(record_types=[\"large_file\"])\nresults = client.search(query)\n\n# download large files\nfor i, r in enumerate(results):\n record_id = r.meta.record_id\n dest = \"./large_file_{0}.txt\".format(i)\n FileMeta = client.read_file(record_id, dest)\n```\n\n## Querying records\n\nE3DB supports many options for querying records based on the terms stored in record metadata. Refer to the API documentation for the complete set of options that can be passed to `e3db.Client.query`.\n\nFor example, to list all records of type `contact` and print a simple report containing names and phone numbers:\n\n```python\nimport e3db\n\nclient = e3db.Client(' config ')\n\nrecord_type = 'contact'\n\nfor record in client.query(record=[record_type]):\n full_name = \"{0} --- {1}\".format(record.data['first_name'], record.data['last_name'])\n print \"{0} --- {1}\".format(full_name, record.data['phone'])\n```\n\nIn this example, the `e3db.Client.query` method returns an iterator that contains each record that matches the query.\n\n## More examples\n\nSee [the simple example code](https://github.com/tozny/e3db-python/blob/master/examples/simple.py) for runnable detailed examples.\n\n## Cipher Suite Selection\n\nThe Python SDK is capable of operating in two different modes - Sodium and NIST. The Sodium mode uses [Libsodium](https://download.libsodium.org/doc/) for all cryptographic primitives. The NIST mode uses NIST-approved primitives via OpenSSL for all cryptographic primitives.\n\nThe SDK will operate in Sodium mode by default. To switch operation to NIST mode, export an environment variable before running any reliant applications:\n\n```sh\nexport CRYPTO_SUITE=NIST\n```\n\nThe NIST mode of operations will leverage:\n- ECDH over curve P-384 for public/private key exchange\n- SHA384 for hashing\n- ECDSA over curve P-384 for cryptographic signatures\n- AES265GCM for symmetric encryption operations\n\n## Documentation\n\nGeneral E3DB documentation is [on our web site](https://tozny.com/documentation/e3db/).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/tozny/e3db-python.\n\n## License\n\nTozny dual licenses this product. For commercial use, please contact [info@tozny.com](mailto:info@tozny.com). For non-commercial use, this license permits use of the software only by government agencies, schools, universities, non-profit organizations or individuals on projects that do not receive external funding other than government research grants and contracts. Any other use requires a commercial license. For the full license, please see [LICENSE.md](https://github.com/tozny/e3db-python/blob/master/LICENSE.md), in this source repository.", "description_content_type": "text/markdown", "docs_url": null, "download_url": "https://github.com/tozny/e3db-python/archive/2.1.4.tar.gz", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/tozny/e3db-python", "keywords": "e3db,encryption,encrypted-store", "license": "TOZNY NON-COMMERCIAL LICENSE", "maintainer": "", "maintainer_email": "", "name": "e3db", "package_url": "https://pypi.org/project/e3db/", "platform": "", "project_url": "https://pypi.org/project/e3db/", "project_urls": { "Download": "https://github.com/tozny/e3db-python/archive/2.1.4.tar.gz", "Homepage": "https://github.com/tozny/e3db-python" }, "release_url": "https://pypi.org/project/e3db/2.1.4/", "requires_dist": null, "requires_python": "", "summary": "E3DB provides a familiar JSON-based NoSQL-style API for reading, writing, and querying data stored securely in the cloud.", "version": "2.1.4" }, "last_serial": 5913450, "releases": { "1.0.0": [ { "comment_text": "", "digests": { "md5": "04e224a8e6066460accd369c581723f6", "sha256": "7528a645bb52cfd15f955f766d106ff48bbbdaa66430b8876a3148d853f2bc79" }, "downloads": -1, "filename": "e3db-1.0.0.tar.gz", "has_sig": false, "md5_digest": "04e224a8e6066460accd369c581723f6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 22389, "upload_time": "2018-04-12T22:45:15", "url": "https://files.pythonhosted.org/packages/f4/4f/63eec1ed2e77f4da24357e6551b6a7812c4d5147f9693d25c46c2ba2614f/e3db-1.0.0.tar.gz" } ], "1.0.1": [ { "comment_text": "", "digests": { "md5": "691f5b93b6a33f7f1f6ed472beca54e2", "sha256": "f0efa34a50496172a57133bb6042088cb55adc662f5dac38f7809c9c8cfe460b" }, "downloads": -1, "filename": "e3db-1.0.1-py2-none-any.whl", "has_sig": false, "md5_digest": "691f5b93b6a33f7f1f6ed472beca54e2", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": null, "size": 29198, "upload_time": "2018-07-25T20:40:08", "url": "https://files.pythonhosted.org/packages/2f/c8/2c6f284ad947cc75516202310efd741edf0d4999ee733a877dcf49cb997c/e3db-1.0.1-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "0599c455d23c0b1b7b9424200af1dcd8", "sha256": "1a18bbceb0f8695ca73dc691ea6891ad92b5cadb90b4fe9a98a50bc494a93c84" }, "downloads": -1, "filename": "e3db-1.0.1.tar.gz", "has_sig": false, "md5_digest": "0599c455d23c0b1b7b9424200af1dcd8", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 22649, "upload_time": "2018-04-14T00:38:45", "url": "https://files.pythonhosted.org/packages/49/3d/0a075dff7fc53e860eac87de4e8ca509cf186b9d790fd3efe1c8bdbfba77/e3db-1.0.1.tar.gz" } ], "1.0.2": [ { "comment_text": "", "digests": { "md5": "77c043444631bec4113fcf085488353f", "sha256": "5f463d8632300f06d5be3bbe2f6c3a5a1009af9902e7887674444f25b2526996" }, "downloads": -1, "filename": "e3db-1.0.2-py2.7.egg", "has_sig": false, "md5_digest": "77c043444631bec4113fcf085488353f", "packagetype": "bdist_egg", "python_version": "2.7", "requires_python": null, "size": 63227, "upload_time": "2018-07-09T23:23:28", "url": "https://files.pythonhosted.org/packages/69/d3/16a28a5c495067e354ffe6d90a2bbabf28950e1f96b1da63ec86da684236/e3db-1.0.2-py2.7.egg" }, { "comment_text": "", "digests": { "md5": "1d364cc83df60afc11252c0b38d58661", "sha256": "5f75b71ccb40f9d9dea5e2c8667754fa547d1318545d3578319a740b28b68ea0" }, "downloads": -1, "filename": "e3db-1.0.2-py2-none-any.whl", "has_sig": false, "md5_digest": "1d364cc83df60afc11252c0b38d58661", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": null, "size": 28630, "upload_time": "2018-07-09T23:23:27", "url": "https://files.pythonhosted.org/packages/a1/fa/08c9420641b0b0b5d6836539f58a413fc49170c3551a574645c7ac086124/e3db-1.0.2-py2-none-any.whl" } ], "1.1.0": [ { "comment_text": "", "digests": { "md5": "3a67280abbc1c544cf32561d6b2d782d", "sha256": "8ebe99cc817565e9a11dd043d3eaec184995b648f920c1760b8edcef74ab20e0" }, "downloads": -1, "filename": "e3db-1.1.0.tar.gz", "has_sig": false, "md5_digest": "3a67280abbc1c544cf32561d6b2d782d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 24847, "upload_time": "2018-07-09T23:23:30", "url": "https://files.pythonhosted.org/packages/8a/94/4feb8bbc903f3c6e42f84cfc00eafb601188c225b28ee1f6d75efe8f020a/e3db-1.1.0.tar.gz" } ], "1.1.1": [ { "comment_text": "", "digests": { "md5": "5d9df1b80b8d8f29122719fa9fc98abc", "sha256": "9abe7a38187e406ebde5aedef21c70868a27f9a8bfac8d593b52203f895aa0d9" }, "downloads": -1, "filename": "e3db-1.1.1.tar.gz", "has_sig": false, "md5_digest": "5d9df1b80b8d8f29122719fa9fc98abc", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 24851, "upload_time": "2018-07-09T23:34:18", "url": "https://files.pythonhosted.org/packages/bf/e3/43a522a53834c11f9df29d07639f639ed74f79570f654262e8f369c1a58e/e3db-1.1.1.tar.gz" } ], "1.1.2": [ { "comment_text": "", "digests": { "md5": "0858d5ab53e6294c54341233019370c7", "sha256": "06ab31f93682280cc48203cffa690f0be83e1f2205d3e5be318be01a52a88ba3" }, "downloads": -1, "filename": "e3db-1.1.2.tar.gz", "has_sig": false, "md5_digest": "0858d5ab53e6294c54341233019370c7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 26860, "upload_time": "2018-07-25T20:40:10", "url": "https://files.pythonhosted.org/packages/3b/c8/e75eaef81afd3af167c4648394f8b058f2c46d9ff3cf3941e5c512df80b9/e3db-1.1.2.tar.gz" } ], "1.1.3": [ { "comment_text": "", "digests": { "md5": "bcb4fe0f3404a06be99467cea90b0db4", "sha256": "c27ee155c53ecdbc0bc7216c001516f2a1bb4b65d6733c6491ee53c8d87404a2" }, "downloads": -1, "filename": "e3db-1.1.3.tar.gz", "has_sig": false, "md5_digest": "bcb4fe0f3404a06be99467cea90b0db4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 27090, "upload_time": "2018-08-03T00:42:17", "url": "https://files.pythonhosted.org/packages/df/9f/bbd5f5ab8f2d21998aea9d641fa1b55fecb8a740df25b9b2a30f01fac0c1/e3db-1.1.3.tar.gz" } ], "1.2.1": [ { "comment_text": "", "digests": { "md5": "321c1a73b0a6e88989184238f483e3ef", "sha256": "de57da704e00e7729d6affe3624aa76d93278e96d91e0733fbcaa97a7c848065" }, "downloads": -1, "filename": "e3db-1.2.1.tar.gz", "has_sig": false, "md5_digest": "321c1a73b0a6e88989184238f483e3ef", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 33318, "upload_time": "2018-10-31T19:36:55", "url": "https://files.pythonhosted.org/packages/16/2a/95d22d7f201ac26538dc11587adf0b6356033a1ca82ef2b13e4621d99cb3/e3db-1.2.1.tar.gz" } ], "1.3.0": [ { "comment_text": "", "digests": { "md5": "b0330a62239f274dcfa661b0b388c7a2", "sha256": "8a6b52cfc4ed81618e7be2ee472dd9da92ce4174d8ecfb2d02f73cb0c6da06d4" }, "downloads": -1, "filename": "e3db-1.3.0.tar.gz", "has_sig": false, "md5_digest": "b0330a62239f274dcfa661b0b388c7a2", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 33198, "upload_time": "2018-12-01T00:57:47", "url": "https://files.pythonhosted.org/packages/da/ac/69c9a033e34afeb9b498f8145845d1faff1e05c907ec4427bf35a0395943/e3db-1.3.0.tar.gz" } ], "2.0.0": [ { "comment_text": "", "digests": { "md5": "82067ff2c628270137afd92f55cff68a", "sha256": "fb0ba2135a56cab10f6c1bec8031babfa54e125bf0daf8895b5d5a6e20560fb8" }, "downloads": -1, "filename": "e3db-2.0.0.tar.gz", "has_sig": false, "md5_digest": "82067ff2c628270137afd92f55cff68a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 33502, "upload_time": "2019-01-07T22:51:25", "url": "https://files.pythonhosted.org/packages/16/e0/0acbd728b0abac146c81116ff537e2d21bfccce82254e950d0259c0f69b1/e3db-2.0.0.tar.gz" } ], "2.0.1": [ { "comment_text": "", "digests": { "md5": "0b7b92e03f3ef1f6435e096b4d249598", "sha256": "ddae6a84436781e901edc514fcb7f0c90ac1794c0106334224756e3ce7f3d260" }, "downloads": -1, "filename": "e3db-2.0.1.tar.gz", "has_sig": false, "md5_digest": "0b7b92e03f3ef1f6435e096b4d249598", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 33415, "upload_time": "2019-01-11T17:50:07", "url": "https://files.pythonhosted.org/packages/07/10/18ffa7a3de5e05f45a4739abb91dc97cddca1743f6a3e7826cc52c2f0600/e3db-2.0.1.tar.gz" } ], "2.1.0": [ { "comment_text": "", "digests": { "md5": "45f6604699030cfde3e6030de867bdd0", "sha256": "7b014efd13b9abc685512001096ad76400e66a5de9d88ec3290f19c21584f778" }, "downloads": -1, "filename": "e3db-2.1.0.tar.gz", "has_sig": false, "md5_digest": "45f6604699030cfde3e6030de867bdd0", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 45968, "upload_time": "2019-02-14T00:25:29", "url": "https://files.pythonhosted.org/packages/a7/bf/695e8b3dd588d3b599c037a9505b5a5b5e2a141046766c2108e10b6128c9/e3db-2.1.0.tar.gz" } ], "2.1.1": [ { "comment_text": "", "digests": { "md5": "ea2b7b7ab87139cf03df64e339c0786e", "sha256": "10f88c0b0b55477388ce584b60980c77b6b0b4f15cf3bcf5ef4d78fc819fa6ae" }, "downloads": -1, "filename": "e3db-2.1.1.tar.gz", "has_sig": false, "md5_digest": "ea2b7b7ab87139cf03df64e339c0786e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 47999, "upload_time": "2019-02-25T18:23:26", "url": "https://files.pythonhosted.org/packages/a6/0e/406cc073bb198f1af361187add9819e6f93f96716074174d173b8a93314d/e3db-2.1.1.tar.gz" } ], "2.1.2": [ { "comment_text": "", "digests": { "md5": "3aa1f311cc56ffe981462b1e93c4d7aa", "sha256": "cd639ae9a3840ee12fda6227c33c909ddd2b5eccbebd7432b811c0b5a40374a0" }, "downloads": -1, "filename": "e3db-2.1.2.tar.gz", "has_sig": false, "md5_digest": "3aa1f311cc56ffe981462b1e93c4d7aa", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 48017, "upload_time": "2019-02-25T19:56:00", "url": "https://files.pythonhosted.org/packages/53/24/d487ebd3adf121d5bb34235adbe072731e16c30172e7dcf880638ac9ed37/e3db-2.1.2.tar.gz" } ], "2.1.3": [ { "comment_text": "", "digests": { "md5": "2cd7b9be9805edf396f9add15b08e58b", "sha256": "6a1be0172c58d6ce3446bbbcf0a87e968aae87501c30759e4931e2b17cdc5f57" }, "downloads": -1, "filename": "e3db-2.1.3.tar.gz", "has_sig": false, "md5_digest": "2cd7b9be9805edf396f9add15b08e58b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 51469, "upload_time": "2019-03-28T17:52:02", "url": "https://files.pythonhosted.org/packages/2c/1f/4cae35fb7a1e0fcdb5a4a285caf6c3af121076baaac094dcbe92ea812bd2/e3db-2.1.3.tar.gz" } ], "2.1.4": [ { "comment_text": "", "digests": { "md5": "0d8683c8175aeb06651d18c252b8c12d", "sha256": "4c3c7591ba5f0e8994c332996dc11bb9915ee9efe5c7e9876ba1bd3561f4518f" }, "downloads": -1, "filename": "e3db-2.1.4.tar.gz", "has_sig": false, "md5_digest": "0d8683c8175aeb06651d18c252b8c12d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 55934, "upload_time": "2019-10-01T15:52:03", "url": "https://files.pythonhosted.org/packages/6a/19/5bbff9cb7fca93454643b0fcae39693b4dd34f977c31331df36bd7e113e5/e3db-2.1.4.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "0d8683c8175aeb06651d18c252b8c12d", "sha256": "4c3c7591ba5f0e8994c332996dc11bb9915ee9efe5c7e9876ba1bd3561f4518f" }, "downloads": -1, "filename": "e3db-2.1.4.tar.gz", "has_sig": false, "md5_digest": "0d8683c8175aeb06651d18c252b8c12d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 55934, "upload_time": "2019-10-01T15:52:03", "url": "https://files.pythonhosted.org/packages/6a/19/5bbff9cb7fca93454643b0fcae39693b4dd34f977c31331df36bd7e113e5/e3db-2.1.4.tar.gz" } ] }