# etcetra
Pure python asyncio Etcd client.
## Installation
```bash
pip install etcetra
```
## API Documentation
Refer [here](/docs/references.md).
## Basic usage
All etcd operations managed by etcetra can be executed using `EtcdClient`.
`EtcdClient` instance is a wrapper which holds connection information to Etcd channel.
This instance is reusable, since actual connection to gRPC channel will be established
when you initiate connection calls (see below).
```python
from etcetra import EtcdClient, HostPortPair
etcd = EtcdClient(HostPortPair('127.0.0.1', 2379))
```
Like I mentioned above, actual connection establishment with Etcd's gRPC channel will be done
when you call `EtcdClient.connect()`. This call returns async context manager, which manages `EtcdCommunicator` instance.
```python
async with etcd.connect() as communicator:
await communicator.put('testkey', 'testvalue')
value = await communicator.get('testkey')
print(value) # testvalue
```
`EtcdCommunicator.get_prefix(prefix)` will return a dictionary containing all key-values with given key prefix.
```python
async with etcd.connect() as communicator:
await communicator.put('/testdir', 'root')
await communicator.put('/testdir/1', '1')
await communicator.put('/testdir/2', '2')
await communicator.put('/testdir/2/3', '3')
test_dir = await communicator.get_prefix('/testdir')
print(test_dir) # {'/testdir': 'root', '/testdir/1': '1', '/testdir/2': '2', '/testdir/2/3': '3'}
```
## Operating with Etcd lock
Just like `EtcdClient.connect()`, you can easilly use etcd lock by calling `EtcdClient.with_lock(lock_name, timeout=None)`.
```python
async def first():
async with etcd.with_lock('foolock') as communicator:
value = await communicator.get('testkey')
print('first:', value, end=' | ')
async def second():
await asyncio.sleep(0.1)
async with etcd.with_lock('foolock') as communicator:
value = await communicator.get('testkey')
print('second:', value)
async with etcd.connect() as communicator:
await communicator.put('testkey', 'testvalue')
await asyncio.gather(first(), second()) # first: testvalue | second: testvalue
```
Adding `timeout` parameter to `EtcdClient.with_lock()` call will add a timeout to lock acquiring process.
```python
async def first():
async with etcd.with_lock('foolock') as communicator:
value = await communicator.get('testkey')
print('first:', value)
await asyncio.sleep(10)
async def second():
await asyncio.sleep(0.1)
async with etcd.with_lock('foolock', timeout=5) as communicator:
value = await communicator.get('testkey')
print('second:', value)
async with etcd.connect() as communicator:
await communicator.put('testkey', 'testvalue')
await asyncio.gather(first(), second()) # asyncio.TimeoutError followed by first: testvalue output
```
Adding `ttl` parameter to `EtcdClient.with_lock()` call will force lock to be released after given seconds.
```python
async def first():
async with etcd.with_lock('foolock', ttl=5) as communicator:
await asyncio.sleep(10)
await first()
# on other file
import time
async def second():
start = time.time()
async with etcd.with_lock('foolock', ttl=5) as communicator:
print(f'acquired lock after {time.time() - start} seconds')
await second() # acquired lock after 4.756163120269775 seconds
```
## Watch
You can watch changes on key with `EtcdCommunicator.watch(key)`.
```python
async def watch():
async with etcd.connect() as communicator:
async for event in communicator.watch('testkey'):
print(event.event, event.value)
async def update():
await asyncio.sleep(0.1)
async with etcd.connect() as communicator:
await communicator.put('testkey', '1')
await communicator.put('testkey', '2')
await communicator.put('testkey', '3')
await communicator.put('testkey', '4')
await communicator.put('testkey', '5')
await asyncio.gather(watch(), update())
# WatchEventType.PUT 1
# WatchEventType.PUT 2
# WatchEventType.PUT 3
# WatchEventType.PUT 4
# WatchEventType.PUT 5
```
Watching changes on keys with specific prefix can be also done by `EtcdCommunicator.watch_prefix(key_prefix)`.
```python
async def watch():
async with etcd.connect() as communicator:
async for event in communicator.watch_prefix('/testdir'):
print(event.event, event.key, event.value)
async def update():
await asyncio.sleep(0.1)
async with etcd.connect() as communicator:
await communicator.put('/testdir', '1')
await communicator.put('/testdir/foo', '2')
await communicator.put('/testdir/bar', '3')
await communicator.put('/testdir/foo/baz', '4')
await asyncio.gather(watch(), update())
# WatchEventType.PUT /testdir 1
# WatchEventType.PUT /testdir/foo 2
# WatchEventType.PUT /testdir/bar 3
# WatchEventType.PUT /testdir/foo/baz 4
```
## Transaction
You can run etcd transaction by calling `EtcdCommunicator.txn_compare(compares, txn_builder)`.
### Constructing compares
Constructing compare operations can be done by comparing `CompareKey` instance with value with Python's built-in comparison operators (`==`, `!=`, `>`, `<`).
```python
from etcetra import CompareKey
compares = [
CompareKey('cmpkey1').value == 'foo',
CompareKey('cmpkey2').value > 'bar',
]
```
### Executing transaction calls
```python
async with etcd.connect() with communicator:
await communicator.put('cmpkey1', 'foo')
await communicator.put('cmpkey2', 'baz')
await communicator.put('successkey', 'asdf')
def _txn(success, failure):
success.get('successkey')
values = await communicator.txn_compare(compares, _txn)
print(values) # ['asdf']
```
```python
compares = [
CompareKey('cmpkey1').value == 'foo',
CompareKey('cmpkey2').value < 'bar',
]
async with etcd.connect() with communicator:
await communicator.put('failurekey', 'asdf')
def _txn(success, failure):
failure.get('failurekey')
values = await communicator.txn_compare(compares, _txn)
print(values) # ['asdf']
```
If you don't need compare conditions for transaction, you can use `EtcdCommunicator.txn(txn_builder)`,
which is a shorthand for `EtcdCommunicator.txn_compare([], lambda success, failure: txn_builder(success))`.
```python
async with etcd.connect() with communicator:
def _txn(action):
action.get('cmpkey1')
action.get('cmpkey2')
values = await communicator.txn(_txn)
print(values) # ['foo', 'baz']
```
# Contributing
## Compiling Protobuf
```bash
$ scripts/compile_protobuf.py <target Etcd version>
```
## Generating documentation
```bash
$ cd docs
$ make markdown
$ mv _build/markdown/index.mf references.md
```
Raw data
{
"_id": null,
"home_page": "https://github.com/lablup/etcetra",
"name": "etcetra",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.10",
"maintainer_email": "",
"keywords": "",
"author": "Lablup Inc.",
"author_email": "kyujin.cho@lablup.com",
"download_url": "https://files.pythonhosted.org/packages/97/52/5a0ad6cf7bfce550b1fe8787f9d99ea68776662093cd0b5638b72876ea28/etcetra-0.1.18.tar.gz",
"platform": null,
"description": "# etcetra\n\nPure python asyncio Etcd client.\n\n## Installation\n\n```bash\npip install etcetra\n```\n\n## API Documentation\n\nRefer [here](/docs/references.md).\n\n## Basic usage\n\nAll etcd operations managed by etcetra can be executed using `EtcdClient`.\n`EtcdClient` instance is a wrapper which holds connection information to Etcd channel.\nThis instance is reusable, since actual connection to gRPC channel will be established\nwhen you initiate connection calls (see below).\n\n```python\nfrom etcetra import EtcdClient, HostPortPair\netcd = EtcdClient(HostPortPair('127.0.0.1', 2379))\n```\n\nLike I mentioned above, actual connection establishment with Etcd's gRPC channel will be done\nwhen you call `EtcdClient.connect()`. This call returns async context manager, which manages `EtcdCommunicator` instance.\n\n```python\nasync with etcd.connect() as communicator:\n await communicator.put('testkey', 'testvalue')\n value = await communicator.get('testkey')\n print(value) # testvalue\n```\n\n`EtcdCommunicator.get_prefix(prefix)` will return a dictionary containing all key-values with given key prefix.\n\n```python\nasync with etcd.connect() as communicator:\n await communicator.put('/testdir', 'root')\n await communicator.put('/testdir/1', '1')\n await communicator.put('/testdir/2', '2')\n await communicator.put('/testdir/2/3', '3')\n test_dir = await communicator.get_prefix('/testdir')\n print(test_dir) # {'/testdir': 'root', '/testdir/1': '1', '/testdir/2': '2', '/testdir/2/3': '3'}\n```\n\n## Operating with Etcd lock\n\nJust like `EtcdClient.connect()`, you can easilly use etcd lock by calling `EtcdClient.with_lock(lock_name, timeout=None)`.\n\n```python\nasync def first():\n async with etcd.with_lock('foolock') as communicator:\n value = await communicator.get('testkey')\n print('first:', value, end=' | ')\n\nasync def second():\n await asyncio.sleep(0.1)\n async with etcd.with_lock('foolock') as communicator:\n value = await communicator.get('testkey')\n print('second:', value)\n\nasync with etcd.connect() as communicator:\n await communicator.put('testkey', 'testvalue')\nawait asyncio.gather(first(), second()) # first: testvalue | second: testvalue\n```\n\nAdding `timeout` parameter to `EtcdClient.with_lock()` call will add a timeout to lock acquiring process.\n\n```python\nasync def first():\n async with etcd.with_lock('foolock') as communicator:\n value = await communicator.get('testkey')\n print('first:', value)\n await asyncio.sleep(10)\n\nasync def second():\n await asyncio.sleep(0.1)\n async with etcd.with_lock('foolock', timeout=5) as communicator:\n value = await communicator.get('testkey')\n print('second:', value)\n\nasync with etcd.connect() as communicator:\n await communicator.put('testkey', 'testvalue')\nawait asyncio.gather(first(), second()) # asyncio.TimeoutError followed by first: testvalue output\n```\n\nAdding `ttl` parameter to `EtcdClient.with_lock()` call will force lock to be released after given seconds.\n\n```python\nasync def first():\n async with etcd.with_lock('foolock', ttl=5) as communicator:\n await asyncio.sleep(10)\n\nawait first()\n\n# on other file\n\nimport time\n\nasync def second():\n start = time.time()\n async with etcd.with_lock('foolock', ttl=5) as communicator:\n print(f'acquired lock after {time.time() - start} seconds')\n\nawait second() # acquired lock after 4.756163120269775 seconds\n```\n\n## Watch\n\nYou can watch changes on key with `EtcdCommunicator.watch(key)`.\n\n```python\nasync def watch():\n async with etcd.connect() as communicator:\n async for event in communicator.watch('testkey'):\n print(event.event, event.value)\n\nasync def update():\n await asyncio.sleep(0.1)\n async with etcd.connect() as communicator:\n await communicator.put('testkey', '1')\n await communicator.put('testkey', '2')\n await communicator.put('testkey', '3')\n await communicator.put('testkey', '4')\n await communicator.put('testkey', '5')\n\nawait asyncio.gather(watch(), update())\n# WatchEventType.PUT 1\n# WatchEventType.PUT 2\n# WatchEventType.PUT 3\n# WatchEventType.PUT 4\n# WatchEventType.PUT 5\n```\n\nWatching changes on keys with specific prefix can be also done by `EtcdCommunicator.watch_prefix(key_prefix)`.\n\n```python\nasync def watch():\n async with etcd.connect() as communicator:\n async for event in communicator.watch_prefix('/testdir'):\n print(event.event, event.key, event.value)\n\nasync def update():\n await asyncio.sleep(0.1)\n async with etcd.connect() as communicator:\n await communicator.put('/testdir', '1')\n await communicator.put('/testdir/foo', '2')\n await communicator.put('/testdir/bar', '3')\n await communicator.put('/testdir/foo/baz', '4')\n\nawait asyncio.gather(watch(), update())\n# WatchEventType.PUT /testdir 1\n# WatchEventType.PUT /testdir/foo 2\n# WatchEventType.PUT /testdir/bar 3\n# WatchEventType.PUT /testdir/foo/baz 4\n```\n\n## Transaction\n\nYou can run etcd transaction by calling `EtcdCommunicator.txn_compare(compares, txn_builder)`.\n\n### Constructing compares\n\nConstructing compare operations can be done by comparing `CompareKey` instance with value with Python's built-in comparison operators (`==`, `!=`, `>`, `<`).\n\n```python\nfrom etcetra import CompareKey\ncompares = [\n CompareKey('cmpkey1').value == 'foo',\n CompareKey('cmpkey2').value > 'bar',\n]\n```\n\n### Executing transaction calls\n\n```python\nasync with etcd.connect() with communicator:\n await communicator.put('cmpkey1', 'foo')\n await communicator.put('cmpkey2', 'baz')\n await communicator.put('successkey', 'asdf')\n\n def _txn(success, failure):\n success.get('successkey')\n\n values = await communicator.txn_compare(compares, _txn)\n print(values) # ['asdf']\n```\n\n```python\ncompares = [\n CompareKey('cmpkey1').value == 'foo',\n CompareKey('cmpkey2').value < 'bar',\n]\nasync with etcd.connect() with communicator:\n await communicator.put('failurekey', 'asdf')\n\n def _txn(success, failure):\n failure.get('failurekey')\n\n values = await communicator.txn_compare(compares, _txn)\n print(values) # ['asdf']\n```\n\nIf you don't need compare conditions for transaction, you can use `EtcdCommunicator.txn(txn_builder)`,\nwhich is a shorthand for `EtcdCommunicator.txn_compare([], lambda success, failure: txn_builder(success))`.\n\n```python\nasync with etcd.connect() with communicator:\n def _txn(action):\n action.get('cmpkey1')\n action.get('cmpkey2')\n\n values = await communicator.txn(_txn)\n print(values) # ['foo', 'baz']\n```\n\n# Contributing\n\n## Compiling Protobuf\n\n```bash\n$ scripts/compile_protobuf.py <target Etcd version>\n```\n\n## Generating documentation\n\n```bash\n$ cd docs\n$ make markdown\n$ mv _build/markdown/index.mf references.md\n```\n",
"bugtrack_url": null,
"license": "Apache License 2.0",
"summary": "Etcd client built with pure asyncio gRPC library",
"version": "0.1.18",
"project_urls": {
"Documentation": "https://github.com/lablup/etcetra/blob/main/docs/references.md",
"Homepage": "https://github.com/lablup/etcetra",
"Source": "https://github.com/lablup/etcetra",
"Tracker": "https://github.com/lablup/etcetra/issues"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "cce92a650363c5f6e2e565f1f87f94f5665054dc04bf42b139a362a93a7be99d",
"md5": "341514c19a944d8ae262c488c2f98704",
"sha256": "d3bd44f8d2b138b63ddf4380942dda49ad5a4edfbf06273e6e4f8eec3ad3be92"
},
"downloads": -1,
"filename": "etcetra-0.1.18-py3-none-any.whl",
"has_sig": false,
"md5_digest": "341514c19a944d8ae262c488c2f98704",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10",
"size": 62260,
"upload_time": "2023-11-14T03:55:51",
"upload_time_iso_8601": "2023-11-14T03:55:51.971630Z",
"url": "https://files.pythonhosted.org/packages/cc/e9/2a650363c5f6e2e565f1f87f94f5665054dc04bf42b139a362a93a7be99d/etcetra-0.1.18-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "97525a0ad6cf7bfce550b1fe8787f9d99ea68776662093cd0b5638b72876ea28",
"md5": "b904b19fd9ea1865c432f5337b2729f0",
"sha256": "606d6dadcc8547897c0f354b4001b38da90d95cdc0228ce6fc0d85856ca0da58"
},
"downloads": -1,
"filename": "etcetra-0.1.18.tar.gz",
"has_sig": false,
"md5_digest": "b904b19fd9ea1865c432f5337b2729f0",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10",
"size": 52390,
"upload_time": "2023-11-14T03:55:53",
"upload_time_iso_8601": "2023-11-14T03:55:53.810490Z",
"url": "https://files.pythonhosted.org/packages/97/52/5a0ad6cf7bfce550b1fe8787f9d99ea68776662093cd0b5638b72876ea28/etcetra-0.1.18.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-11-14 03:55:53",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "lablup",
"github_project": "etcetra",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "etcetra"
}