etcetra


Nameetcetra JSON
Version 0.1.18 PyPI version JSON
download
home_pagehttps://github.com/lablup/etcetra
SummaryEtcd client built with pure asyncio gRPC library
upload_time2023-11-14 03:55:53
maintainer
docs_urlNone
authorLablup Inc.
requires_python>=3.10
licenseApache License 2.0
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # 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"
}
        
Elapsed time: 0.13506s