{"id":14983588,"url":"https://github.com/andriykohut/esdb-py","last_synced_at":"2025-04-10T18:45:16.648Z","repository":{"id":49794601,"uuid":"518047305","full_name":"andriykohut/esdb-py","owner":"andriykohut","description":"EventStoreDB gRPC client for Python","archived":false,"fork":false,"pushed_at":"2024-10-28T20:30:34.000Z","size":861,"stargazers_count":10,"open_issues_count":6,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-24T16:35:35.665Z","etag":null,"topics":["asyncio","cqrs","event-sourcing","eventsourcing","eventstore","grpc","grpc-client","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andriykohut.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-26T12:13:40.000Z","updated_at":"2025-01-27T01:09:30.000Z","dependencies_parsed_at":"2024-03-04T22:29:01.446Z","dependency_job_id":"b17a5d4a-f042-4e47-95a7-aa327f4aa157","html_url":"https://github.com/andriykohut/esdb-py","commit_stats":{"total_commits":316,"total_committers":2,"mean_commits":158.0,"dds":0.4177215189873418,"last_synced_commit":"345950c36c27f2e3247814b8bbf2d8b2afc39cdb"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andriykohut%2Fesdb-py","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andriykohut%2Fesdb-py/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andriykohut%2Fesdb-py/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andriykohut%2Fesdb-py/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andriykohut","download_url":"https://codeload.github.com/andriykohut/esdb-py/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248271925,"owners_count":21075800,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asyncio","cqrs","event-sourcing","eventsourcing","eventstore","grpc","grpc-client","python"],"created_at":"2024-09-24T14:07:33.241Z","updated_at":"2025-04-10T18:45:16.627Z","avatar_url":"https://github.com/andriykohut.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# esdb-py\n\n[![PyPI version](https://badge.fury.io/py/esdb.svg)](https://pypi.org/project/esdb/)\n[![codecov](https://codecov.io/gh/andriykohut/esdb-py/branch/main/graph/badge.svg?token=YVDPTDBPFB)](https://codecov.io/gh/andriykohut/esdb-py)\n\n## EventStoreDB Python gRPC client\n\n\u003e NOTE: This project is still work in progress\n\n\u003c!-- TOC --\u003e\n* [Completed features](#completed-features)\n* [Installation](#installation)\n* [Development](#development)\n* [Usage](#usage)\n  * [Connection string](#connection-string)\n  * [Discovery and node preferences](#discovery-and-node-preferences)\n  * [Connection configuration](#connection-configuration)\n  * [Append, Read, Catch-up subscriptions](#append-read-catch-up-subscriptions)\n  * [Batch append](#batch-append)\n  * [Catch-up subscription to all events with filtering](#catch-up-subscription-to-all-events-with-filtering)\n  * [Persistent subscriptions](#persistent-subscriptions)\n\u003c!-- TOC --\u003e\n\n## Completed features\n\n* [x] secure connection\n* [x] basic auth\n* [x] connection string parsing\n* [x] streams\n  * [x] append\n  * [x] batch append (v21.10+)\n  * [x] delete\n  * [x] read stream\n  * [x] read all with stream/event type filters (v21.10+)\n  * [x] catch-up subscriptions\n  * [x] tombstone\n  * [x] filtering\n* [x] persistent subscriptions\n  * [x] create\n  * [x] read stream\n  * [x] read all with filter (v21.10+)\n  * [x] update\n  * [x] delete\n  * [x] list\n  * [x] info\n  * [ ] reply parked events\n* [ ] CRUD for projections\n* [ ] users\n\n## Installation\n\nUsing pip:\n\n```sh\npip install esdb\n```\n\nUsing poetry:\n\n```sh\npoetry add esdb\n```\n\n## Development\n\n1. Install [poetry](https://python-poetry.org/docs/#installation)\n2. Create virtualenv (i.e. using pyenv):\n\n    ```sh\n    pyenv install 3.12.0\n    pyenv virtualenv 3.12.0 esdb-py\n    pyenv local esdb-py\n    ```\n\n3. Install deps with `poetry install`\n4. Start eventstore in docker: `make run-esdb`\n5. Run the tests: `pytest tests`\n\n## Usage\n\nHave a look at [tests](https://github.com/andriykohut/esdb-py/tree/main/tests) for more examples.\n\n### Connection string examples\n\nDNS discovery with credentials, discovery configuration, node preference and ca file path\n```\nesdb+discover://admin:changeit@localhost:2111?discoveryInterval=0\u0026maxDiscoverAttempts=3\u0026tlsCafile=certs/ca/ca.crt\u0026nodePreference=follower\n```\n\nSingle-node insecure connection\n```\nesdb://localhost:2111?tls=false\n```\n\nSupported parameters:\n - `discoveryInterval`\n - `gossipTimeout`\n - `maxDiscoverAttempts`\n - `nodePreference`\n - `keepAliveInterval`\n - `keepAliveTimeout`\n - `tls`\n - `tlsCafile`\n - `tlsVerifyCert`\n - `defaultDeadline`\n\n\nConnection string can be generated [here](https://developers.eventstore.com/clients/grpc/#connection-details).\n\n### Discovery and node preferences\n\n```py\nfrom esdb import ESClient\n\nclient = ESClient(\"esdb+discover://admin:changeit@localhost:2111?nodePreference=follower\")\n\n```\n\n### Connection configuration\n\n```py\nfrom esdb import ESClient\n\n# Connect without TLS\nclient = ESClient(\"esdb://localhost:2111?tls=false\")\n\n# Secure connection with basic auth and keepalive\nclient = ESClient(\"esdb://admin:changeit@localhost:2111?tlsCafile=certs/ca/ca.crt\u0026keepAliveInterval=5\u0026keepAliveTimeout=5\")\n```\n\n### Append, Read, Catch-up subscriptions\n\n```py\nimport asyncio\nimport datetime\nimport uuid\n\nfrom esdb import ESClient\n\n\nclient = ESClient(\"esdb+discover://admin:changeit@localhost:2111\")\nstream = f\"test-{str(uuid.uuid4())}\"\n\n\nasync def streams():\n    async with client.connect() as conn:\n        # Appending to stream\n        for i in range(10):\n            append_result = await conn.streams.append(\n                stream=stream,\n                event_type=\"test_event\",\n                data={\"i\": i, \"ts\": datetime.datetime.utcnow().isoformat()},\n            )\n\n        # Read up to 10 events\n        async for result in conn.streams.read(stream=stream, count=10):\n            print(result.data)\n\n        # Read up to 10 events, backwards\n        async for result in conn.streams.read(stream=stream, count=10, backwards=True):\n            print(result.data)\n\n        # Read up to 10 events, starting from 5th event\n        async for result in conn.streams.read(stream=stream, count=10, revision=5):\n            print(result.data)\n\n        # Read up to 10 events backwards, starting from 5th event\n        async for result in conn.streams.read(stream=stream, count=10, backwards=True, revision=5):\n            print(result.data)\n\n        # Create a catch-up subscription to a stream\n        async for result in conn.streams.read(stream=stream, subscribe=True):\n            print(result.data)\n\n\nasyncio.run(streams())\n```\n\n### Batch append\n\n```py\nimport asyncio\nimport uuid\n\nfrom esdb import ESClient\nfrom esdb.streams import Message\n\n\nasync def batch_append():\n    # Append multiple events in as a single batch\n    # Batch append is not supported on EventStore \u003c v21.10\n    stream = str(uuid.uuid4())\n    messages: list[Message] = [\n        Message(event_type=\"one\", data={\"item\": 1}),\n        Message(event_type=\"one\", data={\"item\": 2}),\n        Message(event_type=\"one\", data={\"item\": 3}),\n        Message(event_type=\"two\", data={\"item\": 1}),\n        Message(event_type=\"two\", data={\"item\": 2}),\n        Message(event_type=\"two\", data={\"item\": 3}),\n    ]\n    async with ESClient(\"esdb+discover://admin:changeit@localhost:2111\").connect() as conn:\n        response = await conn.streams.batch_append(stream=stream, messages=messages)\n        assert response.current_revision == 5\n        events = [e async for e in conn.streams.read(stream=stream, count=50)]\n        assert len(events) == 6\n\n\nasyncio.run(batch_append())\n```\n\n### Catch-up subscription to all events with filtering\n\n```py\nimport uuid\nimport asyncio\n\nfrom esdb import ESClient\nfrom esdb.shared import Filter\n\n\nasync def filters():\n    async with ESClient(\"esdb+discover://admin:changeit@localhost:2111\").connect() as conn:\n        # Append 10 events with the same prefix to random streams\n        for i in range(10):\n            await conn.streams.append(stream=str(uuid.uuid4()), event_type=f\"prefix-{i}\", data=b\"\")\n        # subscribe to events from all streams, filtering by event type\n        async for event in conn.streams.read_all(\n                subscribe=True,  # subscribe will wait for events, use count=\u003cn\u003e to read \u003cn\u003e events and stop\n                filter_by=Filter(\n                    kind=Filter.Kind.EVENT_TYPE,\n                    regex=\"^prefix-\",\n                    # Checkpoint only required when subscribe=True, it's not needed when using count=\u003cint\u003e\n                    checkpoint_interval_multiplier=1000,\n                ),\n        ):\n            print(event)\n\n\nasyncio.run(filters())\n```\n\n### Persistent subscriptions\n\n```python\nimport asyncio\nfrom esdb import ESClient\nfrom esdb.shared import Filter\nfrom esdb.subscriptions import SubscriptionSettings, NackAction\n\nclient = ESClient(\"esdb+discover://admin:changeit@localhost:2111\")\n\nstream = \"stream-foo\"\ngroup = \"group-bar\"\n\n\nasync def persistent():\n    async with client.connect() as conn:\n        # emit some events to the same stream\n        for i in range(50):\n            await conn.streams.append(stream, \"foobar\", {\"i\": i})\n\n        # create a stream subscription\n        await conn.subscriptions.create_stream_subscription(\n            stream=stream,\n            group_name=group,\n            settings=SubscriptionSettings(\n                max_subscriber_count=50,\n                read_batch_size=5,\n                live_buffer_size=10,\n                history_buffer_size=10,\n                consumer_strategy=SubscriptionSettings.ConsumerStrategy.ROUND_ROBIN,\n                checkpoint_ms=10000,\n            ),\n        )\n\n        # create subscription to all events with filtering\n        # Only supported on EventStore v21.10+\n        await conn.subscriptions.create_all_subscription(\n            group_name=\"subscription_group\",\n            filter_by=Filter(kind=Filter.Kind.EVENT_TYPE, regex=\"^some_type$\", checkpoint_interval_multiplier=200),\n            settings=SubscriptionSettings(\n                read_batch_size=50,\n                live_buffer_size=100,\n                history_buffer_size=100,\n                max_retry_count=2,\n                checkpoint_ms=20000,\n            ),\n        )\n\n    # read from a subscription\n    async with client.connect() as conn:\n        sub = conn.subscriptions.subscribe(stream=stream, group_name=group, buffer_size=5)\n        async for event in sub:\n            try:\n                # do work with event\n                print(event)\n                await sub.ack([event])\n            except Exception as err:\n                await sub.nack([event], NackAction.RETRY, reason=str(err))\n\n        # get subscription info\n        info = await conn.subscriptions.get_info(group, stream)\n        assert info.group_name == group\n\n        # delete subscription\n        await conn.subscriptions.delete(group, stream)\n        \n        # list subscriptions\n        subs = await conn.subscriptions.list()\n        for sub in subs:\n            print(sub.total_items)\n\n\nasyncio.run(persistent())\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandriykohut%2Fesdb-py","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandriykohut%2Fesdb-py","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandriykohut%2Fesdb-py/lists"}